1167 lines
33 KiB
JavaScript
1167 lines
33 KiB
JavaScript
|
|
// @ts-check
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
const asts = require("../asts");
|
||
|
|
const op = require("../opcodes");
|
||
|
|
const visitor = require("../visitor");
|
||
|
|
const Intern = require("../intern");
|
||
|
|
const { ALWAYS_MATCH, SOMETIMES_MATCH, NEVER_MATCH } = require("./inference-match-result");
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {import("../../peg")} PEG
|
||
|
|
*/
|
||
|
|
|
||
|
|
// Generates bytecode.
|
||
|
|
//
|
||
|
|
// Instructions
|
||
|
|
// ============
|
||
|
|
//
|
||
|
|
// Stack Manipulation
|
||
|
|
// ------------------
|
||
|
|
//
|
||
|
|
// [35] PUSH_EMPTY_STRING
|
||
|
|
//
|
||
|
|
// stack.push("");
|
||
|
|
//
|
||
|
|
// [1] PUSH_UNDEFINED
|
||
|
|
//
|
||
|
|
// stack.push(undefined);
|
||
|
|
//
|
||
|
|
// [2] PUSH_NULL
|
||
|
|
//
|
||
|
|
// stack.push(null);
|
||
|
|
//
|
||
|
|
// [3] PUSH_FAILED
|
||
|
|
//
|
||
|
|
// stack.push(FAILED);
|
||
|
|
//
|
||
|
|
// [4] PUSH_EMPTY_ARRAY
|
||
|
|
//
|
||
|
|
// stack.push([]);
|
||
|
|
//
|
||
|
|
// [5] PUSH_CURR_POS
|
||
|
|
//
|
||
|
|
// stack.push(currPos);
|
||
|
|
//
|
||
|
|
// [6] POP
|
||
|
|
//
|
||
|
|
// stack.pop();
|
||
|
|
//
|
||
|
|
// [7] POP_CURR_POS
|
||
|
|
//
|
||
|
|
// currPos = stack.pop();
|
||
|
|
//
|
||
|
|
// [8] POP_N n
|
||
|
|
//
|
||
|
|
// stack.pop(n);
|
||
|
|
//
|
||
|
|
// [9] NIP
|
||
|
|
//
|
||
|
|
// value = stack.pop();
|
||
|
|
// stack.pop();
|
||
|
|
// stack.push(value);
|
||
|
|
//
|
||
|
|
// [10] APPEND
|
||
|
|
//
|
||
|
|
// value = stack.pop();
|
||
|
|
// array = stack.pop();
|
||
|
|
// array.push(value);
|
||
|
|
// stack.push(array);
|
||
|
|
//
|
||
|
|
// [11] WRAP n
|
||
|
|
//
|
||
|
|
// stack.push(stack.pop(n));
|
||
|
|
//
|
||
|
|
// [12] TEXT
|
||
|
|
//
|
||
|
|
// stack.push(input.substring(stack.pop(), currPos));
|
||
|
|
//
|
||
|
|
// [36] PLUCK n, k, p1, ..., pK
|
||
|
|
//
|
||
|
|
// value = [stack[p1], ..., stack[pK]]; // when k != 1
|
||
|
|
// -or-
|
||
|
|
// value = stack[p1]; // when k == 1
|
||
|
|
//
|
||
|
|
// stack.pop(n);
|
||
|
|
// stack.push(value);
|
||
|
|
//
|
||
|
|
// Conditions and Loops
|
||
|
|
// --------------------
|
||
|
|
//
|
||
|
|
// [13] IF t, f
|
||
|
|
//
|
||
|
|
// if (stack.top()) {
|
||
|
|
// interpret(ip + 3, ip + 3 + t);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 3 + t, ip + 3 + t + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [14] IF_ERROR t, f
|
||
|
|
//
|
||
|
|
// if (stack.top() === FAILED) {
|
||
|
|
// interpret(ip + 3, ip + 3 + t);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 3 + t, ip + 3 + t + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [15] IF_NOT_ERROR t, f
|
||
|
|
//
|
||
|
|
// if (stack.top() !== FAILED) {
|
||
|
|
// interpret(ip + 3, ip + 3 + t);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 3 + t, ip + 3 + t + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [30] IF_LT min, t, f
|
||
|
|
//
|
||
|
|
// if (stack.top().length < min) {
|
||
|
|
// interpret(ip + 3, ip + 3 + t);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 3 + t, ip + 3 + t + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [31] IF_GE max, t, f
|
||
|
|
//
|
||
|
|
// if (stack.top().length >= max) {
|
||
|
|
// interpret(ip + 3, ip + 3 + t);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 3 + t, ip + 3 + t + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [32] IF_LT_DYNAMIC min, t, f
|
||
|
|
//
|
||
|
|
// if (stack.top().length < stack[min]) {
|
||
|
|
// interpret(ip + 3, ip + 3 + t);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 3 + t, ip + 3 + t + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [33] IF_GE_DYNAMIC max, t, f
|
||
|
|
//
|
||
|
|
// if (stack.top().length >= stack[max]) {
|
||
|
|
// interpret(ip + 3, ip + 3 + t);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 3 + t, ip + 3 + t + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [16] WHILE_NOT_ERROR b
|
||
|
|
//
|
||
|
|
// while(stack.top() !== FAILED) {
|
||
|
|
// interpret(ip + 2, ip + 2 + b);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// Matching
|
||
|
|
// --------
|
||
|
|
//
|
||
|
|
// [17] MATCH_ANY a, f, ...
|
||
|
|
//
|
||
|
|
// if (input.length > currPos) {
|
||
|
|
// interpret(ip + 3, ip + 3 + a);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 3 + a, ip + 3 + a + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [18] MATCH_STRING s, a, f, ...
|
||
|
|
//
|
||
|
|
// if (input.substr(currPos, literals[s].length) === literals[s]) {
|
||
|
|
// interpret(ip + 4, ip + 4 + a);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 4 + a, ip + 4 + a + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [19] MATCH_STRING_IC s, a, f, ...
|
||
|
|
//
|
||
|
|
// if (input.substr(currPos, literals[s].length).toLowerCase() === literals[s]) {
|
||
|
|
// interpret(ip + 4, ip + 4 + a);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 4 + a, ip + 4 + a + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [20] MATCH_CHAR_CLASS c, a, f, ...
|
||
|
|
//
|
||
|
|
// if (classes[c].test(input.charAt(currPos))) {
|
||
|
|
// interpret(ip + 4, ip + 4 + a);
|
||
|
|
// } else {
|
||
|
|
// interpret(ip + 4 + a, ip + 4 + a + f);
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// [21] ACCEPT_N n
|
||
|
|
//
|
||
|
|
// stack.push(input.substring(currPos, n));
|
||
|
|
// currPos += n;
|
||
|
|
//
|
||
|
|
// [22] ACCEPT_STRING s
|
||
|
|
//
|
||
|
|
// stack.push(literals[s]);
|
||
|
|
// currPos += literals[s].length;
|
||
|
|
//
|
||
|
|
// [23] FAIL e
|
||
|
|
//
|
||
|
|
// stack.push(FAILED);
|
||
|
|
// fail(expectations[e]);
|
||
|
|
//
|
||
|
|
// Calls
|
||
|
|
// -----
|
||
|
|
//
|
||
|
|
// [24] LOAD_SAVED_POS p
|
||
|
|
//
|
||
|
|
// savedPos = stack[p];
|
||
|
|
//
|
||
|
|
// [25] UPDATE_SAVED_POS
|
||
|
|
//
|
||
|
|
// savedPos = currPos;
|
||
|
|
//
|
||
|
|
// [26] CALL f, n, pc, p1, p2, ..., pN
|
||
|
|
//
|
||
|
|
// value = functions[f](stack[p1], ..., stack[pN]);
|
||
|
|
// stack.pop(n);
|
||
|
|
// stack.push(value);
|
||
|
|
//
|
||
|
|
// Rules
|
||
|
|
// -----
|
||
|
|
//
|
||
|
|
// [27] RULE r
|
||
|
|
//
|
||
|
|
// stack.push(parseRule(r));
|
||
|
|
//
|
||
|
|
// [41] LIBRARY_RULE moduleIndex whatIndex
|
||
|
|
//
|
||
|
|
// stack.push(callLibrary(module, what));
|
||
|
|
//
|
||
|
|
// Failure Reporting
|
||
|
|
// -----------------
|
||
|
|
//
|
||
|
|
// [28] SILENT_FAILS_ON
|
||
|
|
//
|
||
|
|
// silentFails++;
|
||
|
|
//
|
||
|
|
// [29] SILENT_FAILS_OFF
|
||
|
|
//
|
||
|
|
// silentFails--;
|
||
|
|
//
|
||
|
|
// Source Mapping
|
||
|
|
// --------------
|
||
|
|
//
|
||
|
|
// [37] SOURCE_MAP_PUSH n
|
||
|
|
//
|
||
|
|
// Everything generated from here until the corresponding SOURCE_MAP_POP
|
||
|
|
// will be wrapped in a SourceNode tagged with locations[n].
|
||
|
|
//
|
||
|
|
// [38] SOURCE_MAP_POP
|
||
|
|
//
|
||
|
|
// See above.
|
||
|
|
//
|
||
|
|
// [39] SOURCE_MAP_LABEL_PUSH sp, label, loc
|
||
|
|
//
|
||
|
|
// Mark that the stack location sp will be used to hold the value
|
||
|
|
// of the label named literals[label], with location info locations[loc]
|
||
|
|
//
|
||
|
|
// [40] SOURCE_MAP_LABEL_POP sp
|
||
|
|
//
|
||
|
|
// End the region started by [39]
|
||
|
|
//
|
||
|
|
// This pass can use the results of other previous passes, each of which can
|
||
|
|
// change the AST (and, as consequence, the bytecode).
|
||
|
|
//
|
||
|
|
// In particular, if the pass |inferenceMatchResult| has been run before this pass,
|
||
|
|
// then each AST node will contain a |match| property, which represents a possible
|
||
|
|
// match result of the node:
|
||
|
|
// - `<0` - node is never matched, for example, `!('a'*)` (negation of the always
|
||
|
|
// matched node). Generator can put |FAILED| to the stack immediately
|
||
|
|
// - `=0` - sometimes node matched, sometimes not. This is the same behavior
|
||
|
|
// when |match| is missed
|
||
|
|
// - `>0` - node is always matched, for example, `'a'*` (because result is an
|
||
|
|
// empty array, or an array with some elements). The generator does not
|
||
|
|
// need to add a check for |FAILED|, because it is impossible
|
||
|
|
//
|
||
|
|
// To handle the situation, when the |inferenceMatchResult| has not run (that
|
||
|
|
// happens, for example, in tests), the |match| value extracted using the
|
||
|
|
// `|0` trick, which performing cast of any value to an integer with value `0`
|
||
|
|
// that is equivalent of an unknown match result and signals the generator that
|
||
|
|
// runtime check for the |FAILED| is required. Trick is explained on the
|
||
|
|
// Wikipedia page (https://en.wikipedia.org/wiki/Asm.js#Code_generation)
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* @param {PEG.ast.Grammar} ast
|
||
|
|
* @param {PEG.ParserOptions} options
|
||
|
|
*/
|
||
|
|
function generateBytecode(ast, options) {
|
||
|
|
const literals = new Intern();
|
||
|
|
/** @type Intern<PEG.ast.CharacterClass, PEG.ast.GrammarCharacterClass> */
|
||
|
|
const classes = new Intern({
|
||
|
|
stringify: JSON.stringify,
|
||
|
|
/** @type {(input: PEG.ast.CharacterClass) => PEG.ast.GrammarCharacterClass} */
|
||
|
|
convert: node => ({
|
||
|
|
value: node.parts,
|
||
|
|
inverted: node.inverted,
|
||
|
|
ignoreCase: node.ignoreCase,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
/** @type Intern<PEG.ast.GrammarExpectation> */
|
||
|
|
const expectations = new Intern({
|
||
|
|
stringify: JSON.stringify,
|
||
|
|
});
|
||
|
|
/**
|
||
|
|
* @type {Intern<string | undefined, string>}
|
||
|
|
*/
|
||
|
|
const importedNames = new Intern();
|
||
|
|
/** @type PEG.ast.FunctionConst[] */
|
||
|
|
const functions = [];
|
||
|
|
/** @type PEG.LocationRange[] */
|
||
|
|
const locations = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {boolean} predicate
|
||
|
|
* @param {string[]} params
|
||
|
|
* @param {{code:string; codeLocation: PEG.LocationRange}} node
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
function addFunctionConst(predicate, params, node) {
|
||
|
|
const func = {
|
||
|
|
predicate,
|
||
|
|
params,
|
||
|
|
body: node.code,
|
||
|
|
location: node.codeLocation,
|
||
|
|
};
|
||
|
|
const pattern = JSON.stringify(func);
|
||
|
|
const index = functions.findIndex(f => JSON.stringify(f) === pattern);
|
||
|
|
|
||
|
|
return index === -1 ? functions.push(func) - 1 : index;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {PEG.LocationRange} location
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
function addLocation(location) {
|
||
|
|
// Don't bother de-duplicating. There can be a lot of locations,
|
||
|
|
// they will almost never collide, and unlike the "consts" above,
|
||
|
|
// it won't affect code generation even if they do.
|
||
|
|
return locations.push(location) - 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @typedef {Record<string, number>} Env */
|
||
|
|
/** @typedef {{ sp: number; env:Env; action:PEG.ast.Action|null; pluck?: number[] }} Context */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Env} env
|
||
|
|
* @returns {Env}
|
||
|
|
*/
|
||
|
|
function cloneEnv(env) {
|
||
|
|
/** @type {Env} */
|
||
|
|
const clone = {};
|
||
|
|
|
||
|
|
Object.keys(env).forEach(name => {
|
||
|
|
clone[name] = env[name];
|
||
|
|
});
|
||
|
|
|
||
|
|
return clone;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number[]} first
|
||
|
|
* @param {number[][]} args
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildSequence(first, ...args) {
|
||
|
|
return first.concat(...args);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number} match
|
||
|
|
* @param {number[]} condCode
|
||
|
|
* @param {number[]} thenCode
|
||
|
|
* @param {number[]} elseCode
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildCondition(match, condCode, thenCode, elseCode) {
|
||
|
|
if (match === ALWAYS_MATCH) { return thenCode; }
|
||
|
|
if (match === NEVER_MATCH) { return elseCode; }
|
||
|
|
|
||
|
|
return condCode.concat(
|
||
|
|
[thenCode.length, elseCode.length],
|
||
|
|
thenCode,
|
||
|
|
elseCode
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number[]} condCode
|
||
|
|
* @param {number[]} bodyCode
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildLoop(condCode, bodyCode) {
|
||
|
|
return condCode.concat([bodyCode.length], bodyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number} functionIndex
|
||
|
|
* @param {number} delta
|
||
|
|
* @param {Env} env
|
||
|
|
* @param {number} sp
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildCall(functionIndex, delta, env, sp) {
|
||
|
|
const params = Object.keys(env).map(name => sp - env[name]);
|
||
|
|
|
||
|
|
return [op.CALL, functionIndex, delta, params.length].concat(params);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @template T
|
||
|
|
* @param {PEG.ast.Expr<T>} expression
|
||
|
|
* @param {boolean} negative
|
||
|
|
* @param {Context} context
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildSimplePredicate(expression, negative, context) {
|
||
|
|
const match = expression.match || 0;
|
||
|
|
|
||
|
|
return buildSequence(
|
||
|
|
[op.PUSH_CURR_POS],
|
||
|
|
[op.SILENT_FAILS_ON],
|
||
|
|
// eslint-disable-next-line no-use-before-define -- Mutual recursion
|
||
|
|
generate(expression, {
|
||
|
|
sp: context.sp + 1,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
}),
|
||
|
|
[op.SILENT_FAILS_OFF],
|
||
|
|
buildCondition(
|
||
|
|
negative ? -match : match,
|
||
|
|
[negative ? op.IF_ERROR : op.IF_NOT_ERROR],
|
||
|
|
buildSequence(
|
||
|
|
[op.POP],
|
||
|
|
[negative ? op.POP : op.POP_CURR_POS],
|
||
|
|
[op.PUSH_UNDEFINED]
|
||
|
|
),
|
||
|
|
buildSequence(
|
||
|
|
[op.POP],
|
||
|
|
[negative ? op.POP_CURR_POS : op.POP],
|
||
|
|
[op.PUSH_FAILED]
|
||
|
|
)
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* @param {PEG.ast.SemanticPredicate} node
|
||
|
|
* @param {boolean} negative
|
||
|
|
* @param {Context} context
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildSemanticPredicate(node, negative, context) {
|
||
|
|
const functionIndex = addFunctionConst(
|
||
|
|
true, Object.keys(context.env), node
|
||
|
|
);
|
||
|
|
|
||
|
|
return buildSequence(
|
||
|
|
[op.UPDATE_SAVED_POS],
|
||
|
|
buildCall(functionIndex, 0, context.env, context.sp),
|
||
|
|
buildCondition(
|
||
|
|
node.match || 0,
|
||
|
|
[op.IF],
|
||
|
|
buildSequence(
|
||
|
|
[op.POP],
|
||
|
|
negative ? [op.PUSH_FAILED] : [op.PUSH_UNDEFINED]
|
||
|
|
),
|
||
|
|
buildSequence(
|
||
|
|
[op.POP],
|
||
|
|
negative ? [op.PUSH_UNDEFINED] : [op.PUSH_FAILED]
|
||
|
|
)
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number[]} expressionCode
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildAppendLoop(expressionCode) {
|
||
|
|
return buildLoop(
|
||
|
|
[op.WHILE_NOT_ERROR],
|
||
|
|
buildSequence([op.APPEND], expressionCode)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {never} boundary
|
||
|
|
* @returns {Error}
|
||
|
|
*/
|
||
|
|
function unknownBoundary(boundary) {
|
||
|
|
const b = /** @type {{ type: string }} */(boundary);
|
||
|
|
return new Error(`Unknown boundary type "${b.type}" for the "repeated" node`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* @param {import("../../peg").ast.RepeatedBoundary} boundary
|
||
|
|
* @param {{ [label: string]: number}} env Mapping of label names to stack positions
|
||
|
|
* @param {number} sp Number of the first free slot in the stack
|
||
|
|
* @param {number} offset
|
||
|
|
*
|
||
|
|
* @returns {{ pre: number[], post: number[], sp: number}}
|
||
|
|
* Bytecode that should be added before and after parsing and new
|
||
|
|
* first free slot in the stack
|
||
|
|
*/
|
||
|
|
function buildRangeCall(boundary, env, sp, offset) {
|
||
|
|
switch (boundary.type) {
|
||
|
|
case "constant":
|
||
|
|
return { pre: [], post: [], sp };
|
||
|
|
case "variable":
|
||
|
|
boundary.sp = offset + sp - env[boundary.value];
|
||
|
|
return { pre: [], post: [], sp };
|
||
|
|
case "function": {
|
||
|
|
boundary.sp = offset;
|
||
|
|
|
||
|
|
const functionIndex = addFunctionConst(
|
||
|
|
true,
|
||
|
|
Object.keys(env),
|
||
|
|
{ code: boundary.value, codeLocation: boundary.codeLocation }
|
||
|
|
);
|
||
|
|
|
||
|
|
return {
|
||
|
|
pre: buildCall(functionIndex, 0, env, sp),
|
||
|
|
post: [op.NIP],
|
||
|
|
// +1 for the function result
|
||
|
|
sp: sp + 1,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// istanbul ignore next Because we never generate invalid boundary type we cannot reach this branch
|
||
|
|
default:
|
||
|
|
throw unknownBoundary(boundary);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* eslint capitalized-comments: "off" */
|
||
|
|
/**
|
||
|
|
* @param {number[]} expressionCode Bytecode for parsing repetitions
|
||
|
|
* @param {import("../../peg").ast.RepeatedBoundary} max Maximum boundary of repetitions.
|
||
|
|
* If `null`, the maximum boundary is unlimited
|
||
|
|
*
|
||
|
|
* @returns {number[]} Bytecode that performs check of the maximum boundary
|
||
|
|
*/
|
||
|
|
function buildCheckMax(expressionCode, max) {
|
||
|
|
if (max.value !== null) {
|
||
|
|
const checkCode = max.type === "constant"
|
||
|
|
? [op.IF_GE, max.value]
|
||
|
|
: [op.IF_GE_DYNAMIC, max.sp || 0];
|
||
|
|
|
||
|
|
// Push `peg$FAILED` - this break loop on next iteration, so |result|
|
||
|
|
// will contains not more then |max| elements.
|
||
|
|
return buildCondition(
|
||
|
|
SOMETIMES_MATCH,
|
||
|
|
checkCode, // if (r.length >= max) stack:[ [elem...] ]
|
||
|
|
[op.PUSH_FAILED], // elem = peg$FAILED; stack:[ [elem...], peg$FAILED ]
|
||
|
|
expressionCode // else
|
||
|
|
); // elem = expr(); stack:[ [elem...], elem ]
|
||
|
|
}
|
||
|
|
|
||
|
|
return expressionCode;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* eslint capitalized-comments: "off" */
|
||
|
|
/**
|
||
|
|
* @param {number[]} expressionCode Bytecode for parsing repeated elements
|
||
|
|
* @param {import("../../peg").ast.RepeatedBoundary} min Minimum boundary of repetitions.
|
||
|
|
* If `null`, the minimum boundary is zero
|
||
|
|
*
|
||
|
|
* @returns {number[]} Bytecode that performs check of the minimum boundary
|
||
|
|
*/
|
||
|
|
function buildCheckMin(expressionCode, min) {
|
||
|
|
const checkCode = min.type === "constant"
|
||
|
|
? [op.IF_LT, min.value]
|
||
|
|
: [op.IF_LT_DYNAMIC, min.sp || 0];
|
||
|
|
|
||
|
|
return buildSequence(
|
||
|
|
expressionCode, // result = [elem...]; stack:[ pos, [elem...] ]
|
||
|
|
buildCondition(
|
||
|
|
SOMETIMES_MATCH,
|
||
|
|
checkCode, // if (result.length < min) {
|
||
|
|
/* eslint-disable @stylistic/indent -- Clarity */
|
||
|
|
[op.POP, op.POP_CURR_POS, // currPos = savedPos; stack:[ ]
|
||
|
|
op.PUSH_FAILED], // result = peg$FAILED; stack:[ peg$FAILED ]
|
||
|
|
/* eslint-enable @stylistic/indent */
|
||
|
|
[op.NIP] // } stack:[ [elem...] ]
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* @param {PEG.ast.Expression|null} delimiterNode
|
||
|
|
* @param {number} expressionMatch
|
||
|
|
* @param {number[]} expressionCode
|
||
|
|
* @param {Context} context
|
||
|
|
* @param {number} offset
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildRangeBody(
|
||
|
|
delimiterNode,
|
||
|
|
expressionMatch,
|
||
|
|
expressionCode,
|
||
|
|
context,
|
||
|
|
offset
|
||
|
|
) {
|
||
|
|
if (delimiterNode) {
|
||
|
|
return buildSequence( // stack:[ ]
|
||
|
|
[op.PUSH_CURR_POS], // pos = peg$currPos; stack:[ pos ]
|
||
|
|
// eslint-disable-next-line no-use-before-define -- Mutual recursion
|
||
|
|
generate(delimiterNode, { // item = delim(); stack:[ pos, delim ]
|
||
|
|
// +1 for the saved offset
|
||
|
|
sp: context.sp + offset + 1,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
}),
|
||
|
|
buildCondition(
|
||
|
|
delimiterNode.match || 0,
|
||
|
|
[op.IF_NOT_ERROR], // if (item !== peg$FAILED) {
|
||
|
|
buildSequence(
|
||
|
|
[op.POP], // stack:[ pos ]
|
||
|
|
expressionCode, // item = expr(); stack:[ pos, item ]
|
||
|
|
buildCondition(
|
||
|
|
-expressionMatch,
|
||
|
|
[op.IF_ERROR], // if (item === peg$FAILED) {
|
||
|
|
// If element FAILED, rollback currPos to saved value.
|
||
|
|
/* eslint-disable @stylistic/indent -- Clarity */
|
||
|
|
[op.POP, // stack:[ pos ]
|
||
|
|
op.POP_CURR_POS, // peg$currPos = pos; stack:[ ]
|
||
|
|
op.PUSH_FAILED], // item = peg$FAILED; stack:[ peg$FAILED ]
|
||
|
|
/* eslint-enable @stylistic/indent */
|
||
|
|
// Else, just drop saved currPos.
|
||
|
|
[op.NIP] // } stack:[ item ]
|
||
|
|
)
|
||
|
|
), // }
|
||
|
|
// If delimiter FAILED, currPos not changed, so just drop it.
|
||
|
|
[op.NIP] // stack:[ peg$FAILED ]
|
||
|
|
) // stack:[ <?> ]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return expressionCode;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {PEG.compiler.visitor.NodeTypes} generators
|
||
|
|
* @returns {PEG.compiler.visitor.AnyFunction}
|
||
|
|
*/
|
||
|
|
function wrapGenerators(generators) {
|
||
|
|
if (options && options.output === "source-and-map") {
|
||
|
|
Object.keys(generators).forEach(name => {
|
||
|
|
// @ts-ignore
|
||
|
|
const generator = generators[name];
|
||
|
|
// @ts-ignore
|
||
|
|
generators[name] = function(node, ...args) {
|
||
|
|
const generated = generator(node, ...args);
|
||
|
|
// Some generators ("grammar" and "rule") don't return anything,
|
||
|
|
// so don't bother wrapping their return values.
|
||
|
|
if (generated === undefined || !node.location) {
|
||
|
|
return generated;
|
||
|
|
}
|
||
|
|
return buildSequence(
|
||
|
|
[
|
||
|
|
op.SOURCE_MAP_PUSH,
|
||
|
|
addLocation(node.location),
|
||
|
|
],
|
||
|
|
generated,
|
||
|
|
[
|
||
|
|
op.SOURCE_MAP_POP,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return visitor.build(generators);
|
||
|
|
}
|
||
|
|
|
||
|
|
const generate = wrapGenerators({
|
||
|
|
grammar(node) {
|
||
|
|
node.rules.forEach(generate);
|
||
|
|
|
||
|
|
node.literals = literals.items;
|
||
|
|
node.classes = classes.items;
|
||
|
|
node.expectations = expectations.items;
|
||
|
|
node.importedNames = importedNames.items;
|
||
|
|
node.functions = functions;
|
||
|
|
node.locations = locations;
|
||
|
|
},
|
||
|
|
|
||
|
|
rule(node) {
|
||
|
|
node.bytecode = generate(node.expression, {
|
||
|
|
sp: -1, // Stack pointer
|
||
|
|
env: {}, // Mapping of label names to stack positions
|
||
|
|
pluck: [], // Fields that have been picked
|
||
|
|
action: null, // Action nodes pass themselves to children here
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
named(node, context) {
|
||
|
|
const match = node.match || 0;
|
||
|
|
// Expectation not required if node always fail
|
||
|
|
const nameIndex = (match === NEVER_MATCH)
|
||
|
|
? -1
|
||
|
|
: expectations.add({ type: "rule", value: node.name });
|
||
|
|
|
||
|
|
// The code generated below is slightly suboptimal because |FAIL| pushes
|
||
|
|
// to the stack, so we need to stick a |POP| in front of it. We lack a
|
||
|
|
// dedicated instruction that would just report the failure and not touch
|
||
|
|
// the stack.
|
||
|
|
return buildSequence(
|
||
|
|
[op.SILENT_FAILS_ON],
|
||
|
|
generate(node.expression, context),
|
||
|
|
[op.SILENT_FAILS_OFF],
|
||
|
|
buildCondition(match, [op.IF_ERROR], [op.FAIL, nameIndex], [])
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
choice(node, context) {
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* @param {PEG.ast.Expression[]} alternatives
|
||
|
|
* @param {Context} context
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildAlternativesCode(alternatives, context) {
|
||
|
|
const match = alternatives[0].match || 0;
|
||
|
|
const first = generate(alternatives[0], {
|
||
|
|
sp: context.sp,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
});
|
||
|
|
// If an alternative always match, no need to generate code for the next
|
||
|
|
// alternatives. Because their will never tried to match, any side-effects
|
||
|
|
// from next alternatives is impossible so we can skip their generation
|
||
|
|
if (match === ALWAYS_MATCH) {
|
||
|
|
return first;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Even if an alternative never match it can have side-effects from
|
||
|
|
// a semantic predicates or an actions, so we can not skip generation
|
||
|
|
// of the first alternative.
|
||
|
|
// We can do that when analysis for possible side-effects will be introduced
|
||
|
|
return buildSequence(
|
||
|
|
first,
|
||
|
|
alternatives.length > 1
|
||
|
|
? buildCondition(
|
||
|
|
SOMETIMES_MATCH,
|
||
|
|
[op.IF_ERROR],
|
||
|
|
buildSequence(
|
||
|
|
[op.POP],
|
||
|
|
buildAlternativesCode(alternatives.slice(1), context)
|
||
|
|
),
|
||
|
|
[]
|
||
|
|
)
|
||
|
|
: []
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return buildAlternativesCode(node.alternatives, context);
|
||
|
|
},
|
||
|
|
|
||
|
|
action(node, context) {
|
||
|
|
const env = cloneEnv(context.env);
|
||
|
|
const emitCall = node.expression.type !== "sequence"
|
||
|
|
|| node.expression.elements.length === 0;
|
||
|
|
const expressionCode = generate(node.expression, {
|
||
|
|
sp: context.sp + (emitCall ? 1 : 0),
|
||
|
|
env,
|
||
|
|
action: node,
|
||
|
|
});
|
||
|
|
const match = node.expression.match || 0;
|
||
|
|
// Function only required if expression can match
|
||
|
|
const functionIndex = emitCall && match !== NEVER_MATCH
|
||
|
|
? addFunctionConst(false, Object.keys(env), node)
|
||
|
|
: -1;
|
||
|
|
|
||
|
|
return emitCall
|
||
|
|
? buildSequence(
|
||
|
|
[op.PUSH_CURR_POS],
|
||
|
|
expressionCode,
|
||
|
|
buildCondition(
|
||
|
|
match,
|
||
|
|
[op.IF_NOT_ERROR],
|
||
|
|
buildSequence(
|
||
|
|
[op.LOAD_SAVED_POS, 1],
|
||
|
|
buildCall(functionIndex, 1, env, context.sp + 2)
|
||
|
|
),
|
||
|
|
[]
|
||
|
|
),
|
||
|
|
[op.NIP]
|
||
|
|
)
|
||
|
|
: expressionCode;
|
||
|
|
},
|
||
|
|
|
||
|
|
sequence(node, context) {
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* @param {PEG.ast.Expression[]} elements
|
||
|
|
* @param {Context} context
|
||
|
|
* @returns {number[]}
|
||
|
|
*/
|
||
|
|
function buildElementsCode(elements, context) {
|
||
|
|
if (elements.length > 0) {
|
||
|
|
const processedCount = node.elements.length - elements.length + 1;
|
||
|
|
|
||
|
|
return buildSequence(
|
||
|
|
generate(elements[0], {
|
||
|
|
sp: context.sp,
|
||
|
|
env: context.env,
|
||
|
|
pluck: context.pluck,
|
||
|
|
action: null,
|
||
|
|
}),
|
||
|
|
buildCondition(
|
||
|
|
elements[0].match || 0,
|
||
|
|
[op.IF_NOT_ERROR],
|
||
|
|
buildElementsCode(elements.slice(1), {
|
||
|
|
sp: context.sp + 1,
|
||
|
|
env: context.env,
|
||
|
|
pluck: context.pluck,
|
||
|
|
action: context.action,
|
||
|
|
}),
|
||
|
|
buildSequence(
|
||
|
|
processedCount > 1 ? [op.POP_N, processedCount] : [op.POP],
|
||
|
|
[op.POP_CURR_POS],
|
||
|
|
[op.PUSH_FAILED]
|
||
|
|
)
|
||
|
|
)
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
if (context.pluck && context.pluck.length > 0) {
|
||
|
|
return buildSequence(
|
||
|
|
[op.PLUCK, node.elements.length + 1, context.pluck.length],
|
||
|
|
context.pluck.map(eSP => context.sp - eSP)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (context.action) {
|
||
|
|
const functionIndex = addFunctionConst(
|
||
|
|
false,
|
||
|
|
Object.keys(context.env),
|
||
|
|
context.action
|
||
|
|
);
|
||
|
|
|
||
|
|
return buildSequence(
|
||
|
|
[op.LOAD_SAVED_POS, node.elements.length],
|
||
|
|
buildCall(
|
||
|
|
functionIndex,
|
||
|
|
node.elements.length + 1,
|
||
|
|
context.env,
|
||
|
|
context.sp
|
||
|
|
)
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
return buildSequence([op.WRAP, node.elements.length], [op.NIP]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return buildSequence(
|
||
|
|
[op.PUSH_CURR_POS],
|
||
|
|
buildElementsCode(node.elements, {
|
||
|
|
sp: context.sp + 1,
|
||
|
|
env: context.env,
|
||
|
|
pluck: [],
|
||
|
|
action: context.action,
|
||
|
|
})
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
labeled(node, context) {
|
||
|
|
let env = context.env;
|
||
|
|
const label = node.label;
|
||
|
|
const sp = context.sp + 1;
|
||
|
|
|
||
|
|
if (label) {
|
||
|
|
env = cloneEnv(context.env);
|
||
|
|
context.env[label] = sp;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (node.pick) {
|
||
|
|
context.pluck.push(sp);
|
||
|
|
}
|
||
|
|
|
||
|
|
const expression = generate(node.expression, {
|
||
|
|
sp: context.sp,
|
||
|
|
env,
|
||
|
|
action: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (label && node.labelLocation && options && options.output === "source-and-map") {
|
||
|
|
return buildSequence(
|
||
|
|
[
|
||
|
|
op.SOURCE_MAP_LABEL_PUSH,
|
||
|
|
sp,
|
||
|
|
literals.add(label),
|
||
|
|
addLocation(node.labelLocation),
|
||
|
|
],
|
||
|
|
expression,
|
||
|
|
[op.SOURCE_MAP_LABEL_POP, sp]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return expression;
|
||
|
|
},
|
||
|
|
|
||
|
|
text(node, context) {
|
||
|
|
return buildSequence(
|
||
|
|
[op.PUSH_CURR_POS],
|
||
|
|
generate(node.expression, {
|
||
|
|
sp: context.sp + 1,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
}),
|
||
|
|
buildCondition(
|
||
|
|
node.match || 0,
|
||
|
|
[op.IF_NOT_ERROR],
|
||
|
|
buildSequence([op.POP], [op.TEXT]),
|
||
|
|
[op.NIP]
|
||
|
|
)
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
simple_and(node, context) {
|
||
|
|
return buildSimplePredicate(node.expression, false, context);
|
||
|
|
},
|
||
|
|
|
||
|
|
simple_not(node, context) {
|
||
|
|
return buildSimplePredicate(node.expression, true, context);
|
||
|
|
},
|
||
|
|
|
||
|
|
optional(node, context) {
|
||
|
|
return buildSequence(
|
||
|
|
generate(node.expression, {
|
||
|
|
sp: context.sp,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
}),
|
||
|
|
buildCondition(
|
||
|
|
// Check expression match, not the node match
|
||
|
|
// If expression always match, no need to replace FAILED to NULL,
|
||
|
|
// because FAILED will never appeared
|
||
|
|
-(node.expression.match || 0),
|
||
|
|
[op.IF_ERROR],
|
||
|
|
buildSequence([op.POP], [op.PUSH_NULL]),
|
||
|
|
[]
|
||
|
|
)
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
zero_or_more(node, context) {
|
||
|
|
const expressionCode = generate(node.expression, {
|
||
|
|
sp: context.sp + 1,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
return buildSequence(
|
||
|
|
[op.PUSH_EMPTY_ARRAY],
|
||
|
|
expressionCode,
|
||
|
|
buildAppendLoop(expressionCode),
|
||
|
|
[op.POP]
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
one_or_more(node, context) {
|
||
|
|
const expressionCode = generate(node.expression, {
|
||
|
|
sp: context.sp + 1,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
return buildSequence(
|
||
|
|
[op.PUSH_EMPTY_ARRAY],
|
||
|
|
expressionCode,
|
||
|
|
buildCondition(
|
||
|
|
// Condition depends on the expression match, not the node match
|
||
|
|
node.expression.match || 0,
|
||
|
|
[op.IF_NOT_ERROR],
|
||
|
|
buildSequence(buildAppendLoop(expressionCode), [op.POP]),
|
||
|
|
buildSequence([op.POP], [op.POP], [op.PUSH_FAILED])
|
||
|
|
)
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
repeated(node, context) {
|
||
|
|
// Handle case when minimum was literally equals to maximum
|
||
|
|
const min = node.min ? node.min : node.max;
|
||
|
|
const hasMin = min.type !== "constant" || min.value > 0;
|
||
|
|
const hasBoundedMax = node.max.type !== "constant" && node.max.value !== null;
|
||
|
|
|
||
|
|
// +1 for the result slot with an array
|
||
|
|
// +1 if we have non-constant (i.e. potentially non-zero) or non-zero minimum
|
||
|
|
// for the position before match for backtracking
|
||
|
|
const offset = hasMin ? 2 : 1;
|
||
|
|
|
||
|
|
// Do not generate function for "minimum" if grammar used `exact` syntax
|
||
|
|
const minCode = node.min
|
||
|
|
? buildRangeCall(
|
||
|
|
node.min,
|
||
|
|
context.env,
|
||
|
|
context.sp,
|
||
|
|
// +1 for the result slot with an array
|
||
|
|
// +1 for the saved position
|
||
|
|
// +1 if we have a "function" maximum it occupies an additional slot in the stack
|
||
|
|
2 + (node.max.type === "function" ? 1 : 0)
|
||
|
|
)
|
||
|
|
: { pre: [], post: [], sp: context.sp };
|
||
|
|
const maxCode = buildRangeCall(node.max, context.env, minCode.sp, offset);
|
||
|
|
|
||
|
|
const firstExpressionCode = generate(node.expression, {
|
||
|
|
sp: maxCode.sp + offset,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
});
|
||
|
|
const expressionCode = node.delimiter !== null
|
||
|
|
? generate(node.expression, {
|
||
|
|
// +1 for the saved position before parsing the `delimiter elem` pair
|
||
|
|
sp: maxCode.sp + offset + 1,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
})
|
||
|
|
: firstExpressionCode;
|
||
|
|
const bodyCode = buildRangeBody(
|
||
|
|
node.delimiter,
|
||
|
|
node.expression.match || 0,
|
||
|
|
expressionCode,
|
||
|
|
context,
|
||
|
|
offset
|
||
|
|
);
|
||
|
|
// Check the high boundary, if it is defined.
|
||
|
|
const checkMaxCode = buildCheckMax(bodyCode, node.max);
|
||
|
|
// For dynamic high boundary we need check the first iteration, because the result can be
|
||
|
|
// empty. Constant boundaries does not require that check, because they are always >=1
|
||
|
|
const firstElemCode = hasBoundedMax
|
||
|
|
? buildCheckMax(firstExpressionCode, node.max)
|
||
|
|
: firstExpressionCode;
|
||
|
|
const mainLoopCode = buildSequence(
|
||
|
|
// If the low boundary present, then backtracking is possible, so save the current pos
|
||
|
|
hasMin ? [op.PUSH_CURR_POS] : [], // var savedPos = curPos; stack:[ pos ]
|
||
|
|
[op.PUSH_EMPTY_ARRAY], // var result = []; stack:[ pos, [] ]
|
||
|
|
firstElemCode, // var elem = expr(); stack:[ pos, [], elem ]
|
||
|
|
buildAppendLoop(checkMaxCode), // while(...)r.push(elem); stack:[ pos, [...], elem|peg$FAILED ]
|
||
|
|
[op.POP] // stack:[ pos, [...] ] (pop elem===`peg$FAILED`)
|
||
|
|
);
|
||
|
|
|
||
|
|
return buildSequence(
|
||
|
|
minCode.pre,
|
||
|
|
maxCode.pre,
|
||
|
|
// Check the low boundary, if it is defined and not |0|.
|
||
|
|
hasMin
|
||
|
|
? buildCheckMin(mainLoopCode, min)
|
||
|
|
: mainLoopCode,
|
||
|
|
maxCode.post,
|
||
|
|
minCode.post
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
group(node, context) {
|
||
|
|
return generate(node.expression, {
|
||
|
|
sp: context.sp,
|
||
|
|
env: cloneEnv(context.env),
|
||
|
|
action: null,
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
semantic_and(node, context) {
|
||
|
|
return buildSemanticPredicate(node, false, context);
|
||
|
|
},
|
||
|
|
|
||
|
|
semantic_not(node, context) {
|
||
|
|
return buildSemanticPredicate(node, true, context);
|
||
|
|
},
|
||
|
|
|
||
|
|
rule_ref(node) {
|
||
|
|
return [op.RULE, asts.indexOfRule(ast, node.name)];
|
||
|
|
},
|
||
|
|
|
||
|
|
library_ref(node) {
|
||
|
|
return [
|
||
|
|
op.LIBRARY_RULE,
|
||
|
|
node.libraryNumber,
|
||
|
|
importedNames.add(node.name),
|
||
|
|
];
|
||
|
|
},
|
||
|
|
|
||
|
|
literal(node) {
|
||
|
|
if (node.value.length > 0) {
|
||
|
|
const match = node.match || 0;
|
||
|
|
// String only required if condition is generated or string is
|
||
|
|
// case-sensitive and node always match
|
||
|
|
const needConst = match === SOMETIMES_MATCH
|
||
|
|
|| (match === ALWAYS_MATCH && !node.ignoreCase);
|
||
|
|
const stringIndex = needConst
|
||
|
|
? literals.add(
|
||
|
|
node.ignoreCase ? node.value.toLowerCase() : node.value
|
||
|
|
)
|
||
|
|
: -1;
|
||
|
|
// Expectation not required if node always match
|
||
|
|
const expectedIndex = (match !== ALWAYS_MATCH)
|
||
|
|
? expectations.add({
|
||
|
|
type: "literal",
|
||
|
|
value: node.value,
|
||
|
|
ignoreCase: node.ignoreCase,
|
||
|
|
})
|
||
|
|
: -1;
|
||
|
|
|
||
|
|
// For case-sensitive strings the value must match the beginning of the
|
||
|
|
// remaining input exactly. As a result, we can use |ACCEPT_STRING| and
|
||
|
|
// save one |substr| call that would be needed if we used |ACCEPT_N|.
|
||
|
|
return buildCondition(
|
||
|
|
match,
|
||
|
|
node.ignoreCase
|
||
|
|
? [op.MATCH_STRING_IC, stringIndex]
|
||
|
|
: [op.MATCH_STRING, stringIndex],
|
||
|
|
node.ignoreCase
|
||
|
|
? [op.ACCEPT_N, node.value.length]
|
||
|
|
: [op.ACCEPT_STRING, stringIndex],
|
||
|
|
[op.FAIL, expectedIndex]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return [op.PUSH_EMPTY_STRING];
|
||
|
|
},
|
||
|
|
|
||
|
|
class(node) {
|
||
|
|
const match = node.match || 0;
|
||
|
|
// Character class constant only required if condition is generated
|
||
|
|
const classIndex = match === SOMETIMES_MATCH ? classes.add(node) : -1;
|
||
|
|
// Expectation not required if node always match
|
||
|
|
const expectedIndex = (match !== ALWAYS_MATCH)
|
||
|
|
? expectations.add({
|
||
|
|
type: "class",
|
||
|
|
value: node.parts,
|
||
|
|
inverted: node.inverted,
|
||
|
|
ignoreCase: node.ignoreCase,
|
||
|
|
})
|
||
|
|
: -1;
|
||
|
|
|
||
|
|
return buildCondition(
|
||
|
|
match,
|
||
|
|
[op.MATCH_CHAR_CLASS, classIndex],
|
||
|
|
[op.ACCEPT_N, 1],
|
||
|
|
[op.FAIL, expectedIndex]
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
any(node) {
|
||
|
|
const match = node.match || 0;
|
||
|
|
// Expectation not required if node always match
|
||
|
|
const expectedIndex = (match !== ALWAYS_MATCH)
|
||
|
|
? expectations.add({
|
||
|
|
type: "any",
|
||
|
|
})
|
||
|
|
: -1;
|
||
|
|
|
||
|
|
return buildCondition(
|
||
|
|
match,
|
||
|
|
[op.MATCH_ANY],
|
||
|
|
[op.ACCEPT_N, 1],
|
||
|
|
[op.FAIL, expectedIndex]
|
||
|
|
);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
generate(ast);
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = generateBytecode;
|