Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions SYNTAX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand Down
6 changes: 3 additions & 3 deletions example/calculator/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
;

Expand All @@ -77,4 +77,4 @@ P(i32) : num {
| lparen E rparen { E }
;

E(i32) : A;
E(_) : A;
6 changes: 3 additions & 3 deletions example/calculator_u8/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@

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::<String>().parse().unwrap() };

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 {
Expand All @@ -33,4 +33,4 @@ E(f32) : E Op e2=E %prec Op {
| P
;

Op(char): '+' | '*' ;
Op(_): '+' | '*' ;
8 changes: 8 additions & 0 deletions rusty_lr_buildscript/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions rusty_lr_parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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],
}
}

Expand Down Expand Up @@ -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()
}
}
}
}
Expand Down
234 changes: 233 additions & 1 deletion rusty_lr_parser/src/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1042,6 +1048,101 @@ impl Grammar {
}
drop(pattern_map);

// Resolve placeholders ('_') using type inference
{
let mut resolved: std::collections::HashMap<String, Option<TokenStream>> = std::collections::HashMap::new();
let mut unresolved_placeholders: std::collections::HashSet<String> = 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<Option<TokenStream>> = 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));
}
}
Comment on lines +1080 to +1113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current type inference logic is order-dependent and can lead to incorrect type resolution or silent mismatches. If a non-terminal has multiple identity rules, the loop immediately breaks on the first rule that resolves (even if it resolves to None or a conflicting type), ignoring subsequent rules.

We should collect all inferred types from all identity rules, prefer concrete types over None, and ensure that all inferred concrete types are consistent (reporting a conflict error if they differ).

                        let mut inferred_types = Vec::new();

                        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(_) => {
                                            inferred_types.push(Some(grammar.token_typename.clone()));
                                        }
                                        Token::NonTerm(to_nonterm_idx) => {
                                            let target_type = &grammar.nonterminals[to_nonterm_idx].ruletype;
                                            let substituted = match target_type {
                                                Some(ts) => {
                                                    let sub = substitute_placeholders(ts.clone(), &resolved);
                                                    if sub.is_empty() {
                                                        None
                                                    } else {
                                                        Some(sub)
                                                    }
                                                }
                                                None => None,
                                            };
                                            if get_placeholder_name(&substituted).is_none() {
                                                inferred_types.push(substituted);
                                            }
                                        }
                                    }
                                }
                            }
                        }

                        if !inferred_types.is_empty() {
                            let concrete_types: Vec<TokenStream> = inferred_types.iter().filter_map(|t| t.clone()).collect();
                            let ty = if !concrete_types.is_empty() {
                                let first_ty = &concrete_types[0];
                                let first_ty_str = first_ty.to_string();
                                for ty in &concrete_types[1..] {
                                    if ty.to_string() != first_ty_str {
                                        return Err(ParseError::TypeInferenceFailed(grammar.nonterminals[i].name.location()));
                                    }
                                }
                                Some(first_ty.clone())
                            } else {
                                None
                            };
                            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,
};
Comment on lines +1139 to +1142

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update the placeholder substitution caller site to handle the new TokenStream return type from substitute_placeholders.

                nonterm.ruletype = match &nonterm.ruletype {
                    Some(ts) => {
                        let sub = substitute_placeholders(ts.clone(), &resolved);
                        if sub.is_empty() {
                            None
                        } else {
                            Some(sub)
                        }
                    }
                    None => None,
                };

}
}

// check for nonterminals in %prec,
// all production rules in that nonterminal must have precedence defined.
let mut nonterm_prec_candidates: Vec<BTreeSet<Option<usize>>> =
Expand Down Expand Up @@ -2155,6 +2256,80 @@ impl Grammar {
}
}

fn is_placeholder_type(ruletype: &Option<TokenStream>) -> 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<TokenStream>) -> Option<String> {
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<String, Option<TokenStream>>,
) -> Option<TokenStream> {
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)
}
}
Comment on lines +2294 to +2331

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current implementation of substitute_placeholders has a critical bug where any empty group (such as (), [], or {}) in a type signature is completely omitted from the output. This happens because substitute_placeholders returns None when the substituted stream is empty, and the caller completely skips extending the token stream if the result is None.

We should change substitute_placeholders to return TokenStream directly. If a group's stream is empty, it will correctly preserve the empty group (e.g., keeping () as ()).

fn substitute_placeholders(
    ts: TokenStream,
    resolved: &std::collections::HashMap<String, Option<TokenStream>>,
) -> TokenStream {
    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) => {
                let 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]);
            }
        }
    }
    new_ts
}


#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -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(_))));
}
}
Loading