How it works

tabnas is built on a few deliberate choices. Understanding them explains why the API is small and why the same grammar behaves identically across languages.

The grammar is the parser

Most parser tools generate code from a grammar, then ask you to keep the generated code in sync. tabnas doesn’t generate anything: a grammar is a data structure the engine executes directly. That means:

  • No build step and nothing to fall out of sync.
  • You can build grammars at runtime (for example, compiled from ABNF).
  • You can inspect a live grammar — describe it, render it back to ABNF, or draw it as a railroad diagram — because it’s just data.

A uniform tree

Parsing always produces the same node shape:

The node shape

// The shape every tree-building grammar produces.type TreeNode = { rule: string; src: string; kids: TreeNode[] }const node: TreeNode = {  rule: 'val',  src: '1+2',  kids: [    { rule: 'add', src: '1', kids: [] },    { rule: 'add', src: '2', kids: [] },  ],}console.log(node.rule, node.kids.length)
package mainimport "fmt"// The shape every tree-building grammar produces.type Node struct {	Rule string `json:"rule"`	Src  string `json:"src"`	Kids []Node `json:"kids"`}func main() {	node := Node{		Rule: "val",		Src:  "1+2",		Kids: []Node{			{Rule: "add", Src: "1", Kids: []Node{}},			{Rule: "add", Src: "2", Kids: []Node{}},		},	}	fmt.Println(node.Rule, len(node.Kids))}

Every tree-building grammar produces the same node shape: the rule that matched, the source text it covered, and its children. Nesting is just kids holding more of the same.

TypeScript expresses it as a structural type; Go as a struct with JSON tags, so the same tree serialises identically from either runtime.

output val 2

rule is the grammar rule that matched, src is the source it covered, and kids are the child nodes. Because the shape is invariant, tooling written for one grammar works for all of them.

One grammar, two runtimes

The engine has two implementations: TypeScript (canonical) and Go (tracking). They compile the same grammar fixtures and must produce the same trees — this is enforced in CI. Practically, you can define a language once and run it in a Node service and a Go binary with confidence they agree.

Some differences are intentional where a runtime’s API can’t mirror the other; those are documented per package.

Scope

tabnas is the shortest path from “I have a grammar” to “I have a parser I can trust, everywhere I run.” It is not a full compiler toolchain, and it isn’t the fastest parser for every workload — it optimises for correctness, portability, and a grammar you can read.

Describes @tabnas/parser 0.8.10 · all pinned versions