cst

    Concrete Syntax Tree library with Red-Green Tree pattern

    cst
    syntax-tree
    parser
    Download zip
    Author
    Version
    0.1.10
    License
    Apache-2.0
    Last updated
    22 days ago
    Downloads
    58K

    #mizchi/cst

    Concrete Syntax Tree library for MoonBit, based on the Red-Green Tree pattern from Rust's rowan (used in rust-analyzer).

    #Features

    • Green Tree — Immutable, position-independent, structurally shareable syntax nodes
    • Red Tree — On-demand wrapper providing absolute positions and parent navigation
    • GreenNodeBuilder — Incremental tree construction with checkpoint/wrap support
    • NodeCache — Token and node interning for memory efficiency
    • Language-agnostic — Define your own SyntaxKind set for any language

    #Install

    moon add mizchi/cst

    #Quick Start

    // Define syntax kinds for your language
    let ROOT = @cst.SyntaxKind::new(1)
    let IDENT = @cst.SyntaxKind::new(2)
    let PLUS = @cst.SyntaxKind::new(3)

    // Build a green tree
    let builder = @cst.GreenNodeBuilder::new()
    builder.start_node(ROOT)
    builder.token(IDENT, "a")
    builder.token(PLUS, "+")
    builder.token(IDENT, "b")
    builder.finish_node()
    let green = builder.finish()

    // Navigate with the red tree
    let root = @cst.SyntaxNode::new_root(green)
    let range = root.text_range() // [0, 3)
    let children = root.children() // [Token("a"), Token("+"), Token("b")]

    #Architecture

    Green Tree (immutable, shareable) Red Tree (on-demand, positional) ┌─────────────────────────────┐ ┌──────────────────────────────┐ │ GreenNode │ │ SyntaxNode │ │ ├─ kind: SyntaxKind │ ──► │ ├─ green: GreenNode │ │ ├─ text_len: TextSize │ │ ├─ parent: SyntaxNode? │ │ └─ children: [GreenChild] │ │ ├─ offset: TextSize │ │ │ │ └─ children() -> [Element] │ │ GreenToken │ │ │ │ ├─ kind: SyntaxKind │ │ SyntaxToken │ │ └─ text: String │ │ ├─ green: GreenToken │ └─────────────────────────────┘ │ ├─ parent: SyntaxNode │ │ └─ text_range() -> Range │ └──────────────────────────────┘

    The green tree stores structure without positions. The red tree wraps it to provide absolute offsets, parent references, and navigation — materialized lazily per node.

    #API

    #Building Trees

    let builder = @cst.GreenNodeBuilder::new()

    // Basic: start/finish nodes, add tokens
    builder.start_node(kind)
    builder.token(kind, "text")
    builder.finish_node()
    let green = builder.finish()

    // Checkpoint: wrap previously added tokens into a node retroactively
    let cp = builder.checkpoint()
    builder.token(NUM, "1")
    builder.token(PLUS, "+")
    builder.token(NUM, "2")
    builder.start_node_at(cp, BINARY_EXPR) // wraps all tokens since checkpoint
    builder.finish_node()

    let root = @cst.SyntaxNode::new_root(green)

    root.kind() // SyntaxKind
    root.text_range() // TextRange [start, end)
    root.text_len() // TextSize
    root.parent() // SyntaxNode?
    root.children() // Array[SyntaxElement]
    root.child_nodes() // Array[SyntaxNode]
    root.child_tokens() // Array[SyntaxToken]
    root.first_child() // SyntaxElement?
    root.child_at(i) // SyntaxElement?

    #Text Primitives

    let size = @cst.TextSize::new(5) // UTF-8 byte offset
    let range = @cst.TextRange::new( // [start, end) span
    @cst.TextSize::new(0),
    @cst.TextSize::new(5),
    )
    range.len() // TextSize
    range.contains(size) // Bool

    #Example: expr_lang

    examples/expr_lang/ contains a complete lexer, parser, and formatter for a small expression language, demonstrating how to build a language toolchain on top of this library.

    let x = 1 + 2 * 3 fn add(a, b) { a + b } add(x, 10)

    moon run examples/expr_lang/main

    #License

    Apache-2.0

    Checkpoint

    pub(all) struct Checkpoint {
    parent_idx : Int
    child_idx : Int
    } derive(Eq,
    Debug
    )

    Checkpoint - Marks a position in the builder for later wrapping

    GreenChild

    pub(all) enum GreenChild {
    Node(rel_offset~ : TextSize, node~ : GreenNode)
    Token(rel_offset~ : TextSize, token~ : GreenToken)
    } derive(Eq,
    Debug
    )

    GreenChild - Child element in a green node (either node or token)

    GreenChild::kind

    fn GreenChild::kind(self : GreenChild) -> SyntaxKind

    GreenChild::rel_offset

    fn GreenChild::rel_offset(self : GreenChild) -> TextSize

    GreenChild::text_len

    fn GreenChild::text_len(self : GreenChild) -> TextSize

    GreenNode

    pub(all) struct GreenNode {
    kind : SyntaxKind
    text_len : TextSize
    children :
    Vector
    [GreenChild]
    } derive(Eq,
    Debug
    )

    GreenNode - Immutable composite node in the green tree Contains syntax kind, total text length, and children

    GreenNode::child

    fn GreenNode::child(self : GreenNode, index : Int) -> GreenChild?

    GreenNode::children_count

    fn GreenNode::children_count(self : GreenNode) -> Int

    GreenNode::kind

    fn GreenNode::kind(self : GreenNode) -> SyntaxKind

    GreenNode::text_len

    fn GreenNode::text_len(self : GreenNode) -> TextSize

    GreenNodeBuilder

    pub(all) struct GreenNodeBuilder {
    cache : NodeCache
    parents : Array[ParentFrame]
    children : Array[GreenChild]
    }

    GreenNodeBuilder - Incrementally builds green trees

    GreenNodeBuilder::checkpoint

    fn GreenNodeBuilder::checkpoint(self : GreenNodeBuilder) -> Checkpoint

    Create a checkpoint at current position

    GreenNodeBuilder::finish

    Finish building and return the root node

    GreenNodeBuilder::finish_node

    fn GreenNodeBuilder::finish_node(self : GreenNodeBuilder) -> Unit

    Finish the current node

    GreenNodeBuilder::new

    GreenNodeBuilder::start_node

    fn GreenNodeBuilder::start_node(self : GreenNodeBuilder, kind : SyntaxKind) -> Unit

    Start a new node

    GreenNodeBuilder::start_node_at

    fn GreenNodeBuilder::start_node_at(self : GreenNodeBuilder, checkpoint : Checkpoint, kind : SyntaxKind) -> Unit

    Start a node at a previous checkpoint position

    GreenNodeBuilder::token

    fn GreenNodeBuilder::token(self : GreenNodeBuilder, kind : SyntaxKind, text : String) -> Unit

    Add a token

    GreenNodeBuilder::with_cache

    fn GreenNodeBuilder::with_cache(cache : NodeCache) -> GreenNodeBuilder

    GreenToken

    pub(all) struct GreenToken {
    kind : SyntaxKind
    text : String
    } derive(Eq, Hash,
    Debug
    )

    GreenToken - Immutable token in the green tree Contains syntax kind and the actual text

    GreenToken::kind

    fn GreenToken::kind(self : GreenToken) -> SyntaxKind

    GreenToken::new

    fn GreenToken::new(kind : SyntaxKind, text : String) -> GreenToken

    GreenToken::text

    fn GreenToken::text(self : GreenToken) -> String

    GreenToken::text_len

    fn GreenToken::text_len(self : GreenToken) -> TextSize

    NodeCache

    NodeCache - Caches interned tokens and nodes for sharing

    NodeCache::new

    fn NodeCache::new() -> NodeCache

    NodeCache::token

    fn NodeCache::token(self : NodeCache, kind : SyntaxKind, text : String) -> GreenToken

    ParentFrame

    pub(all) struct ParentFrame {
    kind : SyntaxKind
    first_child_idx : Int
    accumulated_len : TextSize
    }

    Parent frame for building nested nodes

    accumulated_len is the running sum of text_len() of the children pushed into this frame since start_node. It lets current_offset run in O(1) — the previous implementation walked all children of the current frame on every token push, which turned the overall build into O(N^2) for flat parent frames (e.g. a parser's module body that holds hundreds of top-level declaration nodes plus their interleaved whitespace / comment tokens).

    SyntaxData

    pub(all) struct SyntaxData {
    green : GreenNode
    parent : SyntaxNode?
    offset : TextSize
    } derive(Eq,
    Debug
    )

    SyntaxData - Internal data for red tree nodes

    SyntaxElement

    pub(all) enum SyntaxElement {
    Node(SyntaxNode)
    Token(SyntaxToken)
    } derive(Eq,
    Debug
    )

    SyntaxElement - Either a node or a token in the syntax tree

    SyntaxElement::as_node

    fn SyntaxElement::as_node(self : SyntaxElement) -> SyntaxNode?

    SyntaxElement::as_token

    fn SyntaxElement::as_token(self : SyntaxElement) -> SyntaxToken?

    SyntaxElement::kind

    SyntaxElement::parent

    fn SyntaxElement::parent(self : SyntaxElement) -> SyntaxNode?

    SyntaxElement::text_len

    fn SyntaxElement::text_len(self : SyntaxElement) -> TextSize

    SyntaxElement::text_range

    fn SyntaxElement::text_range(self : SyntaxElement) -> TextRange

    SyntaxKind

    pub(all) struct SyntaxKind {
    raw : UInt
    } derive(Eq, Hash,
    Debug
    )

    SyntaxKind - Language-agnostic syntax element identifier Wraps a UInt16 for efficient storage and comparison

    SyntaxKind::eof

    fn SyntaxKind::eof() -> SyntaxKind

    SyntaxKind::new

    fn SyntaxKind::new(raw : UInt) -> SyntaxKind

    SyntaxKind::raw

    fn SyntaxKind::raw(self : SyntaxKind) -> UInt

    SyntaxKind::syntax_error

    fn SyntaxKind::syntax_error() -> SyntaxKind

    SyntaxKind::tombstone

    fn SyntaxKind::tombstone() -> SyntaxKind

    SyntaxNode

    pub(all) struct SyntaxNode {
    data : SyntaxData
    } derive(Eq,
    Debug
    )

    SyntaxNode - A node in the red tree with navigation capabilities Provides absolute positions and parent references

    SyntaxNode::child_at

    fn SyntaxNode::child_at(self : SyntaxNode, index : Int) -> SyntaxElement?

    Get child at index

    SyntaxNode::child_nodes

    fn SyntaxNode::child_nodes(self : SyntaxNode) -> Array[SyntaxNode]

    Collect child nodes only

    SyntaxNode::child_tokens

    fn SyntaxNode::child_tokens(self : SyntaxNode) -> Array[SyntaxToken]

    Collect child tokens only

    SyntaxNode::children

    fn SyntaxNode::children(self : SyntaxNode) -> Array[SyntaxElement]

    Collect all children (nodes and tokens) as array

    SyntaxNode::children_count

    fn SyntaxNode::children_count(self : SyntaxNode) -> Int

    Get number of children

    SyntaxNode::first_child

    fn SyntaxNode::first_child(self : SyntaxNode) -> SyntaxElement?

    Get first child

    SyntaxNode::first_child_node

    fn SyntaxNode::first_child_node(self : SyntaxNode) -> SyntaxNode?

    Get first child node

    SyntaxNode::first_child_token

    fn SyntaxNode::first_child_token(self : SyntaxNode) -> SyntaxToken?

    Get first child token

    SyntaxNode::green

    fn SyntaxNode::green(self : SyntaxNode) -> GreenNode

    SyntaxNode::kind

    fn SyntaxNode::kind(self : SyntaxNode) -> SyntaxKind

    SyntaxNode::last_child

    fn SyntaxNode::last_child(self : SyntaxNode) -> SyntaxElement?

    Get last child

    SyntaxNode::new_root

    fn SyntaxNode::new_root(green : GreenNode) -> SyntaxNode

    SyntaxNode::parent

    fn SyntaxNode::parent(self : SyntaxNode) -> SyntaxNode?

    SyntaxNode::text_len

    fn SyntaxNode::text_len(self : SyntaxNode) -> TextSize

    SyntaxNode::text_range

    fn SyntaxNode::text_range(self : SyntaxNode) -> TextRange

    SyntaxToken

    pub(all) struct SyntaxToken {
    green : GreenToken
    parent : SyntaxNode
    offset : TextSize
    } derive(Eq,
    Debug
    )

    SyntaxToken - A token in the red tree with navigation capabilities

    SyntaxToken::green

    fn SyntaxToken::green(self : SyntaxToken) -> GreenToken

    SyntaxToken::kind

    fn SyntaxToken::kind(self : SyntaxToken) -> SyntaxKind

    SyntaxToken::new

    fn SyntaxToken::new(green : GreenToken, parent : SyntaxNode, offset : TextSize) -> SyntaxToken

    SyntaxToken::parent

    fn SyntaxToken::parent(self : SyntaxToken) -> SyntaxNode

    SyntaxToken::text

    fn SyntaxToken::text(self : SyntaxToken) -> String

    SyntaxToken::text_len

    fn SyntaxToken::text_len(self : SyntaxToken) -> TextSize

    SyntaxToken::text_range

    fn SyntaxToken::text_range(self : SyntaxToken) -> TextRange

    TextRange

    pub(all) struct TextRange {
    start : TextSize
    end : TextSize
    } derive(Eq, Hash,
    Debug
    )

    TextRange - Represents a span in text [start, end)

    TextRange::at

    fn TextRange::at(offset : TextSize, len : TextSize) -> TextRange

    TextRange::contains

    fn TextRange::contains(self : TextRange, offset : TextSize) -> Bool

    TextRange::contains_range

    fn TextRange::contains_range(self : TextRange, other : TextRange) -> Bool

    TextRange::empty

    fn TextRange::empty(offset : TextSize) -> TextRange

    TextRange::end

    fn TextRange::end(self : TextRange) -> TextSize

    TextRange::is_empty

    fn TextRange::is_empty(self : TextRange) -> Bool

    TextRange::len

    fn TextRange::len(self : TextRange) -> TextSize

    TextRange::new

    fn TextRange::new(start : TextSize, end : TextSize) -> TextRange

    TextRange::start

    fn TextRange::start(self : TextRange) -> TextSize

    TextSize

    pub(all) struct TextSize {
    raw : UInt
    } derive(Eq, Hash,
    Debug
    )

    TextSize - Represents a position in text (UTF-8 byte offset)
    impl Add for TextSize
    impl Compare for TextSize
    impl Sub for TextSize

    TextSize::from_int

    fn TextSize::from_int(n : Int) -> TextSize

    TextSize::new

    fn TextSize::new(raw : UInt) -> TextSize

    TextSize::raw

    fn TextSize::raw(self : TextSize) -> UInt

    TextSize::to_int

    fn TextSize::to_int(self : TextSize) -> Int

    TextSize::zero

    fn TextSize::zero() -> TextSize