From 98e1b2b54b97cef8fd02630b1e370a9325297bb6 Mon Sep 17 00:00:00 2001 From: Taehwan Kim Date: Thu, 18 Jun 2026 15:50:30 +0900 Subject: [PATCH 1/2] type inference --- README.md | 8 + SYNTAX.md | 7 + example/calculator/src/parser.rs | 6 +- example/calculator_u8/src/parser.rs | 6 +- rusty_lr_parser/src/error.rs | 7 + rusty_lr_parser/src/grammar.rs | 234 +++++++++++++++++++++++++++- 6 files changed, 261 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c9e1b82b..8ecf2fd9 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,14 @@ This visualization helps you understand the parsing process step-by-step and is RustyLR's grammar syntax is inspired by traditional Yacc/Bison formats. See [SYNTAX.md](SYNTAX.md) for detailed grammar definition syntax. +### Type Inference with `_` +Assigning a type to a non-terminal can be done automatically using the `_` placeholder. + +```rust +E(_): A; +``` +When `_` is used, RustyLR will infer the type by examining `Identity` reduce actions. If a circular dependency prevents type resolution, a compilation error is raised. + ## Contributing Contributions are welcome! Please feel free to open an issue or submit a pull request. diff --git a/SYNTAX.md b/SYNTAX.md index dc358261..1f90ef43 100644 --- a/SYNTAX.md +++ b/SYNTAX.md @@ -162,6 +162,13 @@ E(MyType): ... ; ``` - `MyType`: The Rust type associated with the non-terminal `E` +### Type Inference with Placeholder `_` +You can use `_` as a placeholder to let `rusty_lr` automatically infer the type of a non-terminal. +```rust +E(_): A; +``` +If `_` is used, the type will be inferred by analyzing the rules (specifically focusing on `Identity` actions/rules that map directly to another typed token or non-terminal). If a circular dependency prevents inference, the parser will return a compilation error. + The actual value of `E` is evaluated by the result of the ReduceAction. diff --git a/example/calculator/src/parser.rs b/example/calculator/src/parser.rs index 2838b78b..b5417149 100644 --- a/example/calculator/src/parser.rs +++ b/example/calculator/src/parser.rs @@ -46,7 +46,7 @@ fn filter( term: &Token ) -> &Token { // data that each token holds can be accessed by its name // s is slice of shifted terminal symbols captured by current rule // userdata can be accessed by `data` ( &mut i32, for this situation ) -A(i32) : A plus a2=A { +A(_) : A plus a2=A { println!("{:?} {:?} {:?}", A, plus, a2 ); // ^ ^ ^ // | | |- value of 2nd 'A' @@ -61,7 +61,7 @@ A(i32) : A plus a2=A { | M ; -M(i32) : M_optim star m2=M_optim { M_optim * m2 } +M(_) : M_optim star m2=M_optim { M_optim * m2 } | P ; @@ -77,4 +77,4 @@ P(i32) : num { | lparen E rparen { E } ; -E(i32) : A; +E(_) : A; diff --git a/example/calculator_u8/src/parser.rs b/example/calculator_u8/src/parser.rs index 24a03df1..c3d5690d 100644 --- a/example/calculator_u8/src/parser.rs +++ b/example/calculator_u8/src/parser.rs @@ -10,7 +10,7 @@ WS0: ' '*; -Digit(char): ['6'-'9'] | "0" {'0'} | '1' | '2' | '3' | '4' | '5'; +Digit(_): ['6'-'9'] | "0" {'0'} | '1' | '2' | '3' | '4' | '5'; Number(i32): WS0 Digit+ WS0 { Digit.into_iter().collect::().parse().unwrap() }; @@ -18,7 +18,7 @@ P(f32): Number { Number as f32 } | WS0 '(' E ')' WS0 { E } ; -E(f32) : E Op e2=E %prec Op { +E(_) : E Op e2=E %prec Op { *data += 1; // access userdata by `data` println!( "{:?} {:?} {:?}", E, Op, e2 ); match Op { @@ -33,4 +33,4 @@ E(f32) : E Op e2=E %prec Op { | P ; -Op(char): '+' | '*' ; +Op(_): '+' | '*' ; diff --git a/rusty_lr_parser/src/error.rs b/rusty_lr_parser/src/error.rs index 3f35ba1b..146f95b9 100644 --- a/rusty_lr_parser/src/error.rs +++ b/rusty_lr_parser/src/error.rs @@ -126,6 +126,9 @@ pub enum ParseError { name: String, max: usize, }, + + /// type inference failed for NonTerminal's ruletype placeholder '_' + TypeInferenceFailed(Location), } #[allow(unused)] impl ArgError { @@ -257,6 +260,7 @@ impl ParseError { ParseError::OnlyUsizeLiteral(loc) => vec![*loc], ParseError::BisonVariableZero(loc) => vec![*loc], ParseError::BisonVariableOutOfRange { location, .. } => vec![*location], + ParseError::TypeInferenceFailed(location) => vec![*location], } } @@ -314,6 +318,9 @@ impl ParseError { ParseError::BisonVariableOutOfRange { name, max, .. } => { format!("bison variable {} is out of range (max: {})", name, max) } + ParseError::TypeInferenceFailed(_) => { + "type inference failed: circular dependency or no identity action found".to_string() + } } } } diff --git a/rusty_lr_parser/src/grammar.rs b/rusty_lr_parser/src/grammar.rs index 5a071b2e..70884bed 100644 --- a/rusty_lr_parser/src/grammar.rs +++ b/rusty_lr_parser/src/grammar.rs @@ -539,10 +539,16 @@ impl Grammar { // insert rule typenames first, since it will be used when inserting rule definitions below for (rule_idx, rules_arg) in grammar_args.rules.iter().enumerate() { + let ruletype = if is_placeholder_type(&rules_arg.typename) { + let placeholder_name = format_ident!("__rustylr_placeholder_{}", rules_arg.name.value()); + Some(quote! { #placeholder_name }) + } else { + rules_arg.typename.clone() + }; let nonterminal = NonTerminalInfo { name: rules_arg.name.clone(), pretty_name: rules_arg.name.value().clone(), - ruletype: rules_arg.typename.clone(), + ruletype, rules: Vec::new(), // production rules will be added later root_location: None, trace: false, @@ -1042,6 +1048,101 @@ impl Grammar { } drop(pattern_map); + // Resolve placeholders ('_') using type inference + { + let mut resolved: std::collections::HashMap> = std::collections::HashMap::new(); + let mut unresolved_placeholders: std::collections::HashSet = std::collections::HashSet::new(); + + for nonterm in &grammar.nonterminals { + if let Some(name) = get_placeholder_name(&nonterm.ruletype) { + unresolved_placeholders.insert(name); + } + } + + let mut changed = true; + while changed { + changed = false; + let mut resolved_this_round = Vec::new(); + + for p_name in &unresolved_placeholders { + // Find the nonterminal index corresponding to p_name + let mut nonterm_idx = None; + for (idx, nonterm) in grammar.nonterminals.iter().enumerate() { + if let Some(name) = &nonterm.ruletype { + if name.to_string() == *p_name { + nonterm_idx = Some(idx); + break; + } + } + } + + if let Some(i) = nonterm_idx { + let mut inferred_type: Option> = None; + + for rule in &grammar.nonterminals[i].rules { + if let Some(ReduceAction::Identity(idx)) = &rule.reduce_action { + if *idx < rule.tokens.len() { + let token = rule.tokens[*idx].token; + match token { + Token::Term(_) => { + let ty = Some(grammar.token_typename.clone()); + inferred_type = Some(ty); + break; + } + Token::NonTerm(to_nonterm_idx) => { + let target_type = &grammar.nonterminals[to_nonterm_idx].ruletype; + // Substitute any already resolved placeholders + let substituted = match target_type { + Some(ts) => substitute_placeholders(ts.clone(), &resolved), + None => None, + }; + // Check if the substituted type contains any placeholders + if get_placeholder_name(&substituted).is_none() { + inferred_type = Some(substituted); + break; + } + } + } + } + } + } + + if let Some(ty) = inferred_type { + resolved_this_round.push((p_name.clone(), ty)); + } + } + } + + for (p_name, ty) in resolved_this_round { + resolved.insert(p_name.clone(), ty); + unresolved_placeholders.remove(&p_name); + changed = true; + } + } + + if !unresolved_placeholders.is_empty() { + let first_p_name = unresolved_placeholders.iter().next().unwrap(); + let mut loc = Location::CallSite; + for nonterm in &grammar.nonterminals { + if let Some(name) = &nonterm.ruletype { + if name.to_string() == *first_p_name { + loc = nonterm.name.location(); + break; + } + } + } + return Err(ParseError::TypeInferenceFailed(loc)); + } + + // Substitute resolved types into all nonterminal ruletypes (including helpers) + for nonterm in &mut grammar.nonterminals { + nonterm.ruletype = match &nonterm.ruletype { + Some(ts) => substitute_placeholders(ts.clone(), &resolved), + None => None, + }; + } + } + // check for nonterminals in %prec, // all production rules in that nonterminal must have precedence defined. let mut nonterm_prec_candidates: Vec>> = @@ -2155,6 +2256,80 @@ impl Grammar { } } +fn is_placeholder_type(ruletype: &Option) -> bool { + if let Some(ts) = ruletype { + let mut it = ts.clone().into_iter(); + if let Some(proc_macro2::TokenTree::Ident(ident)) = it.next() { + if ident.to_string() == "_" && it.next().is_none() { + return true; + } + } + } + false +} + + +fn get_placeholder_name(ruletype: &Option) -> Option { + if let Some(ts) = ruletype { + for token in ts.clone() { + match token { + proc_macro2::TokenTree::Ident(ident) => { + let s = ident.to_string(); + if s.starts_with("__rustylr_placeholder_") { + return Some(s); + } + } + proc_macro2::TokenTree::Group(group) => { + if let Some(name) = get_placeholder_name(&Some(group.stream())) { + return Some(name); + } + } + _ => {} + } + } + } + None +} + +fn substitute_placeholders( + ts: TokenStream, + resolved: &std::collections::HashMap>, +) -> Option { + let mut new_ts = TokenStream::new(); + for token in ts { + match token { + proc_macro2::TokenTree::Ident(ident) => { + let s = ident.to_string(); + if s.starts_with("__rustylr_placeholder_") { + if let Some(replacement_opt) = resolved.get(&s) { + if let Some(replacement) = replacement_opt { + new_ts.extend(replacement.clone()); + } + } else { + new_ts.extend([proc_macro2::TokenTree::Ident(ident)]); + } + } else { + new_ts.extend([proc_macro2::TokenTree::Ident(ident)]); + } + } + proc_macro2::TokenTree::Group(group) => { + if let Some(sub) = substitute_placeholders(group.stream(), resolved) { + let new_group = proc_macro2::Group::new(group.delimiter(), sub); + new_ts.extend([proc_macro2::TokenTree::Group(new_group)]); + } + } + other => { + new_ts.extend([other]); + } + } + } + if new_ts.is_empty() { + None + } else { + Some(new_ts) + } +} + #[cfg(test)] mod tests { use super::*; @@ -2350,4 +2525,61 @@ mod tests { let grammar = Grammar::from_grammar_args(grammar_args); assert!(matches!(grammar, Err(ParseError::BisonVariableOutOfRange { .. }))); } + + #[test] + fn test_type_inference_simple() { + let input = quote! { + %tokentype char; + %start Expr; + Expr(_) : 'a' { $1 }; + }; + let grammar_args = Grammar::parse_args(input).expect("Failed to parse grammar args"); + let grammar = Grammar::from_grammar_args(grammar_args).expect("Failed to build grammar"); + let expr_idx = grammar.nonterminals_index.get("Expr").unwrap(); + let ruletype = &grammar.nonterminals[*expr_idx].ruletype; + assert_eq!(ruletype.as_ref().unwrap().to_string(), "char"); + } + + #[test] + fn test_type_inference_auto_identity() { + let input = quote! { + %tokentype char; + %start Expr; + Expr(_) : 'a'; + }; + let grammar_args = Grammar::parse_args(input).expect("Failed to parse grammar args"); + let grammar = Grammar::from_grammar_args(grammar_args).expect("Failed to build grammar"); + let expr_idx = grammar.nonterminals_index.get("Expr").unwrap(); + let ruletype = &grammar.nonterminals[*expr_idx].ruletype; + assert_eq!(ruletype.as_ref().unwrap().to_string(), "char"); + } + + #[test] + fn test_type_inference_multi_step() { + let input = quote! { + %tokentype char; + %start Expr; + Expr(_) : Term { $1 }; + Term(_) : 'a' { $1 }; + }; + let grammar_args = Grammar::parse_args(input).expect("Failed to parse grammar args"); + let grammar = Grammar::from_grammar_args(grammar_args).expect("Failed to build grammar"); + let expr_idx = grammar.nonterminals_index.get("Expr").unwrap(); + let term_idx = grammar.nonterminals_index.get("Term").unwrap(); + assert_eq!(grammar.nonterminals[*expr_idx].ruletype.as_ref().unwrap().to_string(), "char"); + assert_eq!(grammar.nonterminals[*term_idx].ruletype.as_ref().unwrap().to_string(), "char"); + } + + #[test] + fn test_type_inference_circular_dependency() { + let input = quote! { + %tokentype char; + %start Expr; + Expr(_) : Term { $1 }; + Term(_) : Expr { $1 }; + }; + let grammar_args = Grammar::parse_args(input).expect("Failed to parse grammar args"); + let grammar = Grammar::from_grammar_args(grammar_args); + assert!(matches!(grammar, Err(ParseError::TypeInferenceFailed(_)))); + } } From 21f8f262cf1b98e2ebae2d7178b2e0baa0a1a56d Mon Sep 17 00:00:00 2001 From: Taehwan Kim Date: Thu, 18 Jun 2026 15:53:51 +0900 Subject: [PATCH 2/2] buildscript error --- rusty_lr_buildscript/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rusty_lr_buildscript/src/lib.rs b/rusty_lr_buildscript/src/lib.rs index 49a359f2..4f851ad9 100644 --- a/rusty_lr_buildscript/src/lib.rs +++ b/rusty_lr_buildscript/src/lib.rs @@ -830,6 +830,14 @@ impl Builder { .with_notes(vec![format!("Bison variable {} is out of range (max index: {})", name, max)]) } + ParseError::TypeInferenceFailed(location) => { + let range = span_manager.get_byterange(&location).unwrap_or(0..0); + Diagnostic::error() + .with_message("type inference failed: circular dependency or no identity action found") + .with_labels(vec![Label::primary(file_id, range) + .with_message("failed to infer type for this placeholder")]) + } + _ => { let message = e.short_message(); let mut labels = Vec::new();