Skip to content

CST and AST

The compiler keeps two views of every parsed file: a lossless CST built on rowan, and a typed AST that casts CST nodes to per-syntax wrapper structs.

CST (mty-syntax)

The CST is a rowan green/red tree of SyntaxKind-tagged nodes. It is:

  • lossless — whitespace, comments, and even error-recovery debris are preserved as trivia tokens;
  • immutable-shared — green nodes are interned and cheap to clone;
  • navigableSyntaxNode cursors give parents, children, siblings, and text spans;
  • language-agnostic — rowan does not know what FN_DECL means.

The Mighty-specific wiring is in crates/mty-syntax/src/language.rs:

pub enum Mighty {}
impl rowan::Language for Mighty { ... }

pub type SyntaxNode = rowan::SyntaxNode<Mighty>;
pub type SyntaxToken = rowan::SyntaxToken<Mighty>;
pub type GreenNode = rowan::GreenNode;

AST (mty-ast)

The AST view wraps CST nodes in typed structs. It is generated by a single declarative macro:

macro_rules! ast_node { ($name:ident, $kind:ident) => { ... } }

Every wrapper implements the AstNode trait:

pub trait AstNode: Sized {
    fn can_cast(kind: SyntaxKind) -> bool;
    fn cast(node: SyntaxNode) -> Option<Self>;
    fn syntax(&self) -> &SyntaxNode;
}

Cast a SyntaxNode into an FnDecl and access typed children:

let file: File = File::cast(SyntaxNode::new_root(green))?;
for item in file.items() {
    if let Some(f) = FnDecl::cast(item) {
        println!("fn {}", f.name()?.text());
    }
}

The generated wrappers are in crates/mty-ast/src/generated.rs. Accessors are written by hand for now (one impl per wrapper). When the AST grows large enough to make this tedious, replace generated.rs with a real codegen step.

Why both?

The CST is the right shape for formatting, syntax highlighting, and error recovery: it preserves everything in the source. The AST is the right shape for lowering and analysis: it gives you typed children without having to switch on SyntaxKind in every visitor. Sharing one green tree across both views is rowan's main draw.

See also