Skip to content

Syntax Nodes

David Sisco edited this page Jan 9, 2026 · 4 revisions

Syntax Nodes

Syntax nodes provide pattern-based AST matching to create typed wrapper objects for structural constructs.

Quick Start

using TinyTokenizer.Ast;

// Parse with schema (required for syntax node matching)
var tree = SyntaxTree.Parse("foo(x) + bar.baz", Schema.Default);

// Find all function calls
var funcCalls = tree.Match<FunctionCallSyntax>().ToList();  // [FunctionCallSyntax { Name="foo" }]

// Find all property accesses
var props = tree.Match<PropertyAccessSyntax>().ToList();  // [PropertyAccessSyntax { Object="bar", Property="baz" }]

Built-in Syntax Nodes

Type Pattern Matches Properties
FunctionCallSyntax Ident + ParenBlock foo(x) Name, Arguments
ArrayAccessSyntax Ident + BracketBlock arr[0] Name, Index
PropertyAccessSyntax Ident + . + Ident obj.prop Object, Property
MethodCallSyntax Ident + . + Ident + ParenBlock obj.method(x) Object, Method, Arguments

Using Built-in Nodes

var tree = SyntaxTree.Parse("obj.method(x, y)", Schema.Default);

var methods = tree.Match<MethodCallSyntax>().ToList();  // [MethodCallSyntax { Object="obj", Method="method" }]
foreach (var call in methods)
{
    Console.WriteLine($"{call.Object}.{call.Method}(...)");
}

Creating Custom Syntax Nodes

Step 1: Define the Node Class

public sealed class LambdaSyntax : SyntaxNode
{
    // Constructor receives opaque CreationContext - pass to base
    protected LambdaSyntax(CreationContext context)
        : base(context) { }

    // Access children by index (order matches pattern)
    public SyntaxBlock Parameters => GetTypedChild<SyntaxBlock>(0);
    public SyntaxToken Arrow => GetTypedChild<SyntaxToken>(1);
    public SyntaxBlock Body => GetTypedChild<SyntaxBlock>(2);

    // Convenience properties
    public string ArrowText => Arrow.Text;
}

Step 2: Create a Definition

var lambdaDef = Syntax.Define<LambdaSyntax>("Lambda")
    .Match(
        Query.ParenBlock,           // Parameters (index 0)
        Query.Operator("=>"),       // Arrow (index 1)
        Query.BraceBlock            // Body (index 2)
    )
    .WithPriority(15)  // Optional: higher = matched first
    .Build();

Step 3: Add to Schema

var schema = Schema.Create()
    .WithOperators(CommonOperators.JavaScript)
    .Define(lambdaDef)
    .Build();

Step 4: Match

var tree = SyntaxTree.Parse("(x) => { return x; }", schema);
var lambdas = tree.Match<LambdaSyntax>().ToList();  // [LambdaSyntax { Arrow="=>" }]

foreach (var lambda in lambdas)
{
    Console.WriteLine($"Lambda with body: {lambda.Body.ToFullString()}");
}

INamedNode Interface

Implement INamedNode to enable querying by name:

public sealed class FunctionSyntax : SyntaxNode, INamedNode
{
    protected FunctionSyntax(CreationContext context) : base(context) { }

    // Expose the name for querying
    public string Name => GetTypedChild<SyntaxToken>(1).Text;

    public SyntaxToken Keyword => GetTypedChild<SyntaxToken>(0);     // "function"
    public SyntaxBlock Parameters => GetTypedChild<SyntaxBlock>(2); // (...)
    public SyntaxBlock Body => GetTypedChild<SyntaxBlock>(3);       // {...}
}

Querying Named Nodes

var tree = SyntaxTree.Parse("function main() { } function helper() { }", schema);

// Find functions by name
var mainFunc = tree.Select(
    Query.Syntax<FunctionSyntax>().Named("main")  // Matches function named "main"
).FirstOrDefault();

