A wildcat hands a grammar rule to a badger, who builds an LR parsing table

Borsuk

Polish for badger

A toolkit for your own compilers: parse a language from its grammar, as with Bison or ANTLR, then check its meaning and generate code from the same specification.

View on GitHub Żbik, its LR engine Build it

Three tools, one specification

ag
.ag — grammar

Unicode lexer, LR(k)/LALR(k) parser, token channels and an explicit AST with named fields and variants.

sema
.sema — grammar + semantics

Everything in .ag, plus a semantic contract and analysis: typed actions, inferred analyzer signatures and generated Rust analyzers.

coge
.coge — grammar + semantics + code

Everything in .sema, plus an IR executor, lowering and backends: C11 and LLVM for the constructs they support.

Each file is self-contained: a .sema or .coge file carries its own grammar and lexer.

Deterministic, and it tells you early

Like Bison, Borsuk builds LR tables, but with lookahead k greater than one when the grammar needs it. Conflicts are found when the table is built, for the grammar itself and without sample inputs, and the generated parser never backtracks. LL(*) and GLR parsers accept such grammars and show ambiguity only on a concrete input.

Like ANTLR, a grammar is a readable file with lexer rules, channels and an AST shape. Conflicts you accept are resolved by name, as the classic dangling else here.

Parsers are built into versioned, integrity-checked packages with C++ and Rust runtimes, or exported as static Rust parsers, so a program loads a pinned parser instead of regenerating it at startup.

grammar ToyC;

options {
    parser = LR;
    lookahead = 1;
    ast = explicit;
}

conflicts {
    prefer shift ELSE over reduce ifStatement#IfStatement;
}

node ifStatement
    : IF B thenBranch=statement                           #IfStatement
    | IF B thenBranch=statement ELSE elseBranch=statement #IfElseStatement
    ;

inline statement
    : value=ifStatement
    | value=compoundStatement
    ;

IF   : 'if';
ELSE : 'else';
WS   : [ \t\r\n]+ -> channel(HIDDEN);

An excerpt of if-else-prefer-shift.ag.

What else it has

Unicode lexing

UTF-8 input, longest match, fragments, greedy and lazy quantifiers, token channels and lexer classes scoped by the parser.

Execution and code generation

Typed IR interpreters, execution profiles and a Structured Core IR. ToyC shows the C11 and LLVM backends; ToyCP shows interpretation with classes, inheritance, construction and cleanup.

Migration and inspection

Grammar and analysis projections, specification templates, completeness reports, inspection of checked models and the provenance of every output.

Honest limits

Unsupported constructs are rejected explicitly, never half-handled. The format guides list what each tool supports today.

Build and try

Borsuk needs a C++20 compiler, CMake 4.0 or newer, ICU (uc), nlohmann_json 3.11 or newer, and the Żbik submodule. Generated Rust needs Cargo with the 2024 edition.

git clone --recursive https://github.com/parstools/borsuk.git
cd borsuk
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --target ag sema coge -j2

build/bin/ag --check ag/grammars/examples/cmm.ag
build/bin/coge --check --contracts contracts/toyc-runtime-v1.json coge/examples/toyc/toyc.coge

Normal builds use pinned parser artifacts and need neither Java nor ANTLR. The README walks through ToyC and ToyCP from specification to running interpreter.

Documentation

Most documents also have a Polish version, with the .pl.md suffix.