blob: 8041d71ac6bd92a086c44c4abbb5eb7caaba4749 [file] [edit]
//! Experimental subcommand to identify token-splitting locations.
//!
//! This tries to find where multi-character tokens might be candidates to be
//! split into smaller tokens. This has a fairly high false-positive rate, so
//! it can take some manual effort to analyze.
//!
//! The current analysis of places where tokens are split are exhaustively
//! listed in https://github.com/rust-lang/rust/issues/152398. That issue also
//! highlights situations where rustc fails to split tokens (since it has to
//! do it manually). This highlights a situation where it will be difficult to
//! align the reference grammar with rustc, particularly when doing
//! permutation tests.
use clap::ArgMatches;
use diagnostics::Diagnostics;
use grammar::{Expression, ExpressionKind, Grammar};
use std::collections::{HashMap, HashSet};
// Multi-character tokens that may need to be split
const MULTI_CHAR_TOKENS: &[&str] = &[
"...", "..=", "<<=", ">>=", "!=", "%=", "&&", "&=", "*=", "+=", "-=", "->", "..", "/=", "::",
"<-", "<<", "<=", "==", "=>", ">=", ">>", "^=", "|=", "||",
];
pub fn split_check(_matches: &ArgMatches) {
let mut diag = Diagnostics::new();
let grammar = grammar::load_grammar(&mut diag);
println!("Checking grammar for potential token splitting locations...\n");
// Map to store all locations where token splitting may be necessary
// Key: token, Value: list of (production_name, context)
let mut split_locations: HashMap<&str, Vec<(String, String)>> = HashMap::new();
// Check each production
for (prod_name, production) in &grammar.productions {
let mut locations_in_prod = Vec::new();
let mut visited = HashSet::new();
find_split_locations(
&grammar,
&production.expression,
&mut locations_in_prod,
prod_name,
&mut visited,
);
for (token, context) in locations_in_prod {
split_locations
.entry(token)
.or_insert_with(Vec::new)
.push((prod_name.clone(), context));
}
}
// Print results grouped by token
if split_locations.is_empty() {
println!("No potential token splitting locations found.");
} else {
for token in MULTI_CHAR_TOKENS {
if let Some(locations) = split_locations.get(token) {
println!("Token: `{}`", token);
println!(" Locations: {}", locations.len());
for (prod_name, context) in locations {
println!(" - {}: {}", prod_name, context);
}
println!();
}
}
}
}
fn find_split_locations<'a>(
grammar: &'a Grammar,
expr: &'a Expression,
locations: &mut Vec<(&'a str, String)>,
current_production: &str,
visited: &mut HashSet<String>,
) {
match &expr.kind {
ExpressionKind::Grouped(e) => {
find_split_locations(grammar, e, locations, current_production, visited);
}
ExpressionKind::Alt(es) => {
for e in es {
find_split_locations(grammar, e, locations, current_production, visited);
}
}
ExpressionKind::Sequence(es) => {
// Check for adjacent elements that might require token splitting
for (i, e) in es.iter().enumerate() {
find_split_locations(grammar, e, locations, current_production, visited);
// Check if this element could combine with following elements
// Skip non-token-producing elements (Break, Comment) when looking for the next element
if !matches!(
e.kind,
ExpressionKind::Break(_) | ExpressionKind::Comment(_)
) {
// Find the next token-producing element
for j in (i + 1)..es.len() {
let next = &es[j];
if !matches!(
next.kind,
ExpressionKind::Break(_) | ExpressionKind::Comment(_)
) {
check_adjacent_for_splits(
grammar,
e,
next,
locations,
current_production,
);
break; // Only check the immediate next token-producing element
}
}
}
}
}
ExpressionKind::Optional(e)
| ExpressionKind::NegativeLookahead(e)
| ExpressionKind::NegExpression(e)
| ExpressionKind::Cut(e) => {
find_split_locations(grammar, e, locations, current_production, visited);
}
ExpressionKind::Repeat(e) | ExpressionKind::RepeatPlus(e) => {
find_split_locations(grammar, e, locations, current_production, visited);
// Check if repeating this element could create a multi-char token
check_repeat_for_splits(grammar, e, locations, current_production, "repeat");
}
ExpressionKind::RepeatRange { expr: e, .. } | ExpressionKind::RepeatRangeNamed(e, _) => {
find_split_locations(grammar, e, locations, current_production, visited);
check_repeat_for_splits(grammar, e, locations, current_production, "repeat range");
}
ExpressionKind::Nt(_nt) => {
// Don't recurse into nonterminals - we only want to find direct uses
// and adjacent elements within the current production level.
// The main loop in split_check already visits each production.
}
ExpressionKind::Terminal(term) => {
// Check if this terminal is a multi-char token
for &multi_token in MULTI_CHAR_TOKENS {
if term == multi_token {
locations.push((
multi_token,
format!("direct use of terminal `{}`", multi_token),
));
}
}
}
ExpressionKind::Prose(_)
| ExpressionKind::Break(_)
| ExpressionKind::Comment(_)
| ExpressionKind::Charset(_)
| ExpressionKind::CharacterRange(..)
| ExpressionKind::Unicode(_) => {
// These don't contribute to token splitting
}
}
}
fn describe_expression(expr: &Expression) -> String {
match &expr.kind {
ExpressionKind::Nt(nt) => nt.clone(),
ExpressionKind::Terminal(t) => format!("terminal `{}`", t),
ExpressionKind::Optional(e) => format!("optional {}", describe_expression(e)),
ExpressionKind::Grouped(e) => format!("grouped {}", describe_expression(e)),
ExpressionKind::Repeat(e) => format!("{} repeated", describe_expression(e)),
ExpressionKind::RepeatPlus(e) => format!("{} repeated (+)", describe_expression(e)),
ExpressionKind::Alt(_) => "alternative".to_string(),
ExpressionKind::Sequence(_) => "sequence".to_string(),
ExpressionKind::Prose(p) => format!("<{}>", p),
_ => "expression".to_string(),
}
}
fn check_adjacent_for_splits<'a>(
grammar: &'a Grammar,
left: &'a Expression,
right: &'a Expression,
locations: &mut Vec<(&'a str, String)>,
_current_production: &str,
) {
// Get the possible ending tokens from the left expression
let left_endings = get_possible_endings(grammar, left);
// Get the possible starting tokens from the right expression
let right_starts = get_possible_starts(grammar, right);
// Get descriptions of the left and right elements
let left_desc = describe_expression(left);
let right_desc = describe_expression(right);
// Check if any combination could form a multi-char token
for left_end in &left_endings {
for right_start in &right_starts {
let combined = format!("{}{}", left_end, right_start);
for &multi_token in MULTI_CHAR_TOKENS {
if combined == multi_token {
// Exact match - the two elements combine to form the token
locations.push((
multi_token,
format!(
"{} ends with `{}` and can be immediately followed by {} which can start with `{}`, forming `{}`",
left_desc, left_end, right_desc, right_start, multi_token
),
));
} else if combined.starts_with(multi_token) {
// Combined is longer and starts with the token (e.g., "+=" in "+==" for token "+=")
locations.push((
multi_token,
format!(
"{} ends with `{}` followed by {} starting with `{}` could form `{}`",
left_desc, left_end, right_desc, right_start, multi_token
),
));
} else if multi_token.starts_with(&combined) {
// Token is longer than combined (e.g., "+" and "=" is partial for "+=")
// This shouldn't happen since combined should be complete, but keep for completeness
locations.push((
multi_token,
format!(
"{} ends with `{}` followed by {} starting with `{}` (partial match for `{}`)",
left_desc, left_end, right_desc, right_start, multi_token
),
));
}
}
}
}
}
fn check_repeat_for_splits<'a>(
grammar: &'a Grammar,
expr: &'a Expression,
locations: &mut Vec<(&'a str, String)>,
_current_production: &str,
repeat_type: &str,
) {
// Get the possible endings and starts from the expression
let endings = get_possible_endings(grammar, expr);
let starts = get_possible_starts(grammar, expr);
let expr_desc = describe_expression(expr);
// Check if repeating could form a multi-char token
for ending in &endings {
for start in &starts {
let combined = format!("{}{}", ending, start);
for &multi_token in MULTI_CHAR_TOKENS {
if combined == multi_token {
locations.push((
multi_token,
format!(
"{} (in {}) ends with `{}` and can be immediately followed by another {} which can start with `{}`, forming `{}`",
expr_desc, repeat_type, ending, expr_desc, start, multi_token
),
));
} else if combined.starts_with(multi_token) || multi_token.starts_with(&combined) {
locations.push((
multi_token,
format!(
"{} (in {}) ending with `{}` followed by start `{}` could form `{}`",
expr_desc, repeat_type, ending, start, multi_token
),
));
}
}
}
}
}
fn get_possible_endings(grammar: &Grammar, expr: &Expression) -> HashSet<String> {
let mut endings = HashSet::new();
get_possible_endings_impl(grammar, expr, &mut endings, &mut HashSet::new());
endings
}
fn get_possible_endings_impl(
grammar: &Grammar,
expr: &Expression,
endings: &mut HashSet<String>,
visited: &mut HashSet<String>,
) {
match &expr.kind {
ExpressionKind::Terminal(term) => {
// Extract the last character from the terminal
if let Some(last_ch) = term.chars().last() {
endings.insert(last_ch.to_string());
}
}
ExpressionKind::Grouped(e)
| ExpressionKind::Optional(e)
| ExpressionKind::NegativeLookahead(e)
| ExpressionKind::Repeat(e)
| ExpressionKind::RepeatPlus(e)
| ExpressionKind::RepeatRange { expr: e, .. }
| ExpressionKind::RepeatRangeNamed(e, _)
| ExpressionKind::NegExpression(e)
| ExpressionKind::Cut(e) => {
get_possible_endings_impl(grammar, e, endings, visited);
}
ExpressionKind::Alt(es) => {
for e in es {
get_possible_endings_impl(grammar, e, endings, visited);
}
}
ExpressionKind::Sequence(es) => {
// The ending comes from the last element in the sequence that produces tokens
// Skip trailing Breaks and Comments
for e in es.iter().rev() {
if !matches!(
e.kind,
ExpressionKind::Break(_) | ExpressionKind::Comment(_)
) {
get_possible_endings_impl(grammar, e, endings, visited);
break;
}
}
}
ExpressionKind::Nt(nt) => {
if visited.insert(nt.clone()) {
if let Some(prod) = grammar.productions.get(nt) {
get_possible_endings_impl(grammar, &prod.expression, endings, visited);
}
}
}
ExpressionKind::Charset(chars) => {
for ch in chars {
get_possible_endings_impl(grammar, ch, endings, visited);
}
}
ExpressionKind::CharacterRange(a, b) => {
// For ranges, we'll just add the boundary characters
endings.insert(a.get_ch().to_string());
endings.insert(b.get_ch().to_string());
}
ExpressionKind::Unicode((ch, _)) => {
endings.insert(ch.to_string());
}
ExpressionKind::Prose(text) => {
// Handle "Token" prose - it can be any token
if text.to_lowercase().contains("token") {
// Add all characters that could be part of multi-char tokens
for &token in MULTI_CHAR_TOKENS {
for ch in token.chars() {
endings.insert(ch.to_string());
}
}
}
}
ExpressionKind::Break(_) | ExpressionKind::Comment(_) => {
// These don't produce tokens
}
}
}
fn get_possible_starts(grammar: &Grammar, expr: &Expression) -> HashSet<String> {
let mut starts = HashSet::new();
get_possible_starts_impl(grammar, expr, &mut starts, &mut HashSet::new());
starts
}
fn get_possible_starts_impl(
grammar: &Grammar,
expr: &Expression,
starts: &mut HashSet<String>,
visited: &mut HashSet<String>,
) {
match &expr.kind {
ExpressionKind::Terminal(term) => {
// Extract the first character from the terminal
if let Some(first_ch) = term.chars().next() {
starts.insert(first_ch.to_string());
}
}
ExpressionKind::Grouped(e)
| ExpressionKind::NegativeLookahead(e)
| ExpressionKind::Repeat(e)
| ExpressionKind::RepeatPlus(e)
| ExpressionKind::RepeatRange { expr: e, .. }
| ExpressionKind::RepeatRangeNamed(e, _)
| ExpressionKind::NegExpression(e)
| ExpressionKind::Cut(e) => {
get_possible_starts_impl(grammar, e, starts, visited);
}
ExpressionKind::Optional(e) => {
get_possible_starts_impl(grammar, e, starts, visited);
// Optional also means the next element could be the start
}
ExpressionKind::Alt(es) => {
for e in es {
get_possible_starts_impl(grammar, e, starts, visited);
}
}
ExpressionKind::Sequence(es) => {
// The start comes from the first element in the sequence that produces tokens
// Skip leading Breaks and Comments
for e in es.iter() {
if !matches!(
e.kind,
ExpressionKind::Break(_) | ExpressionKind::Comment(_)
) {
get_possible_starts_impl(grammar, e, starts, visited);
break;
}
}
}
ExpressionKind::Nt(nt) => {
if visited.insert(nt.clone()) {
if let Some(prod) = grammar.productions.get(nt) {
get_possible_starts_impl(grammar, &prod.expression, starts, visited);
}
}
}
ExpressionKind::Charset(chars) => {
for ch in chars {
get_possible_starts_impl(grammar, ch, starts, visited);
}
}
ExpressionKind::CharacterRange(a, b) => {
starts.insert(a.get_ch().to_string());
starts.insert(b.get_ch().to_string());
}
ExpressionKind::Unicode((ch, _)) => {
starts.insert(ch.to_string());
}
ExpressionKind::Prose(text) => {
// Handle "Token" prose - it can be any token
if text.to_lowercase().contains("token") {
// Add all characters that could be part of multi-char tokens
for &token in MULTI_CHAR_TOKENS {
for ch in token.chars() {
starts.insert(ch.to_string());
}
}
}
}
ExpressionKind::Break(_) | ExpressionKind::Comment(_) => {
// These don't produce tokens
}
}
}