Skip to the content.

Getting started

This tutorial walks you through ts-bnf-tool from scratch — what it does, why it exists, and how to use it to write a working tree-sitter grammar.

The problem

Tree-sitter grammars are written in JavaScript. Not configuration — actual JavaScript, calling a DSL of functions: seq(), choice(), repeat(), optional(), token(), and so on. Here is a small fragment describing arithmetic expressions:

module.exports = grammar({
  name: "expr",

  rules: {
    expr: $ => choice(
      seq($.expr, '+', $.expr),
      seq($.expr, '*', $.expr),
      $.number,
      seq('(', $.expr, ')'),
    ),
    number: $ => /[0-9]+/,
  }
});

It works. But the syntactic structure of the language you are describing is buried under layers of JavaScript boilerplate. For a small grammar it is manageable; for a real language it quickly becomes hard to read at a glance.

The solution

ts-bnf-tool lets you write the same grammar in a compact BNF dialect and generates the grammar.js for you:

expr   -> expr '+' expr
        | expr '*' expr
        | number
        | '(' expr ')'
        ;
number -> /[0-9]+/ ;

The language is what you see. The structure is immediately apparent.

Installing

Install from crates.io:

cargo install ts-bnf-tool

Or build from source:

make build
# binary is at target/release/ts-bnf-tool after: make release

Every invocation follows the same shape:

ts-bnf-tool <SUBCOMMAND> [OPTIONS] <file.bnf>

Pass - as the filename to read from stdin.

A complete first example

Create a file called expr.bnf:

# arithmetic expressions
expr -> term ('+' term)* ;
term -> /[0-9]+/ | '(' expr ')' ;

Run the tool:

ts-bnf-tool convert expr.bnf

Output:

module.exports = grammar({
  name: "expr",

  rules: {
    expr: $ => seq($.term, repeat(seq('+', $.term))),
    term: $ => choice(/[0-9]+/, seq('(', $.expr, ')')),
  }
});

That is a ready-to-use grammar.js. Every BNF construct maps to exactly one tree-sitter DSL call — there is no hidden magic.


Next: Syntax walkthrough