---
title: "Examples · tabnas"
description: "Real languages built on the tabnas engine: a chess game to step through, semver strings parsed and ranked, a GBNF grammar to test, aontu in TS, and boru in Go."
source: "https://tabnas.dev/examples/"
---

# Examples

Three small languages running in this page, and two large ones you can go and read. One parses chess notation, one the version strings on every release page, and one the grammars that constrain what a language model may say. Of the large ones, one composes existing grammar plugins into a configuration language, and the other builds a full programming language and its toolchain.

## chess

PGN and SAN: the notation chess games are written in

Runtime

TypeScript · also ported to Go

Built with

[@tabnas/parser](https://github.com/tabnas/parser)

Source

[github.com/tabnas/chess](https://github.com/tabnas/chess)

The input, in full

```
[Event "Casual game"]
[Site "London ENG"]
[Date "1851.06.21"]
[White "Adolf Anderssen"]
[Black "Lionel Kieseritzky"]
[Result "1-0"]

1. e4 e5 2. f4 exf4 3. Bc4 Qh4+ 4. Kf1 b5 5. Bxb5 Nf6 6. Nf3 Qh6
7. d3 Nh5 8. Nh4 Qg5 9. Nf5 c6 10. g4 Nf6 11. Rg1 cxb5 12. h4 Qg6
13. h5 Qg5 14. Qf3 Ng8 15. Bxf4 Qf6 16. Nc3 Bc5 17. Nd5 Qxb2 18. Bd6 Bxg1
19. e5 Qxa1+ 20. Ke2 Na6 21. Nxg7+ Kd8 22. Qf6+ Nxf6 23. Be7# 1-0
```

\[Event "Casual game"\] \[Site "London ENG"\] \[Date "1851.06.21"\] \[White "Adolf Anderssen"\] \[Black "Lionel Kieseritzky"\] \[Result "1-0"\] 1. e4 e5 2. f4 exf4 3. Bc4 Qh4+ 4. Kf1 b5 5. Bxb5 Nf6 6. Nf3 Qh6 7. d3 Nh5 8. Nh4 Qg5 9. Nf5 c6 10. g4 Nf6 11. Rg1 cxb5 12. h4 Qg6 13. h5 Qg5 14. Qf3 Ng8 15. Bxf4 Qf6 16. Nc3 Bc5 17. Nd5 Qxb2 18. Bd6 Bxg1 19. e5 Qxa1+ 20. Ke2 Na6 21. Nxg7+ Kd8 22. Qf6+ Nxf6 23. Be7# 1-0

Anderssen–Kieseritzky, London, 21 June 1851: the Immortal Game. Use the controls, the arrow keys, or click any move. The notation below the board is **editable**: change it and the board re-parses as you type, so you can watch the grammar accept and reject. The board, the move list and the parser are one 43 kB script with no other requests.

That text is the whole input: a PGN game, parsed by [@tabnas/chess](https://github.com/tabnas/chess) into tag pairs, moves, and a result. Everything on the board comes from the parse: the players and date above it are the tag pairs, and each move comes back as structure rather than as a string: `Nxg7+` is a knight, a capture, a destination of `g7` and a check. That is what makes the move list clickable and lets the board follow along.

It is also a good example of where a parser stops. `Nf3` names a piece and a square but not _which_ knight; only the position knows, and the grammar has no position. So the plugin does not pretend to have a board, and the component supplies the missing half: a legal move generator that resolves each parsed move against the position it is played in. Parsing the notation and playing the game are two jobs, and only one of them is the engine's.

## semver

Semantic Versioning 2.0.0: the version strings on every release page

Runtime

TypeScript · also ported to Go

Built with

[@tabnas/parser](https://github.com/tabnas/parser) [@tabnas/abnf](https://github.com/tabnas/abnf)

Source

[github.com/tabnas/semver](https://github.com/tabnas/semver)

simple 0.9.0 1.0.0 1.2.3 2.0.0 complex 1.0.0-beta.2 1.0.0-beta.11 1.0.0-alpha.beta.11+exp.sha.5114f85 1.0.0-x-y-z.-- 9007199254740992.0.0 v1.2.3

Both boxes are **editable**, and every line re-parses as you type. The left one holds ordinary releases. The right one holds the cases that separate a parser from a pattern match: an identifier that sorts by value rather than by character, one made of nothing but hyphens, an integer past `Number.MAX_SAFE_INTEGER`, and a `v` prefix the specification does not allow.

An accepted line comes back as five fields rather than as a string. `1.0.0-alpha.beta.11+exp.sha.5114f85` gives a core of `1`, `0`, `0`, a `prerelease` of `["alpha", "beta", 11]` and a `build` of `["exp", "sha", "5114f85"]`. The `11` is a number and `"beta"` a string because the specification ranks the two kinds by different rules, so the difference has to survive the parse. An integer above `2^53 - 1` comes back as a `bigint` rather than rounded to the nearest double.

The specification publishes its grammar, in BNF. The plugin transcribes it into RFC 5234 ABNF, one production per production and under the same names, and [@tabnas/abnf](https://github.com/tabnas/abnf) compiles that text into the engine's rule set when the plugin is installed. Nothing in the plugin's code decides what a valid version is. That the result accepts exactly what semver.org accepts is checked rather than asserted: both runtimes generate a corpus of 58,449 strings and compare every verdict, every value, and every round trip against the regular expression semver.org publishes for the purpose.

Ranking versions is a second job. Section 11 puts `1.0.0-beta.2` below `1.0.0-beta.11`, puts any pre-release below the release it leads to, and gives build metadata no weight at all. A grammar can state none of that. So `compare` sits beside the parser as an ordinary function over parsed values, and the strip under the verdicts is the accepted lines above passed through it and `Array.prototype.sort`. The package is new, and that corpus is the reason to trust it rather than its time in service.

## gbnf

GBNF: the grammars that constrain what a model may say

Runtime

TypeScript · also ported to Go, C ABI, Python

Built with

[@tabnas/parser](https://github.com/tabnas/parser) [@tabnas/bnf](https://github.com/tabnas/bnf)

Source

[github.com/tabnas/gbnf](https://github.com/tabnas/gbnf)

grammar root ::= answer " (" conf ")" answer ::= "yes" | "no" conf ::= digit+ "%" digit ::= \[0-9\] samples, one per line yes (90%) no (5%) maybe (50%) yes (90) yes (90%)

Both boxes are **editable**, and the verdicts re-run as you type. Every character counts: GBNF is scannerless, so `yes  (90%)` is rejected for the second space. Break the grammar itself and you get a compile error instead of verdicts, which is the distinction the tool exists to keep.

GBNF is llama.cpp's notation for **constrained decoding**: at each step the sampler masks every token that would leave the grammar's language, so the model cannot emit malformed output. XGrammar reads it too, and therefore so do vLLM and SGLang, along with KoboldCpp, LocalAI and node-llama-cpp.

In all of them the grammar runs inside generation and nowhere else. None can answer _does this string match my grammar?_ without loading a model, which leaves two questions open. Text produced without the grammar was never checked against it. And a grammar can compile, constrain a model perfectly, and still accept a different language than its author meant. That is what this parses GBNF for: the grammar becomes an ordinary tabnas grammar, so you can run known-good samples through it before a model ever sees it.

It is also the front end that pushed the engine furthest. GBNF describes input one character at a time while tabnas is a tokenising parser, so the compiled grammar has to switch off every default matcher and empty the ignore set. Those lexer settings are part of the accepted language rather than a tuning choice, which is why the package owns them and the shared [@tabnas/bnf](https://github.com/tabnas/bnf) compiler owns everything downstream of the IR.

## aontu

A JSON structure unifier, in TypeScript

Runtime

TypeScript · also ported to Go

Built with

[@tabnas/jsonic](https://github.com/tabnas/jsonic)[@tabnas/expr](https://github.com/tabnas/expr)[@tabnas/directive](https://github.com/tabnas/directive)[@tabnas/multisource](https://github.com/tabnas/multisource)[@tabnas/path](https://github.com/tabnas/path)[@tabnas/debug](https://github.com/tabnas/debug)

Source

[github.com/rjrodger/aontu](https://github.com/rjrodger/aontu)

Aontu unifies JSON structures: you write schemas and unification rules, and it folds many partial configurations into one consistent result. The language is a purpose-specific dialect of CUE: compact enough to write inline (\`a:1 b:$.a\`), with file evaluation and a REPL.

It is the clearest example of what the plugin model is for. Aontu defines no parser of its own. It starts from the jsonic grammar and layers on the pieces it needs: expressions for computed values, directives for \`@\`-forms, multisource so one document can pull in others, and path tracking so a reference like \`$.a\` knows where it is. The language is assembled out of grammar plugins rather than written from scratch.

## boru

A typed, word-based query language, in Go

Runtime

Go

Built with

[@tabnas/parser](https://github.com/tabnas/parser)[@tabnas/jsonic](https://github.com/tabnas/jsonic)[@tabnas/abnf](https://github.com/tabnas/abnf)[@tabnas/csv](https://github.com/tabnas/csv)[@tabnas/expr](https://github.com/tabnas/expr)

Source

[github.com/boru-lang/boru](https://github.com/boru-lang/boru)

A boru program is a sequence of words, and a word takes its arguments where you write them: \`add 1 2\` reads left to right like an ordinary call, while binary operations also read naturally infix (\`10 sub 3\`). It aims to keep the composability of concatenative, stack-based languages without making you track the stack in your head.

The language is substantial: hierarchical types with typed signatures driving dispatch, lists and maps and records, hygienic macros, concurrency: and the toolchain around it is too: parser, type checker, formatter and LSP server all ship in one CLI. That whole front end is built on the tabnas engine in Go, which makes boru the answer to whether this scales past config formats.

## The grammars themselves

The grammar packages are worth reading as examples in their own right, because most of them are short and each one extends another. The clearest chain is JSON: [json](https://github.com/tabnas/json) is a handful of rules, [jsonc](https://github.com/tabnas/jsonc) adds comments to them, and [jsonic](https://github.com/tabnas/jsonic) relaxes the quoting from there. For a grammar compiled from a specification rather than hand-written, see [proto](https://github.com/tabnas/proto), which parses the Protocol Buffers IDL from an ABNF grammar.

The full list is on the [releases](https://tabnas.dev/releases/) page, and the [docs](https://tabnas.dev/docs/) build two grammars from nothing: one standalone, one on top of an existing plugin.

## Built something?

If you've used tabnas for anything (a config dialect, a log format, a query language, an experiment that didn't work) it's worth saying so in [Discussions](https://github.com/tabnas/parser/discussions). Real usage is the most useful thing a project this early can get, and this page is short precisely because there isn't much of it yet.
