CLI Reference — rdp-gen

rdp-gen is the code generator bundled with @configuredthings/rdp.js. The same functionality is available programmatically via the generateParser function in the @configuredthings/rdp.js/generator module. It reads a grammar file (EBNF or ABNF) and emits a strictly-typed TypeScript parser class that extends RDParser or ObservableRDParser, together with exported discriminated-union types for every node in the parse tree. The generated output compiles cleanly under strict: true and noUncheckedIndexedAccess: true with no type errors.

Installation

Install the package globally to get rdp-gen on your PATH:

npm install -g @configuredthings/rdp.js

Or use it locally via npx:

npx rdp-gen grammar.ebnf --parser-name MyParser

rdp-gen <grammar> — generate a parser

rdp-gen <grammar> [options]
OptionDefaultDescription
<grammar>(required)Path to a .ebnf or .abnf grammar file
-o, --outdir <dir>stdoutWrite output files to a directory; each artifact gets a derived filename
--format <fmt>inferred from extensionForce ebnf or abnf
--parser-name <name>GeneratedParserClass name for the generated parser
--tree-name <name>ParseTreeType name for the generated parse tree
--observableoffExtend ObservableRDParser; adds notifyEnter/notifyExit calls
--lexer <strategy>scannerlessLexer strategy: scannerless (default) or span (emits a span-tokeniser scaffold)
--traversal <strategy>—Emit a traversal scaffold: interpreter or tree-walker (see below)
--transformer [json]—Emit a transformer scaffold; pass json for the two-way JSON variant
--facadeoffWrap the scaffold in a module-as-facade (requires --traversal)
--pipelineoffAdd parse / validate / transform stages (requires --traversal tree-walker)
--ast-onlyoffEmit the grammar AST as JSON (used by the playground)
--abnf-case-sensitive-stringsoffMatch ABNF quoted string literals case-sensitively

Examples

Generate a parser to a directory (writes src/JsonParser.ts):

rdp-gen grammar.ebnf --parser-name JsonParser --outdir src/

Generate with tracing support:

rdp-gen grammar.ebnf --parser-name MyParser --observable --outdir src/

Parse an ABNF grammar and write to stdout:

rdp-gen protocol.abnf --format abnf --parser-name FrameParser

Traversal stubs (--traversal)

When used alone (without --facade, --pipeline, or --transformer), --traversal adds evaluation scaffolding directly to the generated parser file — the output is still a parser, not a separate scaffold. Because the stubs follow the grammar mechanically, the file can be regenerated as the grammar evolves.

FlagsWhat it adds to the parser
--traversal interpreterParser class implements InterpreterMixin<ParseTree, unknown> — one eval{Rule}(node): unknown stub per rule and a static evaluate() entry point
--traversal tree-walkerExports a walk(root, fn) function alongside the existing childNodes() helper; includes a commented-out Visitor template covering every rule
# Generate (or regenerate) a parser with interpreter stubs baked in (writes src/DateParser.ts)
rdp-gen date.ebnf --parser-name DateParser --traversal interpreter --outdir src/

When --traversal is combined with --facade, --pipeline, or --transformer, the traversal strategy moves inside the scaffold — see Scaffolding below.


Scaffolding

When any of --transformer, --facade, --pipeline, or --lexer span is present, rdp-gen emits a scaffold rather than a parser. A scaffold is a one-time starter file — unlike the generated parser it is not designed to be regenerated. It imports the parser by a relative path, wires up the chosen pattern, and leaves stubs for you to fill in. See also the generateScaffold programmatic API and the arithmetic worked example for patterns applied to a real grammar.

FlagsWhat it emits
--traversal interpreter --facadeModule-as-facade: parse{Base}(), {Base}Result class with from(), private eval functions
--traversal tree-walker --facadeModule-as-facade: parse{Base}(), {Base}Result class, private walk utility and visitor stubs
--traversal tree-walker --pipelineExported parse/validate/transform + load{Base}() combinator; tree-walker inside transform
--traversal tree-walker --pipeline --facadeModule-as-facade wrapping a pipeline with a tree walker inside #transform
--transformerExported Transformer<ParseTree, unknown> object with a stub per rule + entry function
--transformer jsonTwo-way stubs: {Base}ToJSON: Transformer<ParseTree, JSONAST> and jsonTo{Base}: Transformer<JSONAST, string> plus round-trip helpers
--lexer spanSpan tokeniser + classifier + {Base}TokenParser stubs (see Tokenising for performance)

Flag compatibility

The scaffold flags are designed to compose. The table below shows which combinations are valid (✓), invalid (✗), or not applicable (—). The one constraint is that --traversal interpreter cannot be combined with --pipeline — the interpreter evaluates directly during traversal, leaving no intermediate tree for the validate stage.

--facade--pipeline--transformer [json]
--traversal interpreter✓✗—
--traversal tree-walker✓✓—
--pipeline✓——
--facade—✓✓
--transformer [json]✓——

