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.
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.
outputrules: __start__ add val
val wraps the chain — it’s the rule that will hold the running total.
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.
outputval "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.)
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.
output1+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: