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.12.9 · all pinned versions