phpeggy
Version:
PHP target for Peggy parser generator
1,042 lines (969 loc) • 31.6 kB
JavaScript
/*
* ! This is a modified version of file "peggy/lib/compiler/passes/generate-bytecode.js"
*/
;
const asts = require("peggy/lib/compiler/asts");
const visitor = require("peggy/lib/compiler/visitor");
const Intern = require("peggy/lib/compiler/intern");
const { ALWAYS_MATCH, SOMETIMES_MATCH, NEVER_MATCH } = require("peggy/lib/compiler/passes/inference-match-result");
const op = require("../opcodes");
/* 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);
* }
*
* [42] MATCH_UNICODE_CLASS c, a, f, ...
*
* if (classes[c].test(input.unicodeCharAt(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));
*
* Failure Reporting
* -----------------
*
* [28] SILENT_FAILS_ON
*
* silentFails++;
*
* [29] SILENT_FAILS_OFF
*
* silentFails--;
*
* Source Mapping (not used for PHP)
* ---------------------------------
*
* [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)
*/
module.exports = function(ast) {
const literals = new Intern();
const classes = new Intern({
stringify: JSON.stringify,
convert: node => ({
value: node.parts,
inverted: node.inverted,
ignoreCase: node.ignoreCase,
unicode: node.unicode,
}),
});
const expectations = new Intern({
stringify: JSON.stringify,
});
const functions = [];
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;
}
function cloneEnv(env) {
const clone = {};
Object.keys(env).forEach(name => {
clone[name] = env[name];
});
return clone;
}
function buildSequence(first, ...args) {
return first.concat(...args);
}
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
);
}
function buildLoop(condCode, bodyCode) {
return condCode.concat([bodyCode.length], bodyCode);
}
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);
}
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]
)
)
);
}
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]
)
)
);
}
function buildAppendLoop(expressionCode) {
return buildLoop(
[op.WHILE_NOT_ERROR],
buildSequence([op.APPEND], expressionCode)
);
}
/**
*
* @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
*
* @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 new Error(`Unknown boundary type "${boundary.type}" for the "repeated" node`);
}
}
/* 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 contain 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, // stack:[ pos ]
op.POP_CURR_POS, // currPos = savedPos; stack:[ ]
op.PUSH_FAILED], // result = peg_FAILED; stack:[ peg_FAILED ]
/* eslint-enable @stylistic/indent */
[op.NIP] // } stack:[ [elem...] ]
)
);
}
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;
}
const generate = visitor.build({
grammar(node) {
node.rules.forEach(generate);
node.literals = literals.items;
node.classes = classes.items;
node.expectations = expectations.items;
node.functions = functions;
},
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 match
const nameIndex = (match === ALWAYS_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) {
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) {
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]);
}
}
}
if (node.elements.length > 0) {
return buildSequence(
[op.PUSH_CURR_POS],
buildElementsCode(node.elements, {
sp: context.sp + 1,
env: context.env,
pluck: [],
action: context.action,
})
);
} else {
return [op.PUSH_EMPTY_ARRAY];
}
},
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);
}
return generate(node.expression, {
sp: context.sp,
env,
action: null,
});
},
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)];
},
literal(node) {
// length of value in terms of code points
const valueLength = [...node.value].length;
if (valueLength > 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, valueLength]
: [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,
unicode: node.unicode,
})
: -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);
};
/*
* MIT License
* Copyright (c) 2010-2025 The Peggy AUTHORS
*
* ------------------------------------------------------------------
* Copyright (c) 2010-2013 David Majda
* Copyright (c) 2014-2025 The PHPeggy AUTHORS
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*/