--lexer span is compatible with any of the above combinations.

When --transformer json is combined with --facade (with or without --pipeline), --outdir produces separate files for each concern so the facade is a genuine module boundary:

# Writes: src/DateParser.ts, src/date-transformer.ts, src/date-pipeline.ts, src/date-facade.ts
rdp-gen date.abnf --parser-name DateParser --transformer json --facade --pipeline --lexer span --outdir src/

The typical workflow for a new grammar:

# 1. Generate the parser (regenerate freely as the grammar evolves) → src/DateParser.ts
rdp-gen date.ebnf --parser-name DateParser --outdir src/

# 2. Generate a scaffold once (edit; do not regenerate) → src/date-facade.ts
rdp-gen date.ebnf --parser-name DateParser --traversal interpreter --facade --outdir src/

Both files land in the same directory, so the scaffold's import of './DateParser.js' resolves correctly.

Interpreter traversal stubs

Adds InterpreterMixin<ParseTree, unknown> to the parser class, emitting one eval{Rule}(node: {Rule}Node): unknown stub per rule and a static evaluate() entry point. The file header notes that stubs are present and safe to edit; the parser structure itself can still be regenerated:

rdp-gen date.ebnf --parser-name DateParser --traversal interpreter --outdir src/

Tree-walker stubs

Exports a walk(root, fn) function alongside the existing childNodes() helper, plus a commented-out Visitor template covering every rule in the grammar:

rdp-gen date.ebnf --parser-name DateParser --traversal tree-walker --outdir src/

Facade scaffold

Emits a module-as-facade: a public parse{Base}() entry point, a {Base}Result domain class with a static from(tree) factory, and a {Base}Error class. Combine with --traversal to specify the strategy used inside the facade:

# Facade wrapping recursive interpreter functions → src/date-facade.ts
rdp-gen date.ebnf --parser-name DateParser --traversal interpreter --facade --outdir src/

# Facade wrapping a tree walker → src/date-facade.ts
rdp-gen date.ebnf --parser-name DateParser --traversal tree-walker --facade --outdir src/

# Facade wrapping a full pipeline (tree walker inside #transform) → src/date-facade.ts
rdp-gen date.ebnf --parser-name DateParser --traversal tree-walker --pipeline --facade --outdir src/

Pipeline scaffold

Emits three exported stage functions — parse() (throws SyntaxError), validate() (returns { ok: true; tree } | { ok: false; errors }), and transform() (stub) — plus a load{Base}() combinator that chains them. Use --traversal to specify the strategy inside transform. Note that --traversal interpreter is not compatible with --pipeline (see flag compatibility above):

# Pipeline with tree walker inside transform → src/date-pipeline.ts
rdp-gen date.ebnf --parser-name DateParser --traversal tree-walker --pipeline --outdir src/

Transformer scaffold

Emits an exported Transformer<ParseTree, unknown> object with one handler stub per grammar rule, plus a transform{Base}() entry function. Replace unknown with your concrete return type, then fill in the handlers. Because Transformer requires all keys, adding a new rule to the grammar will immediately cause a compile error in the scaffold:

rdp-gen date.ebnf --parser-name DateParser --transformer --outdir src/

See Transformer<ParseTree, T> and transform() for the runtime API.

JSON transformer scaffold

Emits two Transformer objects — one for each direction — plus convenience round-trip functions. This is the recommended starting point when JSON is one endpoint of your translation:

rdp-gen date.ebnf --parser-name DateParser --transformer json --outdir src/

Writes src/date-transformer.ts containing:

  • dateToJSON: Transformer<ParseTree, JSONAST> — one stub per grammar rule
  • jsonToDate: Transformer<JSONAST, string> — one stub per JSON kind (string, number, boolean, null, array, object)
  • dateToJSONString(input: string): string — parses the input and calls fromJSONAST
  • jsonStringToDate(input: string): string — calls toJSONAST and emits your format

See Translating to and from JSON for the runtime API and a worked example.

See Using the Parse Tree for worked examples of each pattern.


rdp-gen init — scaffold a new project

rdp-gen init [options]

Creates a package.json, tsconfig.json, and a starter src/<ClassName>.ts in the current directory — everything needed to start writing a hand-crafted parser that compiles immediately.

OptionDefaultDescription
--name <name>my-parsernpm package name; also determines the class name (my-parser → MyParser)
--observableoffExtend ObservableRDParser instead of RDParser

Example

mkdir my-parser && cd my-parser
rdp-gen init --name my-parser
npm install
npm run build   # compiles src/MyParser.ts → dist/

The starter src/MyParser.ts includes the private-constructor / static-parse boilerplate and a #parseRoot() stub with inline guidance on which methods to use. The template is produced by generateInitScaffold.

To scaffold an observable parser with built-in tracing support:

