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: