Skip to content

Commit

Permalink
Error handling: Use a special value (not |null|) to indicate failure
Browse files Browse the repository at this point in the history
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.)
  • Loading branch information
dmajda committed Dec 1, 2013
1 parent 435bb8f commit 57e8063
Show file tree
Hide file tree
Showing 7 changed files with 409 additions and 434 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 5 additions & 19 deletions examples/json.pegjs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -56,7 +42,7 @@ value
/ array
/ "true" _ { return true; }
/ "false" _ { return false; }
/ "null" _ { return null_; }
/ "null" _ { return null; }

/* ===== Lexical Elements ===== */

Expand Down
28 changes: 14 additions & 14 deletions lib/compiler/passes/generate-bytecode.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,23 @@ 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);
* }
*
* [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);
* }
*
* [13] WHILE_NOT_ERROR b
*
* while(stack.top() !== null) {
* while(stack.top() !== FAILED) {
* interpret(ip + 2, ip + 2 + b);
* }
*
Expand Down Expand Up @@ -137,7 +137,7 @@ var utils = require("../../utils"),
*
* [20] FAIL e
*
* stack.push(null);
* stack.push(FAILED);
* fail(consts[e]);
*
* Calls
Expand Down Expand Up @@ -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],
Expand All @@ -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]
)
)
);
Expand All @@ -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],
Expand All @@ -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]
)
)
);
Expand Down Expand Up @@ -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;
Expand All @@ -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]
)
)
);
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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: { },
Expand All @@ -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])
)
);
},
Expand Down
20 changes: 11 additions & 9 deletions lib/compiler/passes/generate-javascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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]]);',
' }',
Expand Down Expand Up @@ -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, ...
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -745,6 +745,8 @@ module.exports = function(ast, options) {
'',
' function parse(input) {',
' var options = arguments.length > 1 ? arguments[1] : {},',
'',
' peg$FAILED = {},',
''
].join('\n'));

Expand Down Expand Up @@ -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);',
Expand Down

0 comments on commit 57e8063

Please sign in to comment.