Error handling: Use a special value (not |null|) to indicate failure

Using a special value to indicate match failure instead of |null| allows
actions to return |null| as a regular value. This simplifies e.g. the
JSON parser.

Note the special value is internal and intentionally undocumented. This
means that there is currently no official way how to trigger a match
failure from an action. This is a temporary state which will be fixed
soon.

The negative performance impact (see below) is probably caused by
changing lot of comparisons against |null| (which likely check the value
against a fixed constant representing |null| in the interpreter) to
comparisons against the special value (which likely check the value
against another value in the interpreter).

Implements part of #198.

Speed impact
------------
Before:     1146.82 kB/s
After:      1031.25 kB/s
Difference: -10.08%

Size impact
-----------
Before:     950817 b
After:      973269 b
Difference: 2.36%

(Measured by /tools/impact with Node.js v0.6.18 on x86_64 GNU/Linux.)
redux
David Majda 11 years ago
parent 435bb8f2df
commit 57e806383c

@ -374,7 +374,7 @@ The action is a piece of JavaScript code that is executed as if it was inside a
function. It gets the match results of labeled expressions in preceding
expression as its arguments. The action should return some JavaScript value
using the `return` statement. This value is considered match result of the
preceding expression. The action can return `null` to indicate a match failure.
preceding expression.
The code inside the action can access all variables and functions defined in the
initializer at the beginning of the grammar. Curly braces in the action code

@ -1,19 +1,5 @@
/* JSON parser based on the grammar described at http://json.org/. */
{
/*
* We can't return |null| in the |value| rule because that would mean parse
* failure. So we return a special object instead and convert it to |null|
* later.
*/
var null_ = new Object;
function fixNull(value) {
return value === null_ ? null : value;
}
}
/* ===== Syntactical Elements ===== */
start
@ -26,9 +12,9 @@ object
members
= head:pair tail:("," _ pair)* {
var result = {};
result[head[0]] = fixNull(head[1]);
result[head[0]] = head[1];
for (var i = 0; i < tail.length; i++) {
result[tail[i][2][0]] = fixNull(tail[i][2][1]);
result[tail[i][2][0]] = tail[i][2][1];
}
return result;
}
@ -42,9 +28,9 @@ array
elements
= head:value tail:("," _ value)* {
var result = [fixNull(head)];
var result = [head];
for (var i = 0; i < tail.length; i++) {
result.push(fixNull(tail[i][2]));
result.push(tail[i][2]);
}
return result;
}
@ -56,7 +42,7 @@ value
/ array
/ "true" _ { return true; }
/ "false" _ { return false; }
/ "null" _ { return null_; }
/ "null" _ { return null; }
/* ===== Lexical Elements ===== */

