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::<String>().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_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();
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<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));
+                        }
+                    }
+                }
+
+                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<BTreeSet<Option<usize>>> =
@@ -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)
+    }
+}
+
 #[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(_))));
+    }
 }