// Use with editor
tree.CreateEditor()
    .InsertBefore(Query.Syntax<FunctionSyntax>().Named("foo"), "// doc\n")
    .Commit();

IBlockContainerNode Interface

Implement IBlockContainerNode to expose named blocks for injection:

public sealed class FunctionSyntax : SyntaxNode, INamedNode, IBlockContainerNode
{
    protected FunctionSyntax(CreationContext context) : base(context) { }

    public string Name => GetTypedChild<SyntaxToken>(1).Text;

    // IBlockContainerNode - expose available block names
    public IReadOnlyList<string> BlockNames => ["body", "params"];

    public SyntaxBlock GetBlock(string? name = null) => name switch
    {
        null or "body" => GetTypedChild<SyntaxBlock>(3),   // Default: body
        "params" => GetTypedChild<SyntaxBlock>(2),
        _ => throw new ArgumentException($"Unknown block: {name}")
    };
}

Injecting into Named Blocks

var mainQuery = Query.Syntax<FunctionSyntax>().Named("main");

tree.CreateEditor()
    .InsertAfter(mainQuery.InnerStart("body"), "\n    console.log('enter');")
    .InsertBefore(mainQuery.InnerEnd("body"), "\n    console.log('exit');")
    .InsertAfter(mainQuery.InnerStart("params"), "int x")
    .Commit();

Definition Priority

When multiple patterns could match, higher priority wins:

// MethodCall should match before FunctionCall
var methodDef = Syntax.Define<MethodCallSyntax>("MethodCall")
    .Match(
        Query.AnyIdent,
        Query.Symbol("."),
        Query.AnyIdent,
        Query.ParenBlock
    )
    .WithPriority(20)  // Higher priority
    .Build();

var funcDef = Syntax.Define<FunctionCallSyntax>("FunctionCall")
    .Match(Query.AnyIdent, Query.ParenBlock)
    .WithPriority(10)  // Lower priority
    .Build();

Accessing Children

Use GetTypedChild<T>(index) based on pattern order:

// Pattern: .Match(Query.AnyIdent, Query.Symbol("."), Query.AnyIdent)
//          Index:         0              1                   2

public sealed class PropertyAccessSyntax : SyntaxNode
{
    public SyntaxToken Object => GetTypedChild<SyntaxToken>(0);     // First ident
    public SyntaxToken Dot => GetTypedChild<SyntaxToken>(1);        // The dot
    public SyntaxToken Property => GetTypedChild<SyntaxToken>(2);   // Second ident
}

Full Custom Example

// 1. Node class
public sealed class IfStatementSyntax : SyntaxNode, IBlockContainerNode
{
    protected IfStatementSyntax(CreationContext context) : base(context) { }

    public SyntaxToken IfKeyword => GetTypedChild<SyntaxToken>(0);
    public SyntaxBlock Condition => GetTypedChild<SyntaxBlock>(1);
    public SyntaxBlock Body => GetTypedChild<SyntaxBlock>(2);

    public IReadOnlyList<string> BlockNames => ["condition", "body"];

    public SyntaxBlock GetBlock(string? name = null) => name switch
    {
        null or "body" => Body,
        "condition" => Condition,
        _ => throw new ArgumentException($"Unknown block: {name}")
    };
}

// 2. Definition
var ifDef = Syntax.Define<IfStatementSyntax>("IfStatement")
    .Match(
        Query.Ident("if"),
        Query.ParenBlock,
        Query.BraceBlock
    )
    .Build();

// 3. Schema
var schema = Schema.Create()
    .Define(ifDef)
    .Build();

// 4. Usage
var tree = SyntaxTree.Parse("if (x) { doSomething(); }", schema);
var ifs = tree.Match<IfStatementSyntax>();

tree.CreateEditor()
    .InsertAfter(Query.Syntax<IfStatementSyntax>().First().InnerStart("body"),
            "\n    console.log('condition was true');")
    .Commit();

See Also

Clone this wiki locally