Debugging with ObservableRDParser
ObservableRDParser adds opt-in parse tracing via a ParseObserver interface.
Attach one to receive a stream of enter/exit/error events for every production rule.
For generated parsers, pass the observer as the second argument to the static parse method.
For hand-written parsers that extend ObservableRDParser directly, call withObserver(obs)
on the instance before calling parse().
Production parsers extend the lean RDParser and carry zero observer overhead — only
parsers that explicitly extend ObservableRDParser incur any tracing cost.
Every production rule entry and exit fires a pair of events on the observer. The sequence below shows what fires when parsing the input "42" against the arithmetic grammar:
DebugObserver formats these as an indented call tree printed to the console. TraceObserver stores them as a ParseEvent[] array for programmatic inspection.
Generating an observable parser
Pass --observable to rdp-gen:
rdp-gen grammar.ebnf --parser-name MyParser --observable --output MyParser.tsThis changes the generated class to extend ObservableRDParser instead of ScannerlessRDParser, and
adds an optional observer?: ParseObserver second argument to the static parse method.
DebugObserver — indented call tree
DebugObserver formats parse events as an indented call tree and writes each line to a sink
(defaults to console.error):
import { MyParser } from './MyParser.js'
import { DebugObserver } from '@configuredthings/rdp.js/observable'
MyParser.parse('3 + 4', new DebugObserver())Example output for the arithmetic grammar parsing 3 + 4:
→ expr pos:0
→ term pos:0
→ factor pos:0
→ number pos:0
← number matched pos:1
← factor matched pos:1
← term matched pos:1
→ term pos:4
→ factor pos:4
→ number pos:4
← number matched pos:5
← factor matched pos:5
← term matched pos:5
← expr matched pos:5
Redirect output to an array for assertions in tests:
const lines: string[] = []
MyParser.parse('3 + 4', new DebugObserver(line => lines.push(line)))TraceObserver — structured events
TraceObserver accumulates a full parse trace as an array of ParseEvent objects — useful
for programmatic inspection and testing:
import { TraceObserver } from '@configuredthings/rdp.js/observable'
const obs = new TraceObserver()
MyParser.parse('42', obs)
console.log(obs.events)
// [
// { kind: 'enter', production: 'expr', position: 0 },
// { kind: 'enter', production: 'term', position: 0 },
// { kind: 'enter', production: 'number', position: 0 },
// { kind: 'exit', production: 'number', position: 2, matched: true },
// { kind: 'exit', production: 'term', position: 2, matched: true },
// { kind: 'exit', production: 'expr', position: 2, matched: true },
// ]Each event is one of:
type ParseEvent =
| { kind: 'enter'; production: string; position: number }
| { kind: 'exit'; production: string; position: number; matched: boolean }
| { kind: 'error'; message: string; position: number }Custom observer
Implement ParseObserver directly for bespoke tracing:
import type { ParseObserver } from '@configuredthings/rdp.js/observable'
class CountingObserver implements ParseObserver {
readonly calls = new Map<string, number>()
onEnterProduction(production: string): void {
this.calls.set(production, (this.calls.get(production) ?? 0) + 1)
}
onExitProduction(): void {}
onError(): void {}
}
const obs = new CountingObserver()
MyParser.parse('3 + 4', obs)
console.log(obs.calls)
// Map { 'expr' => 1, 'term' => 2, 'factor' => 2, 'number' => 2, 'wsp' => 5 }Adding observer support to a TokenRDParser subclass
ObservableRDParser is a concrete subclass of ScannerlessRDParser — it can't be used
directly as a base for token-stream parsers. Use the withObservable() mixin instead,
which applies observer support to any RDParser subclass:
import { TokenRDParser } from '@configuredthings/rdp.js'
import { withObservable } from '@configuredthings/rdp.js/observable'
import type { ParseObserver } from '@configuredthings/rdp.js/observable'
class MyTokenParser extends withObservable(TokenRDParser) {
static parse(input: string, observer?: ParseObserver) {
const stream = classify(input, spanTokenize(input))
const parser = new MyTokenParser(stream)
if (observer) parser.withObserver(observer)
return parser.parse()
}
// ... #parseRule() stubs
}The mixin adds withObserver(obs), notifyEnter(), and notifyExit() — call
notifyEnter/notifyExit in each parse method to emit events, exactly as
--observable does in generated scannerless parsers.
Using GrammarInterpreter with an observer
GrammarInterpreter extends ObservableRDParser, so you can attach any observer to it
for step-by-step grammar debugging without a code-generation step:
import { GrammarInterpreter } from '@configuredthings/rdp.js/interpreter'
import { EBNFParser } from '@configuredthings/rdp.js/generator'
import { TraceObserver } from '@configuredthings/rdp.js/observable'
const ast = EBNFParser.parse(`
Number = Digit, {Digit};
Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
`)
const obs = new TraceObserver()
const bytes = new TextEncoder().encode('42')
new GrammarInterpreter(ast, new DataView(bytes.buffer))
.withObserver(obs)
.parse()
console.log(obs.events)