The rule instance

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.

A grammar to look at

name = value, and nothing else:

A grammar to look at

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.

output key=port value=8080

Two rules, four fields of r between them. Now the rest.

Where you are

FieldWhat it is
r.namethe rule’s name — 'pair'
r.state'o' in the open phase, 'c' in the close phase
r.dstack depth; 0 for the start rule
r.ia 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.

What you matched

The open phase collects tokens into r.o, the close phase into r.c:

FieldWhat it is
r.otokens matched in the open phase
r.o0 r.o1shorthand for r.o[0] and r.o[1]
r.oshow many open tokens matched
r.ctokens matched in the close phase
r.c0 r.c1shorthand for r.c[0] and r.c[1]
r.cshow 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.

output open tokens: 2 · first: "port" · second: "=" · all: port,=

r.o0 is the one you will reach for most: it is the token that decided which alternate you are in, which makes it the right thing to point an error at.

Inside a token

Inside a token

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 val a without; #NR has src 42 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

FieldWhat it is
srcthe source text, exactly as written
valthe resolved value — a real number for #NR, the unquoted string for #ST
name tinthe token’s name and its numeric id
rI cIrow 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 rules around you

FieldWhat it is
r.parentthe enclosing rule instance
r.childthe rule instance that just closed beneath this one
r.prevthe 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.

The value: r.node

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.

Scratch data: u and k

Two bags for your own use. They differ in exactly one way, and it is the important one:

FieldScope
r.uthis rule instance only
r.kthis 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.

output item 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.

Counters: r.n

n is a third bag, for numbers, and it propagates like k. It has comparison helpers, and a trap:

HelperTrue when
r.eq('k', n)the counter equals n
r.lt r.ltebelow / at most n
r.gt r.gteabove / 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.

Reading it live

You do not have to guess at any of this:

Every activation, live

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.

output __start__~o@0 val~o@1 add~o@2 add~c@2 add~o@2 add~c@2 val~c@1 __start__~c@0

Two add instances at the same depth is a repeat; increasing depth is a push. See debugging a grammar for the full trace.

Next

Describes @tabnas/parser 0.8.10 · all pinned versions