Introduction

tabnas is a parsing engine that can handle any language. You describe a language as a grammar and get a working parser that builds a small, uniform syntax tree.

There is no code-generation step. The grammar is the parser: a table of rules and token lookups the engine walks at runtime. That has two consequences, and they are the reason the project exists.

Extension is cheap. A new language is usually an existing one plus some rules — JSONC is JSON with comments, jsonic is JSONC with relaxed quoting, and so on. You add rules and token alternates to a grammar that already parses, rather than forking a parser and owning the fork.

The engine is a good compile target. ABNF compiles into it, so a human can write a grammar in the notation the RFCs already use. And because the grammar is flat declarative data with no control flow, a language model can emit one directly — and you can inspect it, print it back as ABNF, or draw it as a railroad diagram before you run it.

See why tabnas for where this came from, and agents for the second point in full.

The syntax tree

Every parse yields the same 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

Because the shape never changes, a walker, action, or tool you write once applies to every grammar you define.

Runtimes

The engine is implemented twice, in TypeScript and in Go, so it runs wherever you do. TypeScript is the reference implementation; the Go port tracks it and is verified against the same fixtures. A grammar parses the same way in both.

Where to go next

Describes @tabnas/parser 0.8.10 · all pinned versions