Skip to main content

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:
  1. Intuitive structure - Direct correspondence between grammar symbols and recognizable language constructs
  2. 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.
The first rule in the rules object is the start rule (typically source_file).

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:
The ECMAScript spec represents this with 20+ levels of indirection:
Don’t create a 20-level deep tree for a simple expression. Use precedence instead.

Flatten with Precedence

Create a flatter structure using prec:

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

For a * b * c, we need to choose between:
  1. (a * b) * c - left associative
  2. a * (b * c) - right associative
Most operators are left associative:
Assignment is typically right associative:
Result: a = b = c parses as a = (b = c)

Using Conflicts

Intentional Ambiguity

Some constructs are legitimately ambiguous. In JavaScript, [x, y] could be:
  1. An array literal: let a = [x, y]
  2. A destructuring pattern: let [x, y] = arr
Only use conflicts when you have genuine ambiguity that should be resolved at runtime. Tree-sitter will use GLR parsing to explore both possibilities.

Dynamic Precedence

Use prec.dynamic to prefer one interpretation:

Hiding Rules

Underscore Prefix

Rules starting with _ are hidden from the syntax tree:
Hide rules that always wrap a single child to reduce tree depth and noise.

Using Fields

Named Children

Fields let you access children by name instead of index:
Benefits:
  • Code is more readable
  • Resilient to grammar changes
  • Self-documenting structure

Using Extras

Whitespace and Comments

Extras can appear anywhere in the language:
When adding complex patterns to extras, associate them with a rule instead of inlining. This dramatically reduces parser size.
Tree-sitter simplifies \s to [ \t\n\r] as a performance optimization.

Using Supertypes

Abstract Categories

Supertypes represent abstract categories without creating visible nodes:
Effect: _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.
External scanners have priority over all these rules. See External Scanners for details.

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.
Keyword extraction also improves performance by generating a smaller, simpler lexing function.
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: