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-parserMyParser)
--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.