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; } }',