{ } qjs-modules

grammar

Source: lib/parser/grammar.js (pure JS) — default export: Grammar

A composable recursive-descent parsing library built on top of the parser driver and the lexer C module. Grammars are constructed as trees of GrammarRule instances that are combined through methods (then, or, optional, many, some, not, as, map) — no QuickJS operator-overloading extension is required.

The companion ebnf module exports buildGrammar(source, filename) which produces a Grammar from a BNF / yacc-style source file.

Exports

ExportArgsKindDescription
Grammarstart?classNamed collection of rules with a start symbol. (default export)
ParseNoderule, children, tokensclassA node in a parse tree.
ParseContextparser, grammarclassBuffered, back-trackable view of a token stream.
GrammarRulenameclassAbstract base for every grammar element.
Terminalmatcher, name?classMatches a token by type / lexeme / regex / predicate.
NonTerminalnameclassLate-bound reference to another rule in the grammar.
Sequencechildren, name?classMatches children in order.
Alternativeschildren, name?classTries children in order, first match wins.
Optionalchild, name?classZero or one occurrence.
ZeroOrMorechild, name?classZero or more occurrences.
OneOrMorechild, name?classOne or more occurrences.
Notchild, name?classNegative lookahead.
Peekchild, name?classPositive lookahead.
Mappedchild, fn, name?classApplies a transform to the parse result.
Empty—classAlways succeeds without consuming input.
coerce(v)1functionTurns a string / RegExp / function into a Terminal.
t(matcher, name?)1–2factoryShortcut for new Terminal(...).
ref(name)1factoryShortcut for new NonTerminal(name).
seq(...items)*factoryShortcut for new Sequence(...).
alt(...items)*factoryShortcut for new Alternatives(...).
opt(item)1factoryShortcut for new Optional(...).
many(item)1factoryShortcut for new ZeroOrMore(...).
some(item)1factoryShortcut for new OneOrMore(...).
not(item)1factoryShortcut for new Not(...).
peek(item)1factoryShortcut for new Peek(...).
empty()0factoryShortcut for new Empty().

GrammarRule

Every combinator derives from GrammarRule. Two methods drive the machinery:

MethodArgsDescription
parse(ctx)1Entry point. Snapshots the context, calls _parse, rewinds on failure. Returns a ParseNode or null.
_parse(ctx)1Abstract; subclasses implement matching logic.

Composition methods (fluent)

MethodArgsDescription
then(...rest)*Sequence. Named sequences are treated as one group, unnamed ones flatten.
or(...rest)*Alternatives. Named alternatives are one group, unnamed ones flatten.
optional()0Wraps in Optional.
many()0Wraps in ZeroOrMore.
some()0Wraps in OneOrMore.
not()0Wraps in Not.
as(name)1Returns a shallow copy with a new .name (used in the parse tree and in toString).
map(fn)1Wraps in Mapped; fn(node, ctx) is called on each successful match.
toString()0Returns a compact EBNF-like rendering.

Grammar

MethodArgsDescription
define(name, rule)2Registers a rule under name. First define sets the start rule if none.
get(name)1Fetches a registered rule.
ref(name)1Convenience: returns a NonTerminal.
terminal(matcher, name?)1–2Convenience: returns a Terminal.
seq(...items)*Convenience: returns a Sequence.
alt(...items)*Convenience: returns an Alternatives.
parse(parser)1Runs the grammar over a Parser (see parser). Returns the root ParseNode or throws.
toString()0Prints all rules in EBNF-like form.
PropertyKindDescription
rulesMap<string, GrammarRule>Named rules.
startstring | nullName of the start rule.

ParseNode

PropertyKindDescription
ruleGrammarRuleThe rule that produced this node.
childrenParseNode[]Nested parse-tree children.
tokensToken[]Every leaf token spanned by this node.
typegetterRule name (or class name if unnamed).
textgetterConcatenated lexemes of the spanned tokens.
locgetterLocation of the first spanned token.

ParseContext

Grammar.parse wraps a Parser in a ParseContext internally; you rarely construct one manually. It buffers tokens and offers cheap back-tracking:

Method / PropertyDescription
peek(offset = 0)Look ahead without consuming.
consume()Consume and return the current token.
mark() / restore(pos)Backtracking primitives.
eofWhether the token stream is exhausted.

Terminal matchers

new Terminal(matcher) accepts:

matcherMeaning
stringMatches when token.type === matcher or token.lexeme === matcher.
RegExpMatches when matcher.test(token.lexeme) is true.
functionMatches when matcher(token) returns truthy.

Example — fluent method-style CSV parser

import CSVLexer from './lib/lexer/csv.js';
import Parser from './lib/parser.js';
import Grammar, { t } from './lib/parser/grammar.js';

// row = field (separator field)*
const field = t('field');
const sep   = t('separator');
const nl    = t('nl');

const row = field.then(sep.then(field).many()).as('row');
const csv = row.then(nl.then(row).many()).then(nl.optional()).as('csv');

const g = new Grammar();
g.define('csv', csv);

const parser = new Parser(new CSVLexer('a,b,c\n1,"hi, there",2\n', 'x.csv'));
const tree = g.parse(parser);
console.log(tree.type);           // "csv"

Renders as:

csv = "field" ("separator" "field")* ("nl" "field" ("separator" "field")*)* "nl"?

Example — driven from a .y file via buildGrammar

import { buildGrammar } from './lib/parser/ebnf.js';
import Parser from './lib/parser.js';
import BNFLexer from './lib/lexer/bnf.js';
import { readFileSync } from 'fs';

const src = readFileSync('tests/Shell-Grammar.y', 'utf-8');
const g   = buildGrammar(src, 'Shell-Grammar.y');
console.log(g.start, g.rules.size, [...g.declaredTokens].length);

buildGrammar understands %token, %start, alternatives (|), ?, *, +, single-quoted literals, and skips { … } action blocks. Undeclared identifiers referenced but never defined on the LHS are promoted to Terminals so the grammar remains parseable when the source omits declarations.

Notes and caveats

  • The engine is recursive-descent / PEG-style. Left recursion is not supported — rewrite list : list sep item | item as list : item (sep item)* (or use right recursion) before feeding it through buildGrammar.
  • Alternatives commit to the first successful branch; order matters.
  • ZeroOrMore / OneOrMore break out of their loops if a child matches without consuming any tokens, so Empty.many() cannot run forever.
  • parse(parser) throws on failure with the offending token's Location in the message.