Skip to main content
Tree-sitter consists of two main components: a C library (libtree-sitter) for runtime parsing, and a Rust-based CLI for parser generation.

Architecture Overview

The C Library (libtree-sitter)

The runtime library is written in plain C for maximum portability and embeddability.

Core API

The public interface is defined in tree_sitter/api.h:

Key Components

The library source is organized into focused modules:
  • parser.c - Main parsing algorithm and state management
  • lexer.c - Lexical analysis and token generation
  • node.c - Syntax tree node operations
  • language.c - Language definition and version management
  • get_changed_ranges.c - Incremental parsing support
  • alloc.c - Memory allocation with custom allocators

Memory Management

Tree-sitter uses reference counting for tree sharing:
Individual TSTree instances are NOT thread-safe. Always copy a tree before using it on multiple threads simultaneously.

Incremental Parsing

Tree-sitter achieves efficiency through incremental parsing:
The parser identifies unchanged regions and reuses subtrees, making edits extremely fast.

The CLI (tree-sitter)

The CLI is written in Rust and available via:

Parser Generation Pipeline

The tree-sitter generate command transforms grammars through several stages:

1. Grammar Parsing

Implemented in parse_grammar.rs:
The CLI shells out to Node.js to evaluate the grammar and convert it to JSON.
Grammar format is formally specified in grammar.schema.json.

2. Grammar Rules

Grammars are composed of rule types defined in rules.rs:

3. Grammar Preparation

The prepare_grammar module transforms grammars: Transformations include:
  • Inlining - Expand inline rules
  • Precedence extraction - Identify operator precedence
  • Associativity handling - Process left/right associativity
  • Token extraction - Separate tokens from syntax rules
  • Conflict resolution - Handle ambiguities
Output: Two grammars:
  • Syntax grammar - How non-terminals combine
  • Lexical grammar - How terminals (tokens) are formed

4. Parse Table Generation

The CLI generates LR parsing tables:
These tables drive the LR parser in the generated C code.

5. Code Generation

The final step emits parser.c:

Grammar DSL Deep Dive

The grammar DSL provides several powerful constructs:

Fields

Fields enable precise node queries:

Precedence

Higher numbers = higher precedence.

Conflicts

When the grammar is ambiguous:
This tells the parser that these conflicts are expected and acceptable.

External Scanner

For context-sensitive lexing:
Implemented in scanner.c or scanner.cc.

Query System Architecture

Queries use a separate compilation process:

Query Compilation

Internally:
  1. Parse query S-expression
  2. Validate against language symbols
  3. Compile to pattern-matching bytecode
  4. Index by capture names

Query Execution

Predicate System

Predicates are evaluated at runtime:
Built-in predicates:
  • #eq?, #not-eq? - String equality
  • #match?, #not-match? - Regex matching
  • #any-of?, #not-any-of? - Set membership
  • #is?, #is-not? - Property checks
  • #set! - Set properties

Language ABI Versioning

Parsers declare their ABI version:
The runtime checks compatibility:
Recompile parsers when updating Tree-sitter to avoid ABI mismatches.

Error Recovery

Tree-sitter performs automatic error recovery:

ERROR Node Insertion

When parsing fails, an ERROR node is inserted:

MISSING Node Insertion

For expected but absent tokens:

Recovery Strategy

The parser:
  1. Detects an unexpected token
  2. Inserts ERROR node
  3. Skips tokens until finding a recovery point
  4. Resumes parsing
This ensures you always get a complete tree, even for invalid code.

Performance Characteristics

Time Complexity

  • Full parse: O(n) where n = source length
  • Incremental parse: O(e log n) where e = edit size
  • Query execution: O(n × p) where p = pattern complexity

Space Complexity

  • Syntax tree: O(n) nodes
  • Parse stack: O(d) where d = max nesting depth
  • Shared subtrees: Zero extra cost due to reference counting

Optimization Techniques

  1. Subtree reuse - Unchanged regions share nodes
  2. Lazy node materialization - Nodes created on access
  3. Arena allocation - Batch allocations reduce overhead
  4. Stack-based parsing - No heap allocation during parse

Performance

Learn optimization techniques

Creating Parsers

Build your own parser