Writing the Grammar
Writing a grammar requires creativity. There are infinite context-free grammars (CFGs) that can describe any given language. To produce a good Tree-sitter parser, you need to create a grammar with two important properties:- Intuitive structure - Direct correspondence between grammar symbols and recognizable language constructs
- Close adherence to LR(1) - Efficient parsing with minimal conflicts
Tree-sitter produces a concrete syntax tree where each node corresponds to a grammar symbol. Your grammar structure directly affects the tree structure.
Starting Your Grammar
The First Few Rules
Find a formal specification for your language. As you read through the context-free grammar, you’ll discover a complex graph of relationships. Start by creating structure for basic groups:- Declarations - Top-level constructs
- Definitions - Function, class, variable definitions
- Statements - Executable code
- Expressions - Values and operations
- Types - Type annotations
- Patterns - Pattern matching constructs
Breadth-First Approach
For a language like Go, start with a skeleton:grammar.js
1
Create the skeleton
Define basic structure touching on major groups of rules.
2
Choose a sublanguage
Pick one area (types, expressions, statements) to develop first.
3
Flesh out rules
Add rules one-by-one for that sublanguage.
4
Test frequently
Use
tree-sitter parse to verify your progress with real code.5
Add tests
Write tests in
test/corpus/ for each rule you add.Structuring Rules Well
Avoid Language Spec Structure
Language specifications often have deeply nested rules that don’t translate well to syntax trees. Consider this JavaScript code:Flatten with Precedence
Create a flatter structure usingprec:
Using Precedence
Resolving Conflicts
When Tree-sitter encounters conflicts, it provides helpful error messages:The
• character shows exactly where during parsing the conflict occurs.Applying Precedence
For-a * b, we want unary - to bind tighter than binary *:
Using Associativity
Left vs Right
Fora * b * c, we need to choose between:
(a * b) * c- left associativea * (b * c)- right associative
a = b = c parses as a = (b = c)
Using Conflicts
Intentional Ambiguity
Some constructs are legitimately ambiguous. In JavaScript,[x, y] could be:
- An array literal:
let a = [x, y] - A destructuring pattern:
let [x, y] = arr
Dynamic Precedence
Useprec.dynamic to prefer one interpretation:
Hiding Rules
Underscore Prefix
Rules starting with_ are hidden from the syntax tree:
Using Fields
Named Children
Fields let you access children by name instead of index:- Code is more readable
- Resilient to grammar changes
- Self-documenting structure
Using Extras
Whitespace and Comments
Extras can appear anywhere in the language:Tree-sitter simplifies
\s to [ \t\n\r] as a performance optimization.Using Supertypes
Abstract Categories
Supertypes represent abstract categories without creating visible nodes:_expression nodes don’t appear in the tree, but can be used in queries.
Standard Rule Names
Follow these conventions for consistency:source_file
Root node representing an entire source file
expression
Choice between different expression types
statement
Choice between different statement types
block
Parent node for block scopes
type
Type annotations (int, char, void, etc.)
identifier
Variable/function names (often the
word token)string
String literals
comment
Comments (often in
extras)Lexical Analysis
Tree-sitter’s parsing is divided into two phases: parsing and lexing.Conflicting Tokens
Grammars often have tokens that match the same characters. Tree-sitter resolves conflicts using:1
Context-aware lexing
The lexer only tries to recognize tokens that are valid at the current position.
2
Lexical precedence
token(prec(N, ...)) gives explicit precedence values. Higher precedence wins.3
Match length
Prefer the token matching the longest sequence of characters.
4
Match specificity
Prefer a String (
'if') over a RegExp (/[a-z]+/) for the same match.5
Rule order
Prefer the token that appears earlier in the grammar.
Lexical vs Parse Precedence
Don’t confuse these two:- Parse precedence -
prec(N, rule)- Which rule to use for a sequence of tokens - Lexical precedence -
token(prec(N, ...))- Which token to recognize at a position
Keywords
The Word Token
Many languages have keywords (if, for, return) and a general identifier token. Without special handling, instanceofSomething would incorrectly tokenize as instanceof + Something.
Specify a word token to fix this:
How It Works
1
Keyword extraction
Tree-sitter finds all keyword tokens that match strings also matched by the
word token.2
Two-step matching
When parsing, Tree-sitter first matches the
word token, then checks if it’s a keyword.3
Better errors
instanceofSomething correctly tokenizes as one identifier, so the parser can report better errors.The
word token must be unique and not reused by another rule. If needed, use an alias instead.Complete Example
Here’s a complete grammar demonstrating these concepts:grammar.js
Next Steps
Now that you understand grammar writing:- Learn about External Scanners for complex lexical rules
- Write Tests for your grammar
- Publish your parser