cst

Concrete Syntax Tree library with Red-Green Tree pattern

cst
syntax-tree
parser
moon add mizchi/cst@0.1.9
Download zip
Author
Version
0.1.9
License
Apache-2.0
Last updated
22 hours ago
Downloads
58K
README

#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, Show)
Checkpoint - Marks a position in the builder for later wrapping

#
GreenChild

pub(all) enum GreenChild {
Node(TextSize, GreenNode)
Token(TextSize, GreenToken)
} derive(Eq, Show)
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 :
T
[GreenChild]
} derive(Eq, Show)
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

#
GreenNode::children_count

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

#
GreenNode::kind

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

#
GreenNode::new

#
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, Show)
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, Show)
SyntaxData - Internal data for red tree nodes

#
SyntaxElement

pub(all) enum SyntaxElement {
Node(SyntaxNode)
Token(SyntaxToken)
} derive(Eq, Show)
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, Show)
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, Show)
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, Show)
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, Show)
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, Show)
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