ultra-parser's tool generates TypeScript for the ultra-parser package, whose runtime interprets the grammar in WebAssembly. It is the tool's only target and its default, so -Dlanguage is not needed.
For a combined grammar T.g4, the tool generates:
TLexer.tswith classTLexer, which extendsLexer.TParser.tswith classTParser, which extendsParser, and a context class per rule and per labeled alternative, e.g.,ExprContextandAddContext.TListener.tswith classTListener, whoseenter*andexit*methods are optional. Pass-no-listenerto skip it.TVisitor.tswith classTVisitor<Result>, which extendsParseTreeVisitor<Result>and whosevisit*methods are optional, with-visitor.
The recognizers hold the serialized ATN and the names of tokens, rules, channels, and modes as static members, e.g., TParser.ID and TParser.RULE_expr. Parsers have a method per rule that parses from the token stream's current token with that rule and returns its context; the stream then stands after the tokens the rule consumed, so rules can be called repeatedly, e.g., one statement at a time:
const parser = new TParser(new CommonTokenStream(new TLexer(CharStream.fromString(input))));
const tree = parser.expr();Each parse copies the stream's tokens into the runtime unless the copy is current, which it is when the tokens are of exactly the CommonToken class and have not changed; tokens of other classes, including subclasses of CommonToken, are copied for every parse.
Context classes have getters for the elements of their rule, as in ANTLR's TypeScript target: ID() for a token that appears once, ID_list() and ID(i) for a token that appears several times, and likewise for rules. Labels are fields with an underscore, e.g., _op for op=('+'|'-'), and arguments, return values, and locals are fields with their names.
Names that the runtime uses get an underscore appended: rule methods named like members of Parser, such as state and context, and getters, arguments, and return values named like members of ParserRuleContext, such as start and parent.
Actions, predicates, and the @init, @after, and @finally actions of rules are TypeScript. They run with the parser (or lexer) as this:
grammar T;
@parser::members {
count = 0;
}
s : (ID {this.count++;})+ {console.log(`${this.count} identifiers`);} ;
ID : [a-z]+ ;
WS : [ \t\r\n]+ -> skip ;Unlike ANTLR's generated code, parser actions do not run inside rule methods; they are hooks that the runtime calls when it passes their place in the grammar. They see the rule context as localctx, which $ctx, $x, $x.text, and so on refer to; since each hook is a function of its own, grammar code refers to the rule's arguments as $x (the context's field), which every hook of the rule shares. $text, this._input, this._ctx, this.state, getExpectedTokens(), and getRuleInvocationStack() reflect the position of the hook.
The parser reads all tokens before it parses, so lexer actions run before parser actions, and the parser cannot switch lexer modes. catch clauses of rules are ignored.
The ultra-parser package exports classes like ANTLR's: CharStream, CommonToken, CommonTokenStream, Lexer, Parser, ParserRuleContext, TerminalNode, ErrorNode, ParseTreeWalker, ParseTreeVisitor, ConsoleErrorListener, DiagnosticErrorListener, and BailErrorStrategy.
- Offsets and columns count Unicode code points, as in ANTLR's Java runtime.
Parser.predictionModeisPredictionMode.SLL,LL(the default), orLL_EXACT_AMBIG_DETECTION, and can change while parsing.Parser.buildParseTrees = falseskips building the tree;addParseListener()receives events as the tree is built.ctx.toStringTree(parser)formats a tree like ANTLR; trees of any depth can be formatted and walked, since the runtime and these functions do not recurse.
Node.js (20.16 or later, which provides process.getBuiltinModule), Bun, and Deno load the WebAssembly module on first use. Elsewhere, call await init(), which fetches ultra_parser.wasm next to the package, or initSync(module) with a compiled module, e.g., one that Cloudflare Workers import.