/** * LR parser for Example language generated by the Syntax tool. * * https://www.npmjs.com/package/syntax-cli * * npm install -g syntax-cli * * syntax-cli --help * * To regenerate run: * * syntax-cli \ * --grammar ~/path-to-grammar-file \ * --mode \ * --output ~/ParserClassName.example */ // -------------------------------------------------------------- // Global variable EOF /** * A special "end of file" symbol, used by tokenizer as well. */ const EOF = "$"; // -------------------------------------------------------------- // Tokenizer. /** * Tokenizer class. */ {{{TOKENIZER}}} // -------------------------------------------------------------- // Productions array. /** * Encoded grammar productions table, array of arrays. * * Format of an entry array: * * [ , , ] * * Non-terminal indices are from 0 to Last Non-terminal index. * LR-algorithm uses length of RHS to pop symbols from the stack; this * length is stored as the second element of a record. The last element is * an optional name of the semantic action handler. The first record is always * a special marker [-1, 1] entry representing an augmented production. * * Example: * * [ * [-1, 1], * [0, 3, "_handler1"], * [0, 2, "_handler2"], * ... * ] */ const productions = {{{PRODUCTIONS}}}; // -------------------------------------------------------------- // Parsing table. /** * An array of records, where index is a state number, and a value is a * map from an encoded symbol (number) to a parsing action. * * The parsing action can be "Shift/s", "Reduce/r", a state * transition number, or "Accept/acc". * * Example: * * [ * // 0 * { * 0: "1", * 3: "s8", * 4: "s2", * }, * // 1 * { * 1: "s3", * 2: "s4", * 6: "acc", * }, * ... * ] */ const table = {{{TABLE}}}; // -------------------------------------------------------------- // Parsing stack. /** * Parsing stack. Stores instances of StackEntry, and state numbers. */ let stack = []; // -------------------------------------------------------------- // Global variable __ /** * __ holds a result value from a production * handler. In the grammar usually used as $$. */ let __ = null; // -------------------------------------------------------------- // Global variable __loc /** * __loc holds a result location info. In the grammar usually used as @$. */ let __loc = null; // -------------------------------------------------------------- // Locaiton calculation function. /** * Default location calculation algorithm takes start location * from the first symbol, and the end location from the last symbol. * * The location prologue is inserted at the beginning of each handler * as `@$ = yyloc(@1, @)`. A particular handler may then override it, * or calculate custom location format. */ function yyloc(start, end) { if (!shouldCaptureLocations) { return null; } // Epsilon doesn't produce location. if (!start || !end) { return start || end; } return { startOffset: start.startOffset, endOffset: end.endOffset, startLine: start.startLine, endLine: end.endLine, startColumn: start.startColumn, endColumn: end.endColumn, }; } // -------------------------------------------------------------- // Capture locations /** * Whether locations should be captured and propagated. */ let shouldCaptureLocations = {{{CAPTURE_LOCATIONS}}}; // -------------------------------------------- // Parser. /** * Entires on the parsing stack are: * * - an actual entry {symbol, semanticValue}, implemented by `StackEntry`. * - a state number * - location object (in case of capturing locations, or `null` otherwise) */ class StackEntry { constructor({symbol, semanticValue, loc}) { this.symbol = symbol; this.semanticValue = semanticValue; this.loc = loc; } } /** * Base class for the parser. Implements LR parsing algorithm. * * Should implement at least the following API: * * - parse(string stringToParse): object * - setTokenizer(Tokenizer tokenizer): void, or equivalent Tokenizer * accessor property with a setter. */ class yyparse { constructor() { // A tokenizer instance, which is reused for all // `parse` method calls. The actual string is set // in the tokenizer.initString("..."). this.tokenizer = new Tokenizer(); } /** * On parse begin callback. * * Example: yyparse.onParseBegin = (code) => { ... }; * * Default is empty, but callers (including in module include) can override. */ onParseBegin() {} /** * On parse end callback. * * Example: yyparse.onParseEnd = (value) => { ... }; * * Default is empty, but callers (including in module include) can override. */ onParseEnd() {} /** * Production handles. The handlers receive arguments as _1, _2, etc. * The result is always stored in __. * * Example: * * _handler1(_1, _2, _3) { * __ = _1 + _3; * } */ {{{PRODUCTION_HANDLERS}}} /** * Sets parsing options. */ setOptions(options) { if (options.hasOwnProperty('captureLocations')) { shouldCaptureLocations = options.captureLocations; } return this; } /** * Main parsing method which applies LR-algorithm. */ parse(str) { // On parse begin hook. this.onParseBegin(str); // Tokenizer should be set prior calling the parse. if (!this.tokenizer) { throw new Error("Tokenizer instance isn't specified."); } this.tokenizer.initString(str); // Initialize the parsing stack to the initial state 0. stack = [0]; let token = this.tokenizer.getNextToken(); let shiftedToken = null; // Main parsing loop. do { if (!token) { this.unexpectedEndOfInput(); } let state = Number(stack[stack.length - 1]); let column = token.type; let entry = table[state][column]; if (!entry) { this.unexpectedToken(token); } // --------------------------------------------------- // "Shift". Shift-entries always have 's' as their // first char, after which goes *next state number*, e.g. "s5". // On shift we push the token, and the next state on the stack. if (entry[0] == 's') { let loc = null; if (shouldCaptureLocations) { loc = { startOffset: token.startOffset, endOffset: token.endOffset, startLine: token.startLine, endLine: token.endLine, startColumn: token.startColumn, endColumn: token.endColumn, }; } // Push token. stack.push(new StackEntry({ symbol: token.type, semanticValue: token.value, loc, })); // Push next state number: "s5" -> 5 stack.push(Number(entry.slice(1))); shiftedToken = token; token = this.tokenizer.getNextToken(); } // --------------------------------------------------- // "Reduce". Reduce-entries always have 'r' as their // first char, after which goes *production number* to // reduce by, e.g. "r3" - reduce by production 3 in the grammar. // On reduce, we pop of the stack number of symbols on the RHS // of the production, and their pushed state numbers, i.e. // total RHS * 2 symbols. else if (entry[0] == 'r') { // "r3" -> 3 const productionNumber = Number(entry.slice(1)); const production = productions[productionNumber]; // Handler can be optional: [0, 3] - no handler, // [0, 3, "_handler1"] - has handler. const hasSemanticAction = production.length > 2; const semanticValueArgs = hasSemanticAction ? [] : null; const locationArgs = ( hasSemanticAction && shouldCaptureLocations ? [] : null ); // The length of RHS is stored in the production[1]. let rhsLength = Number(production[1]); if (rhsLength != 0) { while (rhsLength-- > 0) { // Pop the state number. stack.pop(); // Pop the stack entry. let stackEntry = stack.pop(); // Collect all semantic values from the stack // to the arguments list, which is passed to the // semantic action handler. if (hasSemanticAction) { semanticValueArgs.unshift(stackEntry.semanticValue); if (locationArgs) { locationArgs.unshift(stackEntry.loc); } } } } // Previous state is on top of the stack. let previousState = Number(stack[stack.length - 1]); // Reduce to the LHS, which is stored (encoded) in the production[0]. let symbolToReduceWith = Number(production[0]); let reduceStackEntry = new StackEntry({ symbol: symbolToReduceWith, semanticValue: null, // updated further if there is semantic action. loc: null, // updated later }); // Execute the semantic action handler. if (hasSemanticAction) { yytext = shiftedToken != null ? shiftedToken.value : null; yyleng = shiftedToken != null ? shiftedToken.value.length : 0; const semanticActionName = production[2]; const semanticActionHandler = this[semanticActionName]; const semanticActionArgs = ( locationArgs !== null ? semanticValueArgs.concat(locationArgs) : semanticValueArgs ); // Call the action, the result is in __. semanticActionHandler.apply(this, semanticActionArgs); // And patch the `semanticValue` with the result. reduceStackEntry.semanticValue = __; // Add location object if needed. if (locationArgs) { reduceStackEntry.loc = __loc; } } // Then push LHS (reduced entry) onto the stack. stack.push(reduceStackEntry); // And the next state number. const nextState = table[previousState][symbolToReduceWith]; stack.push(nextState); } // --------------------------------------------------- // Accept. Pop starting production and its state number. else if (entry == "acc") { // Pop state number. stack.pop(); // Pop the parsed value. const parsed = stack.pop(); if (stack.length != 1 || stack[stack.length - 1] != 0 || this.tokenizer.hasMoreTokens()) { this.unexpectedToken(token); } const parsedValue = parsed.semanticValue; this.onParseEnd(parsedValue); return parsedValue; } } while (this.tokenizer.hasMoreTokens() || stack.length > 1); return null; } unexpectedToken(token) { if (token.type == EOF) { this.unexpectedEndOfInput(); } this.tokenizer.throwUnexpectedToken( token.value, token.startLine, token.startColumn ); } unexpectedEndOfInput() { this.parseError("Unexpected end of input."); } parseError(message) { throw new SyntaxError(message); } } // -------------------------------------------------------------- // Module include. /** * Module include may contains any arbitrary code (usually import/require * statements which include classes for AST nodes, etc). The code is directly * included to the output generated file. It may also contain callbacks * for parsing hooks, such as `onParseBegin`, `onParseEnd`, etc. */ {{{MODULE_INCLUDE}}} /** * An actual parser class. */ module.exports = class {{{PARSER_CLASS_NAME}}} extends yyparse { };