Every action, condition and error function is handed the same thing: r, the
rule instance. It is the rule as it is running — one per activation, not one
per rule — and it is where all the state of a parse lives.
This walks through it with a two-rule grammar you can run. Ten minutes.
import { Tabnas } from '@tabnas/parser'const tn = new Tabnas()tn.options({ fixed: { token: { '#EQ': '=' } }, rule: { start: 'pair' },})// `r.o0` is the token that opened the rule; `r.child` is the rule that just// closed beneath it — which is why the assembly happens in the close phase.tn.rule('pair', (rs: any) => rs .open([{ s: ['#TX', '#EQ'], p: 'val' }]) .close([{ a: (r: any) => { r.node = { key: r.o0.src, value: r.child.node } } }]))tn.rule('val', (rs: any) => rs.open([ { s: '#NR', a: (r: any) => { r.node = r.o0.val } },]))const out: any = tn.parse('port = 8080')console.log(`key=${out.key} value=${out.value}`)
package mainimport ( "fmt" tabnas "github.com/tabnas/parser/go")func main() { j := tabnas.Make() j.SetOptions(tabnas.MapToOptions(map[string]any{ "fixed": map[string]any{"token": map[string]any{"#EQ": "="}}, "rule": map[string]any{"start": "pair"}, })) // Go names tokens by their numeric id, so look the three up once. TX, EQ, NR := j.Token("#TX"), j.Token("#EQ"), j.Token("#NR") // `r.O0` is the token that opened the rule; `r.Child` is the rule that just // closed beneath it — which is why the assembly happens in the close phase. j.Rule("pair", func(rs *tabnas.RuleSpec, p *tabnas.Parser) { rs.AddOpen(&tabnas.AltSpec{S: [][]tabnas.Tin{{TX}, {EQ}}, P: "val"}) rs.AddClose(&tabnas.AltSpec{A: func(r *tabnas.Rule, ctx *tabnas.Context) { r.Node = map[string]any{"key": r.O0.Src, "value": r.Child.Node} }}) }) j.Rule("val", func(rs *tabnas.RuleSpec, p *tabnas.Parser) { rs.AddOpen(&tabnas.AltSpec{S: [][]tabnas.Tin{{NR}}, A: func(r *tabnas.Rule, ctx *tabnas.Context) { r.Node = r.O0.Val }}) }) out, err := j.Parse("port = 8080") if err != nil { panic(err) } n := out.(map[string]any) fmt.Printf("key=%v value=%v\n", n["key"], n["value"])}
Two rules, and four fields of the rule instance between them: r.o0 is the token that opened the rule, r.child the instance that just closed beneath it, and r.node the value each rule carries — the start rule's is what parse returns.
r.child is only meaningful in the close phase, so pair assembles its result there rather than on the way down.
TypeScript names tokens by string in the alternate; Go looks their numeric ids up once with j.Token and matches on those.
outputkey=port value=8080
Two rules, four fields of r between them. Now the rest.
a serial number, unique per rule activation in this parse
r.state matters more than it looks. The same action can be attached to both
phases, and “am I going down or coming back up” is usually the question it
needs answered.
The open phase collects tokens into r.o, the close phase into r.c:
Field
What it is
r.o
tokens matched in the open phase
r.o0r.o1
shorthand for r.o[0] and r.o[1]
r.os
how many open tokens matched
r.c
tokens matched in the close phase
r.c0r.c1
shorthand for r.c[0] and r.c[1]
r.cs
how many close tokens matched
For the alternate { s: ['#TX', '#EQ'] } on input port = 8080:
What the open phase matched
import { Tabnas } from '@tabnas/parser'const tn = new Tabnas()tn.options({ fixed: { token: { '#EQ': '=' } }, rule: { start: 'pair' },})// The open phase collects its matched tokens into `r.o`; `r.o0` and `r.o1` are// shorthand for the first two, and `r.os` is how many matched.tn.rule('pair', (rs: any) => rs .open([{ s: ['#TX', '#EQ'], p: 'val' }]) .close([{ a: (r: any) => { console.log('open tokens:', r.os) console.log('first: ', JSON.stringify(r.o0.src)) console.log('second:', JSON.stringify(r.o1.src)) console.log('all: ', r.o.map((t: any) => t.src).join(',')) } }]))tn.rule('val', (rs: any) => rs.open([{ s: '#NR' }]))tn.parse('port = 8080')
package mainimport ( "fmt" "strings" tabnas "github.com/tabnas/parser/go")func main() { j := tabnas.Make() j.SetOptions(tabnas.MapToOptions(map[string]any{ "fixed": map[string]any{"token": map[string]any{"#EQ": "="}}, "rule": map[string]any{"start": "pair"}, })) TX, EQ, NR := j.Token("#TX"), j.Token("#EQ"), j.Token("#NR") // The open phase collects its matched tokens into `r.O`; `r.O0` and `r.O1` // are shorthand for the first two, and `r.OS` is how many matched. j.Rule("pair", func(rs *tabnas.RuleSpec, p *tabnas.Parser) { rs.AddOpen(&tabnas.AltSpec{S: [][]tabnas.Tin{{TX}, {EQ}}, P: "val"}) rs.AddClose(&tabnas.AltSpec{A: func(r *tabnas.Rule, ctx *tabnas.Context) { srcs := []string{} for _, t := range r.O { srcs = append(srcs, t.Src) } fmt.Println("open tokens:", r.OS) fmt.Println("first: ", fmt.Sprintf("%q", r.O0.Src)) fmt.Println("second:", fmt.Sprintf("%q", r.O1.Src)) fmt.Println("all: ", strings.Join(srcs, ",")) }}) }) j.Rule("val", func(rs *tabnas.RuleSpec, p *tabnas.Parser) { rs.AddOpen(&tabnas.AltSpec{S: [][]tabnas.Tin{{NR}}}) }) if _, err := j.Parse("port = 8080"); err != nil { panic(err) }}
An alternate with a token sequence collects every token it matched. r.o is the list, r.o0 and r.o1 are shorthand for the first two, and r.os is the count. The close phase fills r.c the same way.
r.o0 is the one you reach for most: it is the token that decided which alternate you are in, and so the right thing to point a parse error at.
import { Tabnas } from '@tabnas/parser'import { json } from '@tabnas/json'const tn = new Tabnas({ plugins: [json] })// A token carries its source text, its resolved value, and where it was found.// `src` and `val` are not the same thing: for #NR, `src` is the string '42'// and `val` is the number 42.tn.sub({ lex: (t: any) => { if ('#ST' !== t.name && '#NR' !== t.name) return console.log(t.name, String(t.src).padEnd(4), String(t.val).padEnd(3), t.rI, t.cI) },})tn.parse('{"a": 42}')
package mainimport ( "fmt" tabnasjson "github.com/tabnas/json/go" tabnas "github.com/tabnas/parser/go")func main() { j := tabnas.Make() if err := j.Use(tabnasjson.Json); err != nil { panic(err) } // A token carries its source text, its resolved value, and where it was // found. `Src` and `Val` are not the same thing: for #NR, `Src` is the // string "42" and `Val` is the number 42. j.Sub(func(t *tabnas.Token, r *tabnas.Rule, ctx *tabnas.Context) { if "#ST" != t.Name && "#NR" != t.Name { return } fmt.Printf("%s %-4s %-3s %d %d\n", t.Name, t.Src, fmt.Sprintf("%v", t.Val), t.RI, t.CI) }, nil) if _, err := j.Parse(`{"a": 42}`); err != nil { panic(err) }}
Subscribing to the lexer prints each token's name, source text, resolved value and 1-based row and column. Note the pairs: #ST has src"a" with the quotes and vala without; #NR has src42 the string and val 42 the number.
Picking the wrong one of src and val is a common bug, and the fastest way to settle it is to look.
output#ST "a" a 1 2 · #NR 42 42 1 7
Field
What it is
src
the source text, exactly as written
val
the resolved value — a real number for #NR, the unquoted string for #ST
nametin
the token’s name and its numeric id
rIcI
row and column, 1-based
src and val are not the same thing, and picking the wrong one is a
common bug. r.o0.src for a number is the string '42'; r.o0.val is the
number 42.
the rule instance that just closed beneath this one
r.prev
the previous instance when a rule repeats
r.child is only meaningful in the close phase — going down, nothing has
closed yet. That is why the pair grammar above reads r.child.node in
close and not in open.
Both are always something: when there is no parent or child, they are a
sentinel rule whose name is empty, not undefined. So r.child.name is safe
to read; r.child.name === 'val' is the check to write, rather than a null
test.
r.node is what the rule carries, and the start rule’s r.node is what
parse() returns.
A pushed rule’s node is seeded from its parent. That is why
@setval$ can write into r.node from inside pair and have the result land
in the enclosing map — they are the same object. It is also why
@reset$ exists: a rule that needs its own scalar
value has to clear the inherited one first.
Two bags for your own use. They differ in exactly one way, and it is the
important one:
Field
Scope
r.u
this rule instance only
r.k
this rule and every rule pushed or repeated below it
`u` stays put, `k` travels
import { Tabnas } from '@tabnas/parser'const tn = new Tabnas()tn.options({ rule: { start: 'top' } })// `u` is scoped to this rule instance; `k` propagates to every rule pushed or// repeated below it.tn.rule('top', (rs: any) => rs .open([{ p: 'item', u: { onlyHere: 1 }, k: { everywhere: 2 } }]))tn.rule('item', (rs: any) => rs.open([{ s: '#NR', a: (r: any) => { console.log('item sees u.onlyHere: ', 'onlyHere' in r.u) console.log('item sees k.everywhere:', r.k.everywhere)} }]))tn.parse('42')
package mainimport ( "fmt" tabnas "github.com/tabnas/parser/go")func main() { j := tabnas.Make() j.SetOptions(tabnas.MapToOptions(map[string]any{ "rule": map[string]any{"start": "top"}, })) NR := j.Token("#NR") // `U` is scoped to this rule instance; `K` propagates to every rule pushed // or repeated below it. j.Rule("top", func(rs *tabnas.RuleSpec, p *tabnas.Parser) { rs.AddOpen(&tabnas.AltSpec{ P: "item", U: map[string]any{"onlyHere": 1}, K: map[string]any{"everywhere": 2}, }) }) j.Rule("item", func(rs *tabnas.RuleSpec, p *tabnas.Parser) { rs.AddOpen(&tabnas.AltSpec{S: [][]tabnas.Tin{{NR}}, A: func(r *tabnas.Rule, ctx *tabnas.Context) { _, seen := r.U["onlyHere"] fmt.Println("item sees u.onlyHere: ", seen) fmt.Println("item sees k.everywhere:", r.K["everywhere"]) }}) }) if _, err := j.Parse("42"); err != nil { panic(err) }}
Two scratch bags, differing in exactly one way. u belongs to the one rule instance, so item never sees what top put there. k propagates to every rule pushed or repeated below, so item does see that.
Use u for something a rule needs between its own open and close phases, and k for configuration a whole subtree should read.
outputitem sees u.onlyHere: false · item sees k.everywhere: 2
Use u for something the rule needs between its own open and close phases —
the captured key in a key = value pair. Use k for configuration that a
whole subtree should see.
n is a third bag, for numbers, and it propagates like k. It has comparison
helpers, and a trap:
Helper
True when
r.eq('k', n)
the counter equals n
r.ltr.lte
below / at most n
r.gtr.gte
above / at least n
An unset counter reads as 0 — it has counted nothing. So r.lt('depth', 3)
is true before anything is counted, r.gt('depth', 99) is false, and exactly
one of <, =, > holds. r.exist('k') asks whether the counter was set at
all, which the comparisons cannot: one set to 0 and one never set compare
identically. Conditions and counters covers it properly.
Before 0.6 an unset counter compared as true against every one of them,
so lt and gt were both true and a guard could fire on the first token.
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 = "+"`)// You do not have to guess at any of this: subscribe and watch.// name~state@depth — two `add` instances at the same depth is a repeat,// increasing depth is a push.const trace: string[] = []tn.sub({ rule: (r: any) => trace.push(`${r.name}~${r.state}@${r.d}`) })tn.parse('1+2')console.log(trace.join(' '))
package mainimport ( "fmt" "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) } // You do not have to guess at any of this: subscribe and watch. // name~state@depth — two `add` instances at the same depth is a repeat, // increasing depth is a push. trace := []string{} j.Sub(nil, func(r *tabnas.Rule, ctx *tabnas.Context) { trace = append(trace, fmt.Sprintf("%s~%s@%d", r.Name, r.State, r.D)) }) if _, err := j.Parse("1+2"); err != nil { panic(err) } fmt.Println(strings.Join(trace, " "))}
Every rule activation is visible: name, phase (o open, c close) and stack depth. Two add instances at depth 2 is the tail self-reference compiling to a same-depth repeat; the step from val to add is a push.
__start__ is the wrapper the ABNF compiler adds to require end-of-source — a hand-written rule table has no such check unless you write one.