From 5460a881af45e5bc6d2682f4d480266d822427c2 Mon Sep 17 00:00:00 2001 From: David Majda Date: Sat, 30 Nov 2013 17:26:06 +0100 Subject: [PATCH] Error handling: Implement the |error| function The |error| function allows users to report custom match failures inside actions. If the |error| function is called, and the reported match failure turns out to be the cause of a parse error, the error message reported by the parser will be exactly the one specified in the |error| call. Implements part of #198. Speed impact ------------ Before: 999.83 kB/s After: 1000.84 kB/s Difference: 0.10% Size impact ----------- Before: 1017212 b After: 1019968 b Difference: 0.27% (Measured by /tools/impact with Node.js v0.6.18 on x86_64 GNU/Linux.) --- README.md | 4 ++ lib/compiler/passes/generate-javascript.js | 37 +++++++++++++-- lib/parser.js | 35 ++++++++++++-- spec/generated-parser.spec.js | 54 ++++++++++++++++++++++ 4 files changed, 121 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 05f7acc..3c86bee 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,10 @@ To indicate a match failure, the code inside the action can invoke the at the current position. This description will be used as part of a message of the exception thrown if the match failure leads to an parse error. +The code inside an action can also invoke the `error` function. It takes one +parameter — an error message. This message will be used by the exception thrown +if the match failure leads to an parse error. + 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 must be balanced. diff --git a/lib/compiler/passes/generate-javascript.js b/lib/compiler/passes/generate-javascript.js index 2bdd8bd..cfdc383 100644 --- a/lib/compiler/passes/generate-javascript.js +++ b/lib/compiler/passes/generate-javascript.js @@ -745,6 +745,7 @@ module.exports = function(ast, options) { ' peg$cachedPosDetails = { line: 1, column: 1, seenCR: false },', ' peg$maxFailPos = 0,', ' peg$maxFailExpected = [],', + ' peg$maxFailMessage = null,', ' peg$silentFails = 0,', // 0 = report failures, > 0 = silence failures ' peg$userFail = false,', '' @@ -809,6 +810,13 @@ module.exports = function(ast, options) { ' peg$userFail = true;', ' }', '', + ' function error(message) {', + ' if (peg$silentFails === 0) {', + ' peg$error(message, peg$reportedPos);', + ' }', + ' peg$userFail = true;', + ' }', + '', ' function peg$computePosDetails(pos) {', ' function advance(details, startPos, endPos) {', ' var p, ch;', @@ -848,11 +856,24 @@ module.exports = function(ast, options) { ' if (pos > peg$maxFailPos) {', ' peg$maxFailPos = pos;', ' peg$maxFailExpected = [];', + ' peg$maxFailMessage = null;', ' }', '', ' peg$maxFailExpected.push(expected);', ' }', '', + ' function peg$error(message, pos) {', + ' if (pos < peg$maxFailPos) { return; }', + '', + ' if (pos > peg$maxFailPos) {', + ' peg$maxFailPos = pos;', + ' peg$maxFailExpected = [];', + ' peg$maxFailMessage = null;', + ' }', + '', + ' peg$maxFailMessage = message;', + ' }', + '', ' function peg$buildException() {', ' function cleanupExpected(expected) {', ' var i = 1;', @@ -934,13 +955,19 @@ module.exports = function(ast, options) { '', ' var pos = Math.max(peg$currPos, peg$maxFailPos),', ' posDetails = peg$computePosDetails(pos),', - ' found = pos < input.length ? input.charAt(pos) : null;', - '', - ' cleanupExpected(peg$maxFailExpected);', + ' expected = peg$maxFailMessage === null ? peg$maxFailExpected : null,', + ' found = pos < input.length ? input.charAt(pos) : null,', + ' message = peg$maxFailMessage !== null', + ' ? peg$maxFailMessage', + ' : buildMessage(expected, found);', + '', + ' if (expected !== null) {', + ' cleanupExpected(expected);', + ' }', '', ' return new SyntaxError(', - ' buildMessage(peg$maxFailExpected, found),', - ' peg$maxFailExpected,', + ' message,', + ' expected,', ' found,', ' pos,', ' posDetails.line,', diff --git a/lib/parser.js b/lib/parser.js index 330631c..afa58c7 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -340,6 +340,7 @@ module.exports = (function() { peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }, peg$maxFailPos = 0, peg$maxFailExpected = [], + peg$maxFailMessage = null, peg$silentFails = 0, peg$userFail = false, @@ -379,6 +380,13 @@ module.exports = (function() { peg$userFail = true; } + function error(message) { + if (peg$silentFails === 0) { + peg$error(message, peg$reportedPos); + } + peg$userFail = true; + } + function peg$computePosDetails(pos) { function advance(details, startPos, endPos) { var p, ch; @@ -418,11 +426,24 @@ module.exports = (function() { if (pos > peg$maxFailPos) { peg$maxFailPos = pos; peg$maxFailExpected = []; + peg$maxFailMessage = null; } peg$maxFailExpected.push(expected); } + function peg$error(message, pos) { + if (pos < peg$maxFailPos) { return; } + + if (pos > peg$maxFailPos) { + peg$maxFailPos = pos; + peg$maxFailExpected = []; + peg$maxFailMessage = null; + } + + peg$maxFailMessage = message; + } + function peg$buildException() { function cleanupExpected(expected) { var i = 1; @@ -494,13 +515,19 @@ module.exports = (function() { var pos = Math.max(peg$currPos, peg$maxFailPos), posDetails = peg$computePosDetails(pos), - found = pos < input.length ? input.charAt(pos) : null; + expected = peg$maxFailMessage === null ? peg$maxFailExpected : null, + found = pos < input.length ? input.charAt(pos) : null, + message = peg$maxFailMessage !== null + ? peg$maxFailMessage + : buildMessage(expected, found); - cleanupExpected(peg$maxFailExpected); + if (expected !== null) { + cleanupExpected(expected); + } return new SyntaxError( - buildMessage(peg$maxFailExpected, found), - peg$maxFailExpected, + message, + expected, found, pos, posDetails.line, diff --git a/spec/generated-parser.spec.js b/spec/generated-parser.spec.js index cfabe0d..a1a5bd4 100644 --- a/spec/generated-parser.spec.js +++ b/spec/generated-parser.spec.js @@ -397,6 +397,60 @@ describe("generated parser", function() { }); }); + describe("|error| function", function() { + it("generates a custom match failure", function() { + var parser = PEG.buildParser( + 'start = "a" { error("a"); }', + options + ); + + expect(parser).toFailToParse("a", { + offset: 0, + line: 1, + column: 1, + expected: null, + found: "a", + message: "a" + }); + }); + + it("generated failures overrides failures generated before", function() { + var parser = PEG.buildParser( + 'start = "a" / ("b" { error("b"); })', + options + ); + + expect(parser).toFailToParse("b", { + message: "b", + expected: null + }); + }); + + it("generated failures override failures generated after", function() { + var parser = PEG.buildParser( + 'start = ("a" { error("a"); }) / "b"', + options + ); + + expect(parser).toFailToParse("a", { + message: "a", + expected: null + }); + }); + + it("the last invocation wins", function() { + var parser = PEG.buildParser( + 'start = "a" { error("a1"); error("a2"); }', + options + ); + + expect(parser).toFailToParse("a", { + message: "a2", + expected: null + }); + }); + }); + it("can use functions defined in the initializer", function() { var parser = PEG.buildParser([ '{ function f() { return 42; } }',