tree-sitter-al

June 17, 2026 · View on GitHub

A tree-sitter parser for the AL programming language used in Microsoft Dynamics 365 Business Central.

PyPI crates.io npm

Parser Status

Validated against 15,358 production AL files from the Business Central codebase:

MetricValue
Success rate100% (15,358 / 15,358 files)
Tests1,451
parser.c size~28 MB
grammar.js~3,930 lines
Named keywords82 (queryable via highlights/tags)
Scanner tokens8 (stateful, depth-tracking)
Query files6 (highlights, locals, tags, indents, folds, textobjects)

What's new in 3.0.0

Breaking parse-tree change for editor textobjects and code navigation. Every scoped construct now exposes its content as a single node via a body field (e.g. (page_declaration body: (declaration_body …)), (code_block (begin_keyword) body: (statement_block …) (end_keyword))), instead of a flat list of direct children. This powers Helix / nvim-treesitter textobjects (@class.inside, @function.inside, @parameter) via the new queries/textobjects.scm, plus a parameters field on procedures/triggers/events.

Tree-walkers and structural queries must descend through the body field — see CHANGELOG.md for the full migration guide.

Installation

Rust

cargo add tree-sitter-al
use tree_sitter::Parser;

let mut parser = Parser::new();
let language = tree_sitter_al::LANGUAGE;
parser.set_language(&language.into()).expect("Error loading AL grammar");
let tree = parser.parse("codeunit 50100 MyCodeunit { }", None).unwrap();
println!("{}", tree.root_node().to_sexp());

Query constants are also available:

use tree_sitter_al::{HIGHLIGHTS_QUERY, TAGS_QUERY, LOCALS_QUERY, FOLDS_QUERY, INDENTS_QUERY, TEXTOBJECTS_QUERY};

Python (tree-sitter 0.24+)

pip install tree-sitter-al
import tree_sitter
import tree_sitter_al

lang = tree_sitter.Language(tree_sitter_al.language())
parser = tree_sitter.Parser(lang)
tree = parser.parse(b'codeunit 50100 MyCodeunit { }')
print(tree.root_node.sexp())

Node.js

npm install tree-sitter-al

Pre-built binaries

Download from GitHub Releases:

FilePlatformUse case
tree-sitter-al.wasmAllweb-tree-sitter
tree-sitter-al.soLinux x86_64ast-grep, native bindings
tree-sitter-al.dllWindows x86_64ast-grep, native bindings
tree-sitter-al.dylibmacOS ARM64ast-grep, native bindings

V2 Architecture

The grammar was rewritten from scratch in March 2026, achieving a major reduction in parser size while improving correctness.

Before / After

MetricV1V2 (current)
parser.c106 MB (can't push to GitHub)~25 MB
Errors140
Success rate99.91%100%
Symbols2,249~814
States29,126~12,600
grammar.js8,500 lines~3,930 lines
Tests1,2251,451
Keywordsinvisible in queries82 named nodes
Query files3 (partial)6 (comprehensive)

Key design decisions

  • Stateful external scanner — 8 scanner tokens handle property disambiguation, depth tracking (#if/#endif nesting), named begin/end keywords at depth 0, and split-construct detection via lookahead.
  • Parse structure, don't validate — Accept any Name = Value ; as a property. Semantic validation belongs in linters/LSP servers, not the parser.
  • Generic preprocessor — One preproc_conditional rule + ~15 dedicated rules for genuinely complex split constructs (begin/end, var/begin, brace-close across #if/#else branches).
  • 82 named keyword nodes — All keywords including begin/end are named nodes, enabling proper syntax highlighting and code navigation queries.

See docs/v2-blog-post-notes.md for the full rewrite narrative.

Development

Prerequisites

  • Node.js (v16+)
  • tree-sitter CLI: npm install -g tree-sitter-cli

Building

tree-sitter generate    # Generate parser from grammar.js
tree-sitter test        # Run test suite

Validation

./validate-grammar.sh        # Quick: generation, tests, orphan/duplicate detection
./validate-grammar.sh --full # Full: includes production AL file parsing

For grammar refactors, the parse-tree diff harness proves a change is zero-behavior-change by re-parsing every production file and asserting byte-identical trees:

./tools/tree-harness.sh snapshot ./BC.History .snapshots/bc   # baseline
./tools/tree-harness.sh verify   ./BC.History .snapshots/bc   # verify after a change

Parsing AL files

tree-sitter parse path/to/file.al
tree-sitter parse path/to/file.al -d    # Debug output
tree-sitter parse path/to/file.al -q    # Quiet (errors only)

Key files

FilePurpose
grammar.jsMain grammar definition
src/scanner.cExternal scanner (8 tokens: property, depth tracking, named begin/end, split detection)
test/corpus/Test suite (1,451 tests)
queries/Syntax highlighting, code navigation, folding, indentation, textobjects

Contributing

See CLAUDE.md for detailed development guidelines including architecture, debugging, and conventions.


Author: Torben Leth (sshadows@sshadows.dk) License: MIT (see LICENSE)