Quickstart

Install the engine, define a small grammar in ABNF, parse a string, then attach actions to compute a value. TypeScript here; the Go path mirrors it exactly.

1 · Install

npm install @tabnas/parser @tabnas/abnf

2 · Define a grammar

An addition grammar, written in ABNF — NR is the built-in number token, [ … ] is optional, and the rule refers to itself to handle a whole chain.

An addition grammar, in ABNF

import { Tabnas } from '@tabnas/parser'import { abnf } from '@tabnas/abnf'const tn = new Tabnas({ plugins: [abnf] })tn.abnf(`  val = add  add = NR [ PL add ]  PL  = "+"`)// What the ABNF compiled to: `val` and `add` are rules, `PL` became a token,// and the compiler added a `__start__` wrapper that consumes end-of-source.console.log('rules:', Object.keys(tn.rule() as object).sort().join(' '))
package mainimport (	"fmt"	"sort"	"strings"	abnf "github.com/tabnas/abnf/go"	tabnas "github.com/tabnas/parser/go")func main() {	j := tabnas.Make()	_, err := abnf.Install(j, `  val = add  add = NR [ PL add ]  PL  = "+"`, nil, nil)	if err != nil {		panic(err)	}	// What the ABNF compiled to: `val` and `add` are rules, `PL` became a token,	// and the compiler added a `__start__` wrapper that consumes end-of-source.	rules := []string{}	for name := range j.RSM() {		rules = append(rules, name)	}	sort.Strings(rules)	fmt.Println("rules:", strings.Join(rules, " "))}

Three lines of ABNF install a grammar. Listing the compiled rules shows what the compiler decided: val and add are rules, PL became a fixed token rather than a rule, and a __start__ wrapper was added to require end-of-source.

There is no code generation — the rule table is data, so you can ask a live instance what it holds.

output rules: __start__ add val

val wraps the chain — it’s the rule that will hold the running total.

3 · Parse

That grammar recognises the input and builds a tree. Every parse has the same { rule, src, kids } shape:

The tree it builds

import { Tabnas } from '@tabnas/parser'import { abnf } from '@tabnas/abnf'const tn = new Tabnas({ plugins: [abnf] })tn.abnf(`  val = add  add = NR [ PL add ]  PL  = "+"`)// Every parse has the same { rule, src, kids } shape, so one walker prints any// tree from any grammar.function show(n: any, depth = 0) {  console.log('  '.repeat(depth) + n.rule + ' ' + JSON.stringify(n.src))  for (const kid of n.kids) show(kid, depth + 1)}show(tn.parse('1+2'))
package mainimport (	"fmt"	"strings"	abnf "github.com/tabnas/abnf/go"	tabnas "github.com/tabnas/parser/go")// Every parse has the same { rule, src, kids } shape, so one walker prints any// tree from any grammar.func show(node any, depth int) {	n := node.(map[string]any)	fmt.Printf("%s%s %q\n", strings.Repeat("  ", depth), n["rule"], n["src"])	for _, kid := range n["kids"].([]any) {		show(kid, depth+1)	}}func main() {	j := tabnas.Make()	_, err := abnf.Install(j, `  val = add  add = NR [ PL add ]  PL  = "+"`, nil, nil)	if err != nil {		panic(err)	}	out, err := j.Parse("1+2")	if err != nil {		panic(err)	}	show(out, 0)}

The grammar recognises 1+2 and builds a tree. Both add nodes are siblings, not nested: the compiler turns the tail self-reference [ PL add ] into a same-depth repeat. PL compiled to a token, so the + never appears as a kid.

The walker is four lines because the node shape never varies — the same function prints a tree from any grammar in either runtime.

output val "1+2" · add "1" · add "2"

Each repetition of add is a sibling — the compiler turns the tail self-reference [ PL add ] into a same-depth repeat, not a nested push. (PL compiles to a token, so it never appears in kids.)

4 · Add actions

Recognising isn’t computing. To get a total, attach actions by reference — the grammar text stays untouched.

Actions attach by alternate mark — a rule’s alternate, named by its leading discriminator. '@val:o:add' is the val rule’s alternate that pushes add; '@add:o:NR' is the add rule’s alternate on an NR token. r.o holds the tokens that alternate matched, so r.o[0].val is the number just read — already a number, courtesy of the lexer.

Two actions make it add

import { Tabnas } from '@tabnas/parser'import { abnf } from '@tabnas/abnf'const tn = new Tabnas({ plugins: [abnf] })tn.abnf(`  val = add  add = NR [ PL add ]  PL  = "+"`, {  actions: {    // `val` holds the running total.    '@val:o:add': (r: any) => { r.node.value = 0 },    // Each number adds to it.    '@add:o:NR': (r: any) => { r.parent.node.value += r.o[0].val },  },})// The total rides on the parse, not on an outer variable — so re-parsing the// same input gives the same answer, and the instance carries no state.for (const src of ['1+2+3', '12+3+45', '1+2+3']) {  console.log(src.padEnd(8), '=>', (tn.parse(src) as any).value)}
package mainimport (	"fmt"	abnf "github.com/tabnas/abnf/go"	tabnas "github.com/tabnas/parser/go")func main() {	j := tabnas.Make()	_, err := abnf.Install(j, `  val = add  add = NR [ PL add ]  PL  = "+"`, nil, abnf.ActionsMap{		// `val` holds the running total.		"@val:o:add": {func(r *tabnas.Rule, ctx *tabnas.Context) {			r.Node.(map[string]any)["value"] = float64(0)		}},		// Each number adds to it.		"@add:o:NR": {func(r *tabnas.Rule, ctx *tabnas.Context) {			node := r.Parent.Node.(map[string]any)			node["value"] = node["value"].(float64) + r.O[0].Val.(float64)		}},	})	if err != nil {		panic(err)	}	// The total rides on the parse, not on an outer variable — so re-parsing the	// same input gives the same answer, and the instance carries no state.	for _, src := range []string{"1+2+3", "12+3+45", "1+2+3"} {		out, err := j.Parse(src)		if err != nil {			panic(err)		}		fmt.Printf("%-8s => %v\n", src, int(out.(map[string]any)["value"].(float64)))	}}

Recognising is not computing. Two actions, bound by alternate mark, turn the same grammar into an adding machine: @val:o:add seeds the total, @add:o:NR adds each number to r.parent.node.

Because add repeats at the same depth, r.parent is the one val node for every number — and since the total lives on the parse rather than in an outer variable, parsing the same input twice gives the same answer.

output 1+2+3 => 6 · 12+3+45 => 60 · 1+2+3 => 6

r.parent is val for every repetition — that’s the same-depth repeat again — so the total accumulates in one place, on val’s node, where parse returns it. The instance carries no state between calls.

These are the same two actions a hand-written rule table uses for this grammar (see the home page, steps 3 and 4): ABNF and the rule table aren’t just equivalent notations, they compile to the same machine.

Mark names come from each alternate’s leading discriminator, so ask the compiler rather than guessing:

tabnas-abnf --marks -f grammar.abnf
val  o:add  p:add
val  c:_  (empty)
add  o:NR  s:#NR
add  c:PL  s:#PL
add  c:_  (empty)

add c:PL is the repeat itself — a close-phase alternate you can attach an action to with '@add:c:PL'.

Next steps

  • Open the playground and edit this grammar live.
  • Read about ABNF grammars — repetition, groups, left recursion, and @ref actions in full.
  • The home page shows this grammar four ways, including how to carry the total on the parse itself instead of in an outer variable.
  • Building in Go? The same grammar and tree are available via github.com/tabnas/parser/go.

Describes @tabnas/parser 0.8.10 · all pinned versions