@ -70,7 +70,7 @@ var utils = require("../../utils"),
*
* [11] IF_ERROR t, f
*
* if (stack.top() === null) {
* if (stack.top() === FAILED) {
* interpret(ip + 3, ip + 3 + t);
* } else {
* interpret(ip + 3 + t, ip + 3 + t + f);
@ -78,7 +78,7 @@ var utils = require("../../utils"),
*
* [12] IF_NOT_ERROR t, f
*
* if (stack.top() !== null) {
* if (stack.top() !== FAILED) {
* interpret(ip + 3, ip + 3 + t);
* } else {
* interpret(ip + 3 + t, ip + 3 + t + f);
@ -86,7 +86,7 @@ var utils = require("../../utils"),
*
* [13] WHILE_NOT_ERROR b
*
* while(stack.top() !== null) {
* while(stack.top() !== FAILED) {
* interpret(ip + 2, ip + 2 + b);
* }
*
@ -137,7 +137,7 @@ var utils = require("../../utils"),
*
* [20] FAIL e
*
* stack.push(null);
* stack.push(FAILED);
* fail(consts[e]);
*
* Calls
@ -214,7 +214,7 @@ module.exports = function(ast, options) {
function buildSimplePredicate(expression, negative, context) {
var emptyStringIndex = addConst('""'),
nullIndex = addConst('null');
failedIndex = addConst('peg$FAILED');
return buildSequence(
[op.PUSH_CURR_POS],
@ -235,7 +235,7 @@ module.exports = function(ast, options) {
buildSequence(
[op.POP],
[negative ? op.POP_CURR_POS : op.POP],
[op.PUSH, nullIndex]
[op.PUSH, failedIndex]
)
)
);
@ -244,7 +244,7 @@ module.exports = function(ast, options) {
function buildSemanticPredicate(code, negative, context) {
var functionIndex = addFunctionConst(utils.keys(context.env), code),
emptyStringIndex = addConst('""'),
nullIndex = addConst('null');
failedIndex = addConst('peg$FAILED');
return buildSequence(
[op.REPORT_CURR_POS],
@ -253,11 +253,11 @@ module.exports = function(ast, options) {
[op.IF],
buildSequence(
[op.POP],
[op.PUSH, negative ? nullIndex : emptyStringIndex]
[op.PUSH, negative ? failedIndex : emptyStringIndex]
),
buildSequence(
[op.POP],
[op.PUSH, negative ? emptyStringIndex : nullIndex]
[op.PUSH, negative ? emptyStringIndex : failedIndex]
)
)
);
@ -357,7 +357,7 @@ module.exports = function(ast, options) {
},
sequence: function(node, context) {
var emptyArrayIndex, nullIndex;
var emptyArrayIndex, failIndex;
function buildElementsCode(elements, context) {
var processedCount, functionIndex;
@ -381,7 +381,7 @@ module.exports = function(ast, options) {
buildSequence(
processedCount > 1 ? [op.POP_N, processedCount] : [op.POP],
[op.POP_CURR_POS],
[op.PUSH, nullIndex]
[op.PUSH, failedIndex]
)
)
);
@ -409,7 +409,7 @@ module.exports = function(ast, options) {
}
if (node.elements.length > 0) {
nullIndex = addConst('null');
failedIndex = addConst('peg$FAILED');
return buildSequence(
[op.PUSH_CURR_POS],
@ -500,7 +500,7 @@ module.exports = function(ast, options) {
one_or_more: function(node, context) {
var emptyArrayIndex = addConst('[]');
nullIndex = addConst('null');
failedIndex = addConst('peg$FAILED');
expressionCode = generate(node.expression, {
sp: context.sp + 1,
env: { },
@ -513,7 +513,7 @@ module.exports = function(ast, options) {
buildCondition(
[op.IF_NOT_ERROR],
buildSequence(buildAppendLoop(expressionCode), [op.POP]),
buildSequence([op.POP], [op.POP], [op.PUSH, nullIndex])
buildSequence([op.POP], [op.POP], [op.PUSH, failedIndex])
)
);
},

@ -223,18 +223,18 @@ module.exports = function(ast, options) {
'',
' case ' + op.IF_ERROR + ':', // IF_ERROR t, f
indent10(generateCondition(
'stack[stack.length - 1] === null',
'stack[stack.length - 1] === peg$FAILED',
0
)),
'',
' case ' + op.IF_NOT_ERROR + ':', // IF_NOT_ERROR t, f
indent10(
generateCondition('stack[stack.length - 1] !== null',
generateCondition('stack[stack.length - 1] !== peg$FAILED',
0
)),
'',
' case ' + op.WHILE_NOT_ERROR + ':', // WHILE_NOT_ERROR b
indent10(generateLoop('stack[stack.length - 1] !== null')),
indent10(generateLoop('stack[stack.length - 1] !== peg$FAILED')),
'',
' case ' + op.MATCH_ANY + ':', // MATCH_ANY a, f, ...
indent10(generateCondition('input.length > peg$currPos', 0)),
@ -270,7 +270,7 @@ module.exports = function(ast, options) {
' break;',
'',
' case ' + op.FAIL + ':', // FAIL e
' stack.push(null);',
' stack.push(peg$FAILED);',
' if (peg$silentFails === 0) {',
' peg$fail(peg$consts[bc[ip + 1]]);',
' }',
@ -515,15 +515,15 @@ module.exports = function(ast, options) {
break;
case op.IF_ERROR: // IF_ERROR t, f
compileCondition(stack.top() + ' === null', 0);
compileCondition(stack.top() + ' === peg$FAILED', 0);
break;
case op.IF_NOT_ERROR: // IF_NOT_ERROR t, f
compileCondition(stack.top() + ' !== null', 0);
compileCondition(stack.top() + ' !== peg$FAILED', 0);
break;
case op.WHILE_NOT_ERROR: // WHILE_NOT_ERROR b
compileLoop(stack.top() + ' !== null', 0);
compileLoop(stack.top() + ' !== peg$FAILED', 0);
break;
case op.MATCH_ANY: // MATCH_ANY a, f, ...
@ -585,7 +585,7 @@ module.exports = function(ast, options) {
break;
case op.FAIL: // FAIL e
parts.push(stack.push('null'));
parts.push(stack.push('peg$FAILED'));
parts.push('if (peg$silentFails === 0) { peg$fail(' + c(bc[ip + 1]) + '); }');
ip += 2;
break;
@ -745,6 +745,8 @@ module.exports = function(ast, options) {
'',
' function parse(input) {',
' var options = arguments.length > 1 ? arguments[1] : {},',
'',
' peg$FAILED = {},',
''
].join('\n'));
@ -937,7 +939,7 @@ module.exports = function(ast, options) {
parts.push([
'',
' if (peg$result !== null && peg$currPos === input.length) {',
' if (peg$result !== peg$FAILED && peg$currPos === input.length) {',
' return peg$result;',
' } else {',
' peg$cleanupExpected(peg$maxFailExpected);',

File diff suppressed because it is too large Load Diff

@ -163,7 +163,7 @@ describe("compiler pass |generateBytecode|", function() {
it("defines correct constants", function() {
expect(pass).toChangeAST(grammar, constsDetails([
'null',
'peg$FAILED',
'"a"',
'{ type: "literal", value: "a", description: "\\"a\\"" }',
'"b"',
@ -219,7 +219,7 @@ describe("compiler pass |generateBytecode|", function() {
it("defines correct constants", function() {
expect(pass).toChangeAST(grammar, constsDetails([
'null',
'peg$FAILED',
'"a"',
'{ type: "literal", value: "a", description: "\\"a\\"" }',
'"b"',
@ -273,7 +273,7 @@ describe("compiler pass |generateBytecode|", function() {
it("defines correct constants", function() {
expect(pass).toChangeAST(grammar, constsDetails([
'""',
'null',
'peg$FAILED',
'"a"',
'{ type: "literal", value: "a", description: "\\"a\\"" }'
]));
@ -302,7 +302,7 @@ describe("compiler pass |generateBytecode|", function() {
it("defines correct constants", function() {
expect(pass).toChangeAST(grammar, constsDetails([
'""',
'null',
'peg$FAILED',
'"a"',
'{ type: "literal", value: "a", description: "\\"a\\"" }'
]));
@ -328,7 +328,7 @@ describe("compiler pass |generateBytecode|", function() {
it("defines correct constants", function() {
expect(pass).toChangeAST(
grammar,
constsDetails(['function() { code }', '""', 'null'])
constsDetails(['function() { code }', '""', 'peg$FAILED'])
);
});
});
@ -372,7 +372,7 @@ describe("compiler pass |generateBytecode|", function() {
it("defines correct constants", function() {
expect(pass).toChangeAST(grammar, constsDetails([
'null',
'peg$FAILED',
'"a"',
'{ type: "literal", value: "a", description: "\\"a\\"" }',
'"b"',
@ -405,7 +405,7 @@ describe("compiler pass |generateBytecode|", function() {
it("defines correct constants", function() {
expect(pass).toChangeAST(
grammar,
constsDetails(['function() { code }', '""', 'null'])
constsDetails(['function() { code }', '""', 'peg$FAILED'])
);
});
});
@ -449,7 +449,7 @@ describe("compiler pass |generateBytecode|", function() {
it("defines correct constants", function() {
expect(pass).toChangeAST(grammar, constsDetails([
'null',
'peg$FAILED',
'"a"',
'{ type: "literal", value: "a", description: "\\"a\\"" }',
'"b"',
@ -528,7 +528,7 @@ describe("compiler pass |generateBytecode|", function() {
it("defines correct constants", function() {
expect(pass).toChangeAST(grammar, constsDetails([
'[]',
'null',
'peg$FAILED',
'"a"',
'{ type: "literal", value: "a", description: "\\"a\\"" }'
]));

@ -266,18 +266,12 @@ describe("generated parser", function() {
});
describe("action code", function() {
it("tranforms the expression result by returnung a non-|null| value", function() {
it("tranforms the expression result by returnung a value", function() {
var parser = PEG.buildParser('start = "a" { return 42; }', options);
expect(parser).toParse("a", 42);
});
it("causes match failure by returning |null|", function() {
var parser = PEG.buildParser('start = "a" { return null; }', options);
expect(parser).toFailToParse("a");
});
it("is not called when the expression does not match", function() {
var parser = PEG.buildParser(
'start = "a" { throw "Boom!"; } / "b"',
@ -360,15 +354,6 @@ describe("generated parser", function() {
expect(parser).toParse("a", { a: 42 }, { a: 42 });
});
it("does not advance position when the expression matches but the action returns |null|", function() {
var parser = PEG.buildParser(
'start = "a" { return null; } / "a"',
options
);
expect(parser).toParse("a", "a");
});
});
describe("sequence matching", function() {

Loading…
Cancel
Save