rdp-gen init --name my-parser --observable

The observable variant extends ObservableRDParser, adds notifyEnter/notifyExit call sites in the stub, and exposes an optional ParseObserver parameter in static parse() so a TraceObserver or DebugObserver can be attached at call time without modifying the parser itself.

The scaffolded tsconfig.json includes:

{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "moduleResolution": "node16"
  }
}

These options are required — the parser uses native # private fields (target: ES2022), is written to strict null-check standards (strict: true, noUncheckedIndexedAccess: true), and needs the package exports map (moduleResolution: node16 or bundler).


Generated class shape

Every parser emitted by rdp-gen follows the same structure:

export class MyParser extends ScannerlessRDParser {
  private constructor(source: DataView) { super(source) }

  /** Encode `input`, parse it, and return the typed parse tree — or throw on failure. */
  static parse(input: string): RootNode { ... }

  parse(): RootNode { ... }          // called internally by static parse
  #parse_rule(): RuleNode | null { ... }  // one private method per production rule
}

The constructor is private, so callers always go through static parse:

const tree = MyParser.parse('some input')

Generated parse-tree types

Alongside the parser class, rdp-gen emits a discriminated-union type for every production rule and a root ParseTree union that covers all of them. Given a grammar such as:

Expr   = Term, {('+' | '-'), Term};
Term   = Factor, {'*', Factor};
Factor = '(', Expr, ')' | Number;
Number = Digit, {Digit};
Digit  = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';

rdp-gen emits types shaped like:

export type ExprNode = {
  kind: 'Expr'
  term: TermNode
  item1: { item0: string; term: TermNode }[]
}

export type TermNode = {
  kind: 'Term'
  factor: FactorNode
  item1: { item0: string; factor: FactorNode }[]
}

// … one type per rule …

export type ParseTree =
  | ExprNode
  | TermNode
  | FactorNode
  | NumberNode
  | DigitNode

The kind field is a string-literal type equal to the rule name, so the union is a proper TypeScript discriminated union — you can narrow on kind with a switch statement or if check and TypeScript will track the exact node shape in each branch.

rdp-gen always emits a childNodes helper alongside the parser:

export function childNodes(node: ParseTree): ParseTree[]

childNodes returns the direct ParseTree children of any node — every named rule node reachable from its fields, with arrays and anonymous sequence objects unwrapped. Pair it with the Visitor<T> type and visit() from @configuredthings/rdp.js to build tree walkers, linters, and analysis passes. See Tree walking for worked examples.

The generated code compiles without errors under strict: true and noUncheckedIndexedAccess: true; this is enforced by the rdp-gen test suite.

With --observable, the static method gains an optional second parameter:

static parse(input: string, observer?: ParseObserver): RootNode

Pass any ParseObserver implementation (e.g. DebugObserver, TraceObserver) to trace the parse without constructing the instance directly.


Grammar format — EBNF

rdp-gen accepts ISO 14977 EBNF.

(* A simple arithmetic expression grammar with operator precedence and whitespace.
   Supports +, -, *, / with correct precedence via Expr -> Term -> Factor.
   Parentheses for grouping; integers are one or more decimal digits. *)

Expr   = wsp, Term, {wsp, ('+' | '-'), wsp, Term}, wsp;
Term   = Factor, {wsp, ('*' | '/'), wsp, Factor};
Factor = '(', Expr, ')' | Number;
Number = Digit, {Digit};
Digit  = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
wsp    = {' '};
SyntaxMeaning
'x' or "x"String literal (\n \t \r \\ \' \" escape sequences supported)
A, BSequence (comma concatenation)
A | BAlternation
[A]Optional (zero or one)
{A}Zero or more
A, {A}One or more
n * AExactly n repetitions
A - BException: match A but not B
(A)Grouping

The railroad diagrams below show how these operators combine in the arithmetic grammar — a concrete example of sequencing (,), alternation (|), and repetition ({…}) in practice:

Expr
wsp Term wsp "+" "-" wsp Term wsp
Term
Factor wsp "*" "/" wsp Factor
Factor
"(" Expr ")" Number

Grammar format — ABNF

Pass --format abnf (or use a .abnf file extension) to parse RFC 5234 ABNF. Quoted strings are case-insensitive by default; use --abnf-case-sensitive-strings to preserve exact case.


LL(1) requirement

rdp-gen generates LL(1) parsers only. It detects left recursion at generation time and exits with an error. Common-prefix violations are detected at runtime — if an alternative consumes input and then fails, the generated parser throws:

LL(1) violation: alternative consumed input up to position N before failing
(started at M) — two alternatives share a common prefix

This surfaces non-LL(1) grammars immediately in tests rather than letting them silently mis-parse production input.

If your grammar is not LL(1), rewrite it to eliminate left recursion using iteration (* / +), and ensure each alternative is uniquely identified by its first character.