Extending Syntax (New Nodes)
How to add new AST node types to bsharp_syntax.
1. Define the Node
- Add a struct or enum in the relevant module under
src/bsharp_syntax/src/. - Derive
bsharp_syntax_derive::AstNodeso it participates in traversal and rendering.
#![allow(unused)] fn main() { #[derive(bsharp_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct InterpolatedString { pub parts: Vec<InterpolatedPart>, } #[derive(bsharp_syntax_derive::AstNode, Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum InterpolatedPart { Text(String), Expr(Expression), } }
The derive implements AstNode and auto-generates children() that pushes nested nodes.
2. Implement Emit (Optional)
If the node needs to be formatted back to C#, implement Emit in bsharp_syntax emitters.
#![allow(unused)] fn main() { impl crate::emitters::emit_trait::Emit for InterpolatedString { fn emit<W: std::fmt::Write>(&self, w: &mut W, cx: &mut EmitCtx) -> Result<(), EmitError> { cx.token(w, "$")?; cx.bracketed(w, '"', '"', || { for p in &self.parts { p.emit(w, cx)?; } Ok(()) }) } } }
Add per-part emitters in the same or nearby module (e.g., emitters/expressions/...).
3. Wire Up Parser (in bsharp_parser)
- Add a parser in
src/bsharp_parser/src/expressions/...that constructs the new node. - Use
Span-based parsers (bsharp_parser::syntax::span::Span). - On errors, rely on helpers and contexts so
format_error_tree()is informative.
3a. Add Keywords & Tokens
- Define keyword helpers using
define_keyword_pair!insrc/bsharp_parser/src/keywords/. - If a new reserved word, add it to
KEYWORDS(identifier filtering). - Use
kw_*()/peek_*()in parsers, wrapped withws()at boundaries, and insert.cut()after commitment.
See: docs/parser/keywords-and-tokens.md for the macro and examples.
3b. Use Syntax Parsers (Whitespace/Lists)
- Whitespace/comments:
syntax/comment_parser.rs(ws(),parse_whitespace_or_comments()) - Lists:
syntax/list_parser.rsfor delimited/separated lists - Tokens: prefer
nom_supreme::tag::complete::tag()and compose withpreceded/terminated/delimitedandws()
Example token with trivia:
#![allow(unused)] fn main() { use nom::{combinator::map, sequence::delimited}; use nom_supreme::tag::complete::tag; use crate::syntax::comment_parser::ws; map(delimited(ws, tag(","), ws), |_| ()) }
4. Tests (bsharp_tests)
- Create tests under
src/bsharp_tests/src/parser/...verifying the node appears in the AST. - Add formatter round-trip tests if
Emitis implemented.
#![allow(unused)] fn main() { #[test] fn interpolated_string_ast() { let src = r#"class C { void M(){ var s = $"x={x}"; } }"#; let (cu, _spans) = bsharp_parser::facade::Parser::new().parse_with_spans(src).unwrap(); // Use Query to find InterpolatedString once parser supports it } }
5. Visualization (Optional)
Graph views require no changes: to_text, to_mermaid, and to_dot use AstNode traversal.
Tips
- Box recursion: Use
Box<T>for recursive enum variants. - Keep primitives out: Store
String,bool, numbers as payload only; derive will skip them. - Naming: Use PascalCase node names; no
Syntaxsuffix.