diff --git a/benchmark/benchmarks.js b/benchmark/benchmarks.js index 227888b..39c8406 100644 --- a/benchmark/benchmarks.js +++ b/benchmark/benchmarks.js @@ -1,42 +1,42 @@ "use strict"; let benchmarks = [ - { - id: "json", - title: "JSON", - tests: [ - { file: "example1.json", title: "Example 1" }, - { file: "example2.json", title: "Example 2" }, - { file: "example3.json", title: "Example 3" }, - { file: "example4.json", title: "Example 4" }, - { file: "example5.json", title: "Example 5" } - ] - }, - { - id: "css", - title: "CSS", - tests: [ - { file: "blueprint/src/reset.css", title: "Blueprint - reset.css (source)" }, - { file: "blueprint/src/typography.css", title: "Blueprint - typography.css (source)" }, - { file: "blueprint/src/forms.css", title: "Blueprint - forms.css (source)" }, - { file: "blueprint/src/grid.css", title: "Blueprint - grid.css (source)" }, - { file: "blueprint/src/print.css", title: "Blueprint - print.css (source)" }, - // Contains syntax errors. - // { file: "blueprint/src/ie.css", title: "Blueprint - ie.css (source)" }, - { file: "blueprint/min/screen.css", title: "Blueprint - screen.css (minified)" }, - { file: "blueprint/min/print.css", title: "Blueprint - print.css (minified)" }, - // Contains syntax errors. - // { file: "blueprint/min/ie.css", title: "Blueprint - ie.css (minified)" }, - { file: "960.gs/src/reset.css", title: "960.gs - reset.css (source)" }, - { file: "960.gs/src/text.css", title: "960.gs - text.css (source)" }, - { file: "960.gs/src/960.css", title: "960.gs - 960.css (source)" }, - { file: "960.gs/src/960_24_col.css", title: "960.gs - 960_24_col.css (source)" }, - { file: "960.gs/min/reset.css", title: "960.gs - reset.css (minified)" }, - { file: "960.gs/min/text.css", title: "960.gs - text.css (minified)" }, - { file: "960.gs/min/960.css", title: "960.gs - 960.css (minified)" }, - { file: "960.gs/min/960_24_col.css", title: "960.gs - 960_24_col.css (minified)" } - ] - } + { + id: "json", + title: "JSON", + tests: [ + { file: "example1.json", title: "Example 1" }, + { file: "example2.json", title: "Example 2" }, + { file: "example3.json", title: "Example 3" }, + { file: "example4.json", title: "Example 4" }, + { file: "example5.json", title: "Example 5" } + ] + }, + { + id: "css", + title: "CSS", + tests: [ + { file: "blueprint/src/reset.css", title: "Blueprint - reset.css (source)" }, + { file: "blueprint/src/typography.css", title: "Blueprint - typography.css (source)" }, + { file: "blueprint/src/forms.css", title: "Blueprint - forms.css (source)" }, + { file: "blueprint/src/grid.css", title: "Blueprint - grid.css (source)" }, + { file: "blueprint/src/print.css", title: "Blueprint - print.css (source)" }, + // Contains syntax errors. + // { file: "blueprint/src/ie.css", title: "Blueprint - ie.css (source)" }, + { file: "blueprint/min/screen.css", title: "Blueprint - screen.css (minified)" }, + { file: "blueprint/min/print.css", title: "Blueprint - print.css (minified)" }, + // Contains syntax errors. + // { file: "blueprint/min/ie.css", title: "Blueprint - ie.css (minified)" }, + { file: "960.gs/src/reset.css", title: "960.gs - reset.css (source)" }, + { file: "960.gs/src/text.css", title: "960.gs - text.css (source)" }, + { file: "960.gs/src/960.css", title: "960.gs - 960.css (source)" }, + { file: "960.gs/src/960_24_col.css", title: "960.gs - 960_24_col.css (source)" }, + { file: "960.gs/min/reset.css", title: "960.gs - reset.css (minified)" }, + { file: "960.gs/min/text.css", title: "960.gs - text.css (minified)" }, + { file: "960.gs/min/960.css", title: "960.gs - 960.css (minified)" }, + { file: "960.gs/min/960_24_col.css", title: "960.gs - 960_24_col.css (minified)" } + ] + } ]; module.exports = benchmarks; diff --git a/benchmark/index.js b/benchmark/index.js index 190d95a..527e5ea 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -6,22 +6,22 @@ let Runner = require("./runner.js"); let benchmarks = require("./benchmarks.js"); $("#run").click(() => { - // Results Table Manipulation + // Results Table Manipulation - let resultsTable = $("#results-table"); + let resultsTable = $("#results-table"); - function appendHeading(heading) { - resultsTable.append( - "" + heading + "" - ); - } + function appendHeading(heading) { + resultsTable.append( + "" + heading + "" + ); + } - function appendResult(klass, title, url, inputSize, parseTime) { - const KB = 1024; - const MS_IN_S = 1000; + function appendResult(klass, title, url, inputSize, parseTime) { + const KB = 1024; + const MS_IN_S = 1000; - resultsTable.append( - "" + resultsTable.append( + "" + "" + (url !== null ? "" : "") + title @@ -46,93 +46,93 @@ $("#run").click(() => { + " kB/s" + "" + "" - ); - } - - // Main - - // Each input is parsed multiple times and the results are averaged. We - // do this for two reasons: - // - // 1. To warm up the interpreter (PEG.js-generated parsers will be - // most likely used repeatedly, so it makes sense to measure - // performance after warming up). - // - // 2. To minimize random errors. - - let runCount = parseInt($("#run-count").val(), 10); - let options = { - cache: $("#cache").is(":checked"), - optimize: $("#optimize").val() - }; - - if (isNaN(runCount) || runCount <= 0) { - alert("Number of runs must be a positive integer."); - - return; - } - - Runner.run(benchmarks, runCount, options, { - readFile(file) { - return $.ajax({ - type: "GET", - url: file, - dataType: "text", - async: false - }).responseText; - }, - - testStart() { - // Nothing to do. - }, - - testFinish(benchmark, test, inputSize, parseTime) { - appendResult( - "individual", - test.title, - benchmark.id + "/" + test.file, - inputSize, - parseTime - ); - }, - - benchmarkStart(benchmark) { - appendHeading(benchmark.title); - }, - - benchmarkFinish(benchmark, inputSize, parseTime) { - appendResult( - "benchmark-total", - benchmark.title + " total", - null, - inputSize, - parseTime - ); - }, - - start() { - $("#run-count, #cache, #run").attr("disabled", "disabled"); - - resultsTable.show(); - $("#results-table tr").slice(1).remove(); - }, - - finish(inputSize, parseTime) { - appendResult( - "total", - "Total", - null, - inputSize, - parseTime - ); - - $.scrollTo("max", { axis: "y", duration: 500 }); - - $("#run-count, #cache, #run").removeAttr("disabled"); - } - }); + ); + } + + // Main + + // Each input is parsed multiple times and the results are averaged. We + // do this for two reasons: + // + // 1. To warm up the interpreter (PEG.js-generated parsers will be + // most likely used repeatedly, so it makes sense to measure + // performance after warming up). + // + // 2. To minimize random errors. + + let runCount = parseInt($("#run-count").val(), 10); + let options = { + cache: $("#cache").is(":checked"), + optimize: $("#optimize").val() + }; + + if (isNaN(runCount) || runCount <= 0) { + alert("Number of runs must be a positive integer."); + + return; + } + + Runner.run(benchmarks, runCount, options, { + readFile(file) { + return $.ajax({ + type: "GET", + url: file, + dataType: "text", + async: false + }).responseText; + }, + + testStart() { + // Nothing to do. + }, + + testFinish(benchmark, test, inputSize, parseTime) { + appendResult( + "individual", + test.title, + benchmark.id + "/" + test.file, + inputSize, + parseTime + ); + }, + + benchmarkStart(benchmark) { + appendHeading(benchmark.title); + }, + + benchmarkFinish(benchmark, inputSize, parseTime) { + appendResult( + "benchmark-total", + benchmark.title + " total", + null, + inputSize, + parseTime + ); + }, + + start() { + $("#run-count, #cache, #run").attr("disabled", "disabled"); + + resultsTable.show(); + $("#results-table tr").slice(1).remove(); + }, + + finish(inputSize, parseTime) { + appendResult( + "total", + "Total", + null, + inputSize, + parseTime + ); + + $.scrollTo("max", { axis: "y", duration: 500 }); + + $("#run-count, #cache, #run").removeAttr("disabled"); + } + }); }); $(document).ready(() => { - $("#run").focus(); + $("#run").focus(); }); diff --git a/benchmark/runner.js b/benchmark/runner.js index 861bf7a..dbd2100 100644 --- a/benchmark/runner.js +++ b/benchmark/runner.js @@ -5,114 +5,114 @@ let peg = require("../lib/peg"); let Runner = { - run(benchmarks, runCount, options, callbacks) { - // Queue - - let Q = { - functions: [], - - add(f) { - this.functions.push(f); - }, - - run() { - if (this.functions.length > 0) { - this.functions.shift()(); - - // We can't use |arguments.callee| here because |this| would get - // messed-up in that case. - setTimeout(() => { Q.run(); }, 0); - } - } - }; - - // The benchmark itself is factored out into several functions (some of them - // generated), which are enqueued and run one by one using |setTimeout|. We - // do this for two reasons: - // - // 1. To avoid bowser mechanism for interrupting long-running scripts to - // kick-in (or at least to not kick-in that often). - // - // 2. To ensure progressive rendering of results in the browser (some - // browsers do not render at all when running JavaScript code). - // - // The enqueued functions share state, which is all stored in the properties - // of the |state| object. - - let state = {}; - - function initialize() { - callbacks.start(); - - state.totalInputSize = 0; - state.totalParseTime = 0; - } - - function benchmarkInitializer(benchmark) { - return function() { - callbacks.benchmarkStart(benchmark); - - state.parser = peg.generate( - callbacks.readFile("../examples/" + benchmark.id + ".pegjs"), - options - ); - state.benchmarkInputSize = 0; - state.benchmarkParseTime = 0; - }; - } - - function testRunner(benchmark, test) { - return function() { - callbacks.testStart(benchmark, test); - - let input = callbacks.readFile(benchmark.id + "/" + test.file); - - let parseTime = 0; - for (let i = 0; i < runCount; i++) { - let t = (new Date()).getTime(); - state.parser.parse(input); - parseTime += (new Date()).getTime() - t; - } - let averageParseTime = parseTime / runCount; - - callbacks.testFinish(benchmark, test, input.length, averageParseTime); - - state.benchmarkInputSize += input.length; - state.benchmarkParseTime += averageParseTime; - }; - } - - function benchmarkFinalizer(benchmark) { - return function() { - callbacks.benchmarkFinish( - benchmark, - state.benchmarkInputSize, - state.benchmarkParseTime - ); - - state.totalInputSize += state.benchmarkInputSize; - state.totalParseTime += state.benchmarkParseTime; - }; - } - - function finalize() { - callbacks.finish(state.totalInputSize, state.totalParseTime); - } - - // Main - - Q.add(initialize); - benchmarks.forEach(benchmark => { - Q.add(benchmarkInitializer(benchmark)); - benchmark.tests.forEach(test => { - Q.add(testRunner(benchmark, test)); - }); - Q.add(benchmarkFinalizer(benchmark)); - }); - Q.add(finalize); - - Q.run(); - } + run(benchmarks, runCount, options, callbacks) { + // Queue + + let Q = { + functions: [], + + add(f) { + this.functions.push(f); + }, + + run() { + if (this.functions.length > 0) { + this.functions.shift()(); + + // We can't use |arguments.callee| here because |this| would get + // messed-up in that case. + setTimeout(() => { Q.run(); }, 0); + } + } + }; + + // The benchmark itself is factored out into several functions (some of them + // generated), which are enqueued and run one by one using |setTimeout|. We + // do this for two reasons: + // + // 1. To avoid bowser mechanism for interrupting long-running scripts to + // kick-in (or at least to not kick-in that often). + // + // 2. To ensure progressive rendering of results in the browser (some + // browsers do not render at all when running JavaScript code). + // + // The enqueued functions share state, which is all stored in the properties + // of the |state| object. + + let state = {}; + + function initialize() { + callbacks.start(); + + state.totalInputSize = 0; + state.totalParseTime = 0; + } + + function benchmarkInitializer(benchmark) { + return function() { + callbacks.benchmarkStart(benchmark); + + state.parser = peg.generate( + callbacks.readFile("../examples/" + benchmark.id + ".pegjs"), + options + ); + state.benchmarkInputSize = 0; + state.benchmarkParseTime = 0; + }; + } + + function testRunner(benchmark, test) { + return function() { + callbacks.testStart(benchmark, test); + + let input = callbacks.readFile(benchmark.id + "/" + test.file); + + let parseTime = 0; + for (let i = 0; i < runCount; i++) { + let t = (new Date()).getTime(); + state.parser.parse(input); + parseTime += (new Date()).getTime() - t; + } + let averageParseTime = parseTime / runCount; + + callbacks.testFinish(benchmark, test, input.length, averageParseTime); + + state.benchmarkInputSize += input.length; + state.benchmarkParseTime += averageParseTime; + }; + } + + function benchmarkFinalizer(benchmark) { + return function() { + callbacks.benchmarkFinish( + benchmark, + state.benchmarkInputSize, + state.benchmarkParseTime + ); + + state.totalInputSize += state.benchmarkInputSize; + state.totalParseTime += state.benchmarkParseTime; + }; + } + + function finalize() { + callbacks.finish(state.totalInputSize, state.totalParseTime); + } + + // Main + + Q.add(initialize); + benchmarks.forEach(benchmark => { + Q.add(benchmarkInitializer(benchmark)); + benchmark.tests.forEach(test => { + Q.add(testRunner(benchmark, test)); + }); + Q.add(benchmarkFinalizer(benchmark)); + }); + Q.add(finalize); + + Q.run(); + } }; module.exports = Runner; diff --git a/gulpfile.js b/gulpfile.js index 4c0ef1b..e55c7f1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,16 +6,16 @@ let rename = require("gulp-rename"); let transform = require("gulp-transform"); function generate(contents) { - return peg.generate(contents.toString(), { - output: "source", - format: "commonjs" - }); + return peg.generate(contents.toString(), { + output: "source", + format: "commonjs" + }); } // Generate the grammar parser. gulp.task("parser", () => - gulp.src("src/parser.pegjs") - .pipe(transform("utf8", generate)) - .pipe(rename({ extname: ".js" })) - .pipe(gulp.dest("lib")) + gulp.src("src/parser.pegjs") + .pipe(transform("utf8", generate)) + .pipe(rename({ extname: ".js" })) + .pipe(gulp.dest("lib")) ); diff --git a/lib/compiler/asts.js b/lib/compiler/asts.js index e7f383a..49f06ed 100644 --- a/lib/compiler/asts.js +++ b/lib/compiler/asts.js @@ -4,73 +4,73 @@ let visitor = require("./visitor"); // AST utilities. let asts = { - findRule(ast, name) { - for (let i = 0; i < ast.rules.length; i++) { - if (ast.rules[i].name === name) { - return ast.rules[i]; - } - } - - return undefined; - }, - - indexOfRule(ast, name) { - for (let i = 0; i < ast.rules.length; i++) { - if (ast.rules[i].name === name) { - return i; - } - } - - return -1; - }, - - alwaysConsumesOnSuccess(ast, node) { - function consumesTrue() { return true; } - function consumesFalse() { return false; } - - function consumesExpression(node) { - return consumes(node.expression); - } - - let consumes = visitor.build({ - rule: consumesExpression, - named: consumesExpression, - - choice(node) { - return node.alternatives.every(consumes); - }, - - action: consumesExpression, - - sequence(node) { - return node.elements.some(consumes); - }, - - labeled: consumesExpression, - text: consumesExpression, - simple_and: consumesFalse, - simple_not: consumesFalse, - optional: consumesFalse, - zero_or_more: consumesFalse, - one_or_more: consumesExpression, - group: consumesExpression, - semantic_and: consumesFalse, - semantic_not: consumesFalse, - - rule_ref(node) { - return consumes(asts.findRule(ast, node.name)); - }, - - literal(node) { - return node.value !== ""; - }, - - class: consumesTrue, - any: consumesTrue - }); - - return consumes(node); - } + findRule(ast, name) { + for (let i = 0; i < ast.rules.length; i++) { + if (ast.rules[i].name === name) { + return ast.rules[i]; + } + } + + return undefined; + }, + + indexOfRule(ast, name) { + for (let i = 0; i < ast.rules.length; i++) { + if (ast.rules[i].name === name) { + return i; + } + } + + return -1; + }, + + alwaysConsumesOnSuccess(ast, node) { + function consumesTrue() { return true; } + function consumesFalse() { return false; } + + function consumesExpression(node) { + return consumes(node.expression); + } + + let consumes = visitor.build({ + rule: consumesExpression, + named: consumesExpression, + + choice(node) { + return node.alternatives.every(consumes); + }, + + action: consumesExpression, + + sequence(node) { + return node.elements.some(consumes); + }, + + labeled: consumesExpression, + text: consumesExpression, + simple_and: consumesFalse, + simple_not: consumesFalse, + optional: consumesFalse, + zero_or_more: consumesFalse, + one_or_more: consumesExpression, + group: consumesExpression, + semantic_and: consumesFalse, + semantic_not: consumesFalse, + + rule_ref(node) { + return consumes(asts.findRule(ast, node.name)); + }, + + literal(node) { + return node.value !== ""; + }, + + class: consumesTrue, + any: consumesTrue + }); + + return consumes(node); + } }; module.exports = asts; diff --git a/lib/compiler/index.js b/lib/compiler/index.js index 7722ca1..44ebd29 100644 --- a/lib/compiler/index.js +++ b/lib/compiler/index.js @@ -11,81 +11,81 @@ let reportUndefinedRules = require("./passes/report-undefined-rules"); let visitor = require("./visitor"); function processOptions(options, defaults) { - let processedOptions = {}; + let processedOptions = {}; - Object.keys(options).forEach(name => { - processedOptions[name] = options[name]; - }); + Object.keys(options).forEach(name => { + processedOptions[name] = options[name]; + }); - Object.keys(defaults).forEach(name => { - if (!Object.prototype.hasOwnProperty.call(processedOptions, name)) { - processedOptions[name] = defaults[name]; - } - }); + Object.keys(defaults).forEach(name => { + if (!Object.prototype.hasOwnProperty.call(processedOptions, name)) { + processedOptions[name] = defaults[name]; + } + }); - return processedOptions; + return processedOptions; } let compiler = { - // AST node visitor builder. Useful mainly for plugins which manipulate the - // AST. - visitor: visitor, + // AST node visitor builder. Useful mainly for plugins which manipulate the + // AST. + visitor: visitor, - // Compiler passes. - // - // Each pass is a function that is passed the AST. It can perform checks on it - // or modify it as needed. If the pass encounters a semantic error, it throws - // |peg.GrammarError|. - passes: { - check: { - reportUndefinedRules: reportUndefinedRules, - reportDuplicateRules: reportDuplicateRules, - reportDuplicateLabels: reportDuplicateLabels, - reportInfiniteRecursion: reportInfiniteRecursion, - reportInfiniteRepetition: reportInfiniteRepetition - }, - transform: { - removeProxyRules: removeProxyRules - }, - generate: { - generateBytecode: generateBytecode, - generateJS: generateJS - } - }, + // Compiler passes. + // + // Each pass is a function that is passed the AST. It can perform checks on it + // or modify it as needed. If the pass encounters a semantic error, it throws + // |peg.GrammarError|. + passes: { + check: { + reportUndefinedRules: reportUndefinedRules, + reportDuplicateRules: reportDuplicateRules, + reportDuplicateLabels: reportDuplicateLabels, + reportInfiniteRecursion: reportInfiniteRecursion, + reportInfiniteRepetition: reportInfiniteRepetition + }, + transform: { + removeProxyRules: removeProxyRules + }, + generate: { + generateBytecode: generateBytecode, + generateJS: generateJS + } + }, - // Generates a parser from a specified grammar AST. Throws |peg.GrammarError| - // if the AST contains a semantic error. Note that not all errors are detected - // during the generation and some may protrude to the generated parser and - // cause its malfunction. - compile(ast, passes, options) { - options = options !== undefined ? options : {}; + // Generates a parser from a specified grammar AST. Throws |peg.GrammarError| + // if the AST contains a semantic error. Note that not all errors are detected + // during the generation and some may protrude to the generated parser and + // cause its malfunction. + compile(ast, passes, options) { + options = options !== undefined ? options : {}; - options = processOptions(options, { - allowedStartRules: [ast.rules[0].name], - cache: false, - dependencies: {}, - exportVar: null, - format: "bare", - optimize: "speed", - output: "parser", - trace: false - }); + options = processOptions(options, { + allowedStartRules: [ast.rules[0].name], + cache: false, + dependencies: {}, + exportVar: null, + format: "bare", + optimize: "speed", + output: "parser", + trace: false + }); - Object.keys(passes).forEach(stage => { - passes[stage].forEach(p => { p(ast, options); }); - }); + Object.keys(passes).forEach(stage => { + passes[stage].forEach(p => { p(ast, options); }); + }); - switch (options.output) { - case "parser": - return eval(ast.code); + switch (options.output) { + case "parser": + return eval(ast.code); - case "source": - return ast.code; + case "source": + return ast.code; - default: - throw new Error("Invalid output format: " + options.output + "."); - } - } + default: + throw new Error("Invalid output format: " + options.output + "."); + } + } }; module.exports = compiler; diff --git a/lib/compiler/js.js b/lib/compiler/js.js index 09cc713..8c70c71 100644 --- a/lib/compiler/js.js +++ b/lib/compiler/js.js @@ -4,51 +4,51 @@ function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } // JavaScript code generation helpers. let js = { - stringEscape(s) { - // ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a string - // literal except for the closing quote character, backslash, carriage - // return, line separator, paragraph separator, and line feed. Any character - // may appear in the form of an escape sequence. - // - // For portability, we also escape all control and non-ASCII characters. - return s - .replace(/\\/g, "\\\\") // backslash - .replace(/"/g, "\\\"") // closing double quote - .replace(/\0/g, "\\0") // null - .replace(/\x08/g, "\\b") // backspace - .replace(/\t/g, "\\t") // horizontal tab - .replace(/\n/g, "\\n") // line feed - .replace(/\v/g, "\\v") // vertical tab - .replace(/\f/g, "\\f") // form feed - .replace(/\r/g, "\\r") // carriage return - .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch)) - .replace(/[\x10-\x1F\x7F-\xFF]/g, ch => "\\x" + hex(ch)) - .replace(/[\u0100-\u0FFF]/g, ch => "\\u0" + hex(ch)) - .replace(/[\u1000-\uFFFF]/g, ch => "\\u" + hex(ch)); - }, + stringEscape(s) { + // ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a string + // literal except for the closing quote character, backslash, carriage + // return, line separator, paragraph separator, and line feed. Any character + // may appear in the form of an escape sequence. + // + // For portability, we also escape all control and non-ASCII characters. + return s + .replace(/\\/g, "\\\\") // backslash + .replace(/"/g, "\\\"") // closing double quote + .replace(/\0/g, "\\0") // null + .replace(/\x08/g, "\\b") // backspace + .replace(/\t/g, "\\t") // horizontal tab + .replace(/\n/g, "\\n") // line feed + .replace(/\v/g, "\\v") // vertical tab + .replace(/\f/g, "\\f") // form feed + .replace(/\r/g, "\\r") // carriage return + .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch)) + .replace(/[\x10-\x1F\x7F-\xFF]/g, ch => "\\x" + hex(ch)) + .replace(/[\u0100-\u0FFF]/g, ch => "\\u0" + hex(ch)) + .replace(/[\u1000-\uFFFF]/g, ch => "\\u" + hex(ch)); + }, - regexpClassEscape(s) { - // Based on ECMA-262, 5th ed., 7.8.5 & 15.10.1. - // - // For portability, we also escape all control and non-ASCII characters. - return s - .replace(/\\/g, "\\\\") // backslash - .replace(/\//g, "\\/") // closing slash - .replace(/]/g, "\\]") // closing bracket - .replace(/\^/g, "\\^") // caret - .replace(/-/g, "\\-") // dash - .replace(/\0/g, "\\0") // null - .replace(/\x08/g, "\\b") // backspace - .replace(/\t/g, "\\t") // horizontal tab - .replace(/\n/g, "\\n") // line feed - .replace(/\v/g, "\\v") // vertical tab - .replace(/\f/g, "\\f") // form feed - .replace(/\r/g, "\\r") // carriage return - .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch)) - .replace(/[\x10-\x1F\x7F-\xFF]/g, ch => "\\x" + hex(ch)) - .replace(/[\u0100-\u0FFF]/g, ch => "\\u0" + hex(ch)) - .replace(/[\u1000-\uFFFF]/g, ch => "\\u" + hex(ch)); - } + regexpClassEscape(s) { + // Based on ECMA-262, 5th ed., 7.8.5 & 15.10.1. + // + // For portability, we also escape all control and non-ASCII characters. + return s + .replace(/\\/g, "\\\\") // backslash + .replace(/\//g, "\\/") // closing slash + .replace(/]/g, "\\]") // closing bracket + .replace(/\^/g, "\\^") // caret + .replace(/-/g, "\\-") // dash + .replace(/\0/g, "\\0") // null + .replace(/\x08/g, "\\b") // backspace + .replace(/\t/g, "\\t") // horizontal tab + .replace(/\n/g, "\\n") // line feed + .replace(/\v/g, "\\v") // vertical tab + .replace(/\f/g, "\\f") // form feed + .replace(/\r/g, "\\r") // carriage return + .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch)) + .replace(/[\x10-\x1F\x7F-\xFF]/g, ch => "\\x" + hex(ch)) + .replace(/[\u0100-\u0FFF]/g, ch => "\\u0" + hex(ch)) + .replace(/[\u1000-\uFFFF]/g, ch => "\\u" + hex(ch)); + } }; module.exports = js; diff --git a/lib/compiler/opcodes.js b/lib/compiler/opcodes.js index 58e09b5..3620e7c 100644 --- a/lib/compiler/opcodes.js +++ b/lib/compiler/opcodes.js @@ -2,53 +2,53 @@ // Bytecode instruction opcodes. let opcodes = { - // Stack Manipulation + // Stack Manipulation - PUSH: 0, // PUSH c - PUSH_UNDEFINED: 1, // PUSH_UNDEFINED - PUSH_NULL: 2, // PUSH_NULL - PUSH_FAILED: 3, // PUSH_FAILED - PUSH_EMPTY_ARRAY: 4, // PUSH_EMPTY_ARRAY - PUSH_CURR_POS: 5, // PUSH_CURR_POS - POP: 6, // POP - POP_CURR_POS: 7, // POP_CURR_POS - POP_N: 8, // POP_N n - NIP: 9, // NIP - APPEND: 10, // APPEND - WRAP: 11, // WRAP n - TEXT: 12, // TEXT + PUSH: 0, // PUSH c + PUSH_UNDEFINED: 1, // PUSH_UNDEFINED + PUSH_NULL: 2, // PUSH_NULL + PUSH_FAILED: 3, // PUSH_FAILED + PUSH_EMPTY_ARRAY: 4, // PUSH_EMPTY_ARRAY + PUSH_CURR_POS: 5, // PUSH_CURR_POS + POP: 6, // POP + POP_CURR_POS: 7, // POP_CURR_POS + POP_N: 8, // POP_N n + NIP: 9, // NIP + APPEND: 10, // APPEND + WRAP: 11, // WRAP n + TEXT: 12, // TEXT - // Conditions and Loops + // Conditions and Loops - IF: 13, // IF t, f - IF_ERROR: 14, // IF_ERROR t, f - IF_NOT_ERROR: 15, // IF_NOT_ERROR t, f - WHILE_NOT_ERROR: 16, // WHILE_NOT_ERROR b + IF: 13, // IF t, f + IF_ERROR: 14, // IF_ERROR t, f + IF_NOT_ERROR: 15, // IF_NOT_ERROR t, f + WHILE_NOT_ERROR: 16, // WHILE_NOT_ERROR b - // Matching + // Matching - MATCH_ANY: 17, // MATCH_ANY a, f, ... - MATCH_STRING: 18, // MATCH_STRING s, a, f, ... - MATCH_STRING_IC: 19, // MATCH_STRING_IC s, a, f, ... - MATCH_REGEXP: 20, // MATCH_REGEXP r, a, f, ... - ACCEPT_N: 21, // ACCEPT_N n - ACCEPT_STRING: 22, // ACCEPT_STRING s - FAIL: 23, // FAIL e + MATCH_ANY: 17, // MATCH_ANY a, f, ... + MATCH_STRING: 18, // MATCH_STRING s, a, f, ... + MATCH_STRING_IC: 19, // MATCH_STRING_IC s, a, f, ... + MATCH_REGEXP: 20, // MATCH_REGEXP r, a, f, ... + ACCEPT_N: 21, // ACCEPT_N n + ACCEPT_STRING: 22, // ACCEPT_STRING s + FAIL: 23, // FAIL e - // Calls + // Calls - LOAD_SAVED_POS: 24, // LOAD_SAVED_POS p - UPDATE_SAVED_POS: 25, // UPDATE_SAVED_POS - CALL: 26, // CALL f, n, pc, p1, p2, ..., pN + LOAD_SAVED_POS: 24, // LOAD_SAVED_POS p + UPDATE_SAVED_POS: 25, // UPDATE_SAVED_POS + CALL: 26, // CALL f, n, pc, p1, p2, ..., pN - // Rules + // Rules - RULE: 27, // RULE r + RULE: 27, // RULE r - // Failure Reporting + // Failure Reporting - SILENT_FAILS_ON: 28, // SILENT_FAILS_ON - SILENT_FAILS_OFF: 29 // SILENT_FAILS_OFF + SILENT_FAILS_ON: 28, // SILENT_FAILS_ON + SILENT_FAILS_OFF: 29 // SILENT_FAILS_OFF }; module.exports = opcodes; diff --git a/lib/compiler/passes/generate-bytecode.js b/lib/compiler/passes/generate-bytecode.js index d944f28..61664f6 100644 --- a/lib/compiler/passes/generate-bytecode.js +++ b/lib/compiler/passes/generate-bytecode.js @@ -188,431 +188,431 @@ let visitor = require("../visitor"); // // silentFails--; function generateBytecode(ast) { - let consts = []; - - function addConst(value) { - let index = consts.indexOf(value); - - return index === -1 ? consts.push(value) - 1 : index; - } - - function addFunctionConst(params, code) { - return addConst( - "function(" + params.join(", ") + ") {" + code + "}" - ); - } - - function cloneEnv(env) { - let clone = {}; - - Object.keys(env).forEach(name => { - clone[name] = env[name]; - }); - - return clone; - } - - function buildSequence() { - return Array.prototype.concat.apply([], arguments); - } - - function buildCondition(condCode, thenCode, 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) { - let params = Object.keys(env).map(name => sp - env[name]); - - return [op.CALL, functionIndex, delta, params.length].concat(params); - } - - function buildSimplePredicate(expression, negative, context) { - return buildSequence( - [op.PUSH_CURR_POS], - [op.SILENT_FAILS_ON], - generate(expression, { - sp: context.sp + 1, - env: cloneEnv(context.env), - action: null - }), - [op.SILENT_FAILS_OFF], - buildCondition( - [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(code, negative, context) { - let functionIndex = addFunctionConst(Object.keys(context.env), code); - - return buildSequence( - [op.UPDATE_SAVED_POS], - buildCall(functionIndex, 0, context.env, context.sp), - buildCondition( - [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) - ); - } - - let generate = visitor.build({ - grammar(node) { - node.rules.forEach(generate); - - node.consts = consts; - }, - - rule(node) { - node.bytecode = generate(node.expression, { - sp: -1, // stack pointer - env: { }, // mapping of label names to stack positions - action: null // action nodes pass themselves to children here - }); - }, - - named(node, context) { - let nameIndex = addConst( - "peg$otherExpectation(\"" + js.stringEscape(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([op.IF_ERROR], [op.FAIL, nameIndex], []) - ); - }, - - choice(node, context) { - function buildAlternativesCode(alternatives, context) { - return buildSequence( - generate(alternatives[0], { - sp: context.sp, - env: cloneEnv(context.env), - action: null - }), - alternatives.length > 1 - ? buildCondition( - [op.IF_ERROR], - buildSequence( - [op.POP], - buildAlternativesCode(alternatives.slice(1), context) - ), - [] - ) - : [] - ); - } - - return buildAlternativesCode(node.alternatives, context); - }, - - action(node, context) { - let env = cloneEnv(context.env); - let emitCall = node.expression.type !== "sequence" + let consts = []; + + function addConst(value) { + let index = consts.indexOf(value); + + return index === -1 ? consts.push(value) - 1 : index; + } + + function addFunctionConst(params, code) { + return addConst( + "function(" + params.join(", ") + ") {" + code + "}" + ); + } + + function cloneEnv(env) { + let clone = {}; + + Object.keys(env).forEach(name => { + clone[name] = env[name]; + }); + + return clone; + } + + function buildSequence() { + return Array.prototype.concat.apply([], arguments); + } + + function buildCondition(condCode, thenCode, 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) { + let params = Object.keys(env).map(name => sp - env[name]); + + return [op.CALL, functionIndex, delta, params.length].concat(params); + } + + function buildSimplePredicate(expression, negative, context) { + return buildSequence( + [op.PUSH_CURR_POS], + [op.SILENT_FAILS_ON], + generate(expression, { + sp: context.sp + 1, + env: cloneEnv(context.env), + action: null + }), + [op.SILENT_FAILS_OFF], + buildCondition( + [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(code, negative, context) { + let functionIndex = addFunctionConst(Object.keys(context.env), code); + + return buildSequence( + [op.UPDATE_SAVED_POS], + buildCall(functionIndex, 0, context.env, context.sp), + buildCondition( + [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) + ); + } + + let generate = visitor.build({ + grammar(node) { + node.rules.forEach(generate); + + node.consts = consts; + }, + + rule(node) { + node.bytecode = generate(node.expression, { + sp: -1, // stack pointer + env: { }, // mapping of label names to stack positions + action: null // action nodes pass themselves to children here + }); + }, + + named(node, context) { + let nameIndex = addConst( + "peg$otherExpectation(\"" + js.stringEscape(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([op.IF_ERROR], [op.FAIL, nameIndex], []) + ); + }, + + choice(node, context) { + function buildAlternativesCode(alternatives, context) { + return buildSequence( + generate(alternatives[0], { + sp: context.sp, + env: cloneEnv(context.env), + action: null + }), + alternatives.length > 1 + ? buildCondition( + [op.IF_ERROR], + buildSequence( + [op.POP], + buildAlternativesCode(alternatives.slice(1), context) + ), + [] + ) + : [] + ); + } + + return buildAlternativesCode(node.alternatives, context); + }, + + action(node, context) { + let env = cloneEnv(context.env); + let emitCall = node.expression.type !== "sequence" || node.expression.elements.length === 0; - let expressionCode = generate(node.expression, { - sp: context.sp + (emitCall ? 1 : 0), - env: env, - action: node - }); - let functionIndex = addFunctionConst(Object.keys(env), node.code); - - return emitCall - ? buildSequence( - [op.PUSH_CURR_POS], - expressionCode, - buildCondition( - [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) { - let processedCount = node.elements.length - elements.slice(1).length; - - return buildSequence( - generate(elements[0], { - sp: context.sp, - env: context.env, - action: null - }), - buildCondition( - [op.IF_NOT_ERROR], - buildElementsCode(elements.slice(1), { - sp: context.sp + 1, - env: context.env, - action: context.action - }), - buildSequence( - processedCount > 1 ? [op.POP_N, processedCount] : [op.POP], - [op.POP_CURR_POS], - [op.PUSH_FAILED] - ) - ) - ); - } else { - if (context.action) { - let functionIndex = addFunctionConst( - Object.keys(context.env), - context.action.code - ); - - return buildSequence( - [op.LOAD_SAVED_POS, node.elements.length], - buildCall( - functionIndex, - node.elements.length, - context.env, - context.sp - ), - [op.NIP] - ); - } else { - return buildSequence([op.WRAP, node.elements.length], [op.NIP]); - } - } - } - - return buildSequence( - [op.PUSH_CURR_POS], - buildElementsCode(node.elements, { - sp: context.sp + 1, - env: context.env, - action: context.action - }) - ); - }, - - labeled(node, context) { - let env = cloneEnv(context.env); - - context.env[node.label] = context.sp + 1; - - return generate(node.expression, { - sp: context.sp, - env: 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( - [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( - [op.IF_ERROR], - buildSequence([op.POP], [op.PUSH_NULL]), - [] - ) - ); - }, - - zero_or_more(node, context) { - let 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) { - let expressionCode = generate(node.expression, { - sp: context.sp + 1, - env: cloneEnv(context.env), - action: null - }); - - return buildSequence( - [op.PUSH_EMPTY_ARRAY], - expressionCode, - buildCondition( - [op.IF_NOT_ERROR], - buildSequence(buildAppendLoop(expressionCode), [op.POP]), - buildSequence([op.POP], [op.POP], [op.PUSH_FAILED]) - ) - ); - }, - - group(node, context) { - return generate(node.expression, { - sp: context.sp, - env: cloneEnv(context.env), - action: null - }); - }, - - semantic_and(node, context) { - return buildSemanticPredicate(node.code, false, context); - }, - - semantic_not(node, context) { - return buildSemanticPredicate(node.code, true, context); - }, - - rule_ref(node) { - return [op.RULE, asts.indexOfRule(ast, node.name)]; - }, - - literal(node) { - if (node.value.length > 0) { - let stringIndex = addConst("\"" + let expressionCode = generate(node.expression, { + sp: context.sp + (emitCall ? 1 : 0), + env: env, + action: node + }); + let functionIndex = addFunctionConst(Object.keys(env), node.code); + + return emitCall + ? buildSequence( + [op.PUSH_CURR_POS], + expressionCode, + buildCondition( + [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) { + let processedCount = node.elements.length - elements.slice(1).length; + + return buildSequence( + generate(elements[0], { + sp: context.sp, + env: context.env, + action: null + }), + buildCondition( + [op.IF_NOT_ERROR], + buildElementsCode(elements.slice(1), { + sp: context.sp + 1, + env: context.env, + action: context.action + }), + buildSequence( + processedCount > 1 ? [op.POP_N, processedCount] : [op.POP], + [op.POP_CURR_POS], + [op.PUSH_FAILED] + ) + ) + ); + } else { + if (context.action) { + let functionIndex = addFunctionConst( + Object.keys(context.env), + context.action.code + ); + + return buildSequence( + [op.LOAD_SAVED_POS, node.elements.length], + buildCall( + functionIndex, + node.elements.length, + context.env, + context.sp + ), + [op.NIP] + ); + } else { + return buildSequence([op.WRAP, node.elements.length], [op.NIP]); + } + } + } + + return buildSequence( + [op.PUSH_CURR_POS], + buildElementsCode(node.elements, { + sp: context.sp + 1, + env: context.env, + action: context.action + }) + ); + }, + + labeled(node, context) { + let env = cloneEnv(context.env); + + context.env[node.label] = context.sp + 1; + + return generate(node.expression, { + sp: context.sp, + env: 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( + [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( + [op.IF_ERROR], + buildSequence([op.POP], [op.PUSH_NULL]), + [] + ) + ); + }, + + zero_or_more(node, context) { + let 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) { + let expressionCode = generate(node.expression, { + sp: context.sp + 1, + env: cloneEnv(context.env), + action: null + }); + + return buildSequence( + [op.PUSH_EMPTY_ARRAY], + expressionCode, + buildCondition( + [op.IF_NOT_ERROR], + buildSequence(buildAppendLoop(expressionCode), [op.POP]), + buildSequence([op.POP], [op.POP], [op.PUSH_FAILED]) + ) + ); + }, + + group(node, context) { + return generate(node.expression, { + sp: context.sp, + env: cloneEnv(context.env), + action: null + }); + }, + + semantic_and(node, context) { + return buildSemanticPredicate(node.code, false, context); + }, + + semantic_not(node, context) { + return buildSemanticPredicate(node.code, true, context); + }, + + rule_ref(node) { + return [op.RULE, asts.indexOfRule(ast, node.name)]; + }, + + literal(node) { + if (node.value.length > 0) { + let stringIndex = addConst("\"" + js.stringEscape( - node.ignoreCase ? node.value.toLowerCase() : node.value - ) + node.ignoreCase ? node.value.toLowerCase() : node.value + ) + "\"" - ); - let expectedIndex = addConst( - "peg$literalExpectation(" + ); + let expectedIndex = addConst( + "peg$literalExpectation(" + "\"" + js.stringEscape(node.value) + "\", " + node.ignoreCase + ")" - ); - - // 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( - node.ignoreCase - ? [op.MATCH_STRING_IC, stringIndex] - : [op.MATCH_STRING, stringIndex], - node.ignoreCase - ? [op.ACCEPT_N, node.value.length] - : [op.ACCEPT_STRING, stringIndex], - [op.FAIL, expectedIndex] - ); - } else { - let stringIndex = addConst("\"\""); - - return [op.PUSH, stringIndex]; - } - }, - - class(node) { - let regexp = "/^[" + ); + + // 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( + node.ignoreCase + ? [op.MATCH_STRING_IC, stringIndex] + : [op.MATCH_STRING, stringIndex], + node.ignoreCase + ? [op.ACCEPT_N, node.value.length] + : [op.ACCEPT_STRING, stringIndex], + [op.FAIL, expectedIndex] + ); + } else { + let stringIndex = addConst("\"\""); + + return [op.PUSH, stringIndex]; + } + }, + + class(node) { + let regexp = "/^[" + (node.inverted ? "^" : "") + node.parts.map(part => - Array.isArray(part) - ? js.regexpClassEscape(part[0]) + Array.isArray(part) + ? js.regexpClassEscape(part[0]) + "-" + js.regexpClassEscape(part[1]) - : js.regexpClassEscape(part) - ).join("") + : js.regexpClassEscape(part) + ).join("") + "]/" + (node.ignoreCase ? "i" : ""); - let parts = "[" + let parts = "[" + node.parts.map(part => - Array.isArray(part) - ? "[\"" + js.stringEscape(part[0]) + "\", \"" + js.stringEscape(part[1]) + "\"]" - : "\"" + js.stringEscape(part) + "\"" - ).join(", ") + Array.isArray(part) + ? "[\"" + js.stringEscape(part[0]) + "\", \"" + js.stringEscape(part[1]) + "\"]" + : "\"" + js.stringEscape(part) + "\"" + ).join(", ") + "]"; - let regexpIndex = addConst(regexp); - let expectedIndex = addConst( - "peg$classExpectation(" + let regexpIndex = addConst(regexp); + let expectedIndex = addConst( + "peg$classExpectation(" + parts + ", " + node.inverted + ", " + node.ignoreCase + ")" - ); - - return buildCondition( - [op.MATCH_REGEXP, regexpIndex], - [op.ACCEPT_N, 1], - [op.FAIL, expectedIndex] - ); - }, - - any() { - let expectedIndex = addConst("peg$anyExpectation()"); - - return buildCondition( - [op.MATCH_ANY], - [op.ACCEPT_N, 1], - [op.FAIL, expectedIndex] - ); - } - }); - - generate(ast); + ); + + return buildCondition( + [op.MATCH_REGEXP, regexpIndex], + [op.ACCEPT_N, 1], + [op.FAIL, expectedIndex] + ); + }, + + any() { + let expectedIndex = addConst("peg$anyExpectation()"); + + return buildCondition( + [op.MATCH_ANY], + [op.ACCEPT_N, 1], + [op.FAIL, expectedIndex] + ); + } + }); + + generate(ast); } module.exports = generateBytecode; diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index 4dd0295..12c1da8 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -6,1360 +6,1360 @@ let op = require("../opcodes"); // Generates parser JavaScript code. function generateJS(ast, options) { - // These only indent non-empty lines to avoid trailing whitespace. - function indent2(code) { return code.replace(/^(.+)$/gm, " $1"); } - function indent10(code) { return code.replace(/^(.+)$/gm, " $1"); } - - function generateTables() { - if (options.optimize === "size") { - return [ - "var peg$consts = [", - indent2(ast.consts.join(",\n")), - "];", - "", - "var peg$bytecode = [", - indent2(ast.rules.map(rule => - "peg$decode(\"" + // These only indent non-empty lines to avoid trailing whitespace. + function indent2(code) { return code.replace(/^(.+)$/gm, " $1"); } + function indent10(code) { return code.replace(/^(.+)$/gm, " $1"); } + + function generateTables() { + if (options.optimize === "size") { + return [ + "var peg$consts = [", + indent2(ast.consts.join(",\n")), + "];", + "", + "var peg$bytecode = [", + indent2(ast.rules.map(rule => + "peg$decode(\"" + js.stringEscape(rule.bytecode.map( - b => String.fromCharCode(b + 32) - ).join("")) + b => String.fromCharCode(b + 32) + ).join("")) + "\")" - ).join(",\n")), - "];" - ].join("\n"); - } else { - return ast.consts.map((c, i) => "var peg$c" + i + " = " + c + ";").join("\n"); - } - } - - function generateRuleHeader(ruleNameCode, ruleIndexCode) { - let parts = []; - - parts.push(""); - - if (options.trace) { - parts.push([ - "peg$tracer.trace({", - " type: \"rule.enter\",", - " rule: " + ruleNameCode + ",", - " location: peg$computeLocation(startPos, startPos)", - "});", - "" - ].join("\n")); - } - - if (options.cache) { - parts.push([ - "var key = peg$currPos * " + ast.rules.length + " + " + ruleIndexCode + ";", - "var cached = peg$resultsCache[key];", - "", - "if (cached) {", - " peg$currPos = cached.nextPos;", - "" - ].join("\n")); - - if (options.trace) { - parts.push([ - "if (cached.result !== peg$FAILED) {", - " peg$tracer.trace({", - " type: \"rule.match\",", - " rule: " + ruleNameCode + ",", - " result: cached.result,", - " location: peg$computeLocation(startPos, peg$currPos)", - " });", - "} else {", - " peg$tracer.trace({", - " type: \"rule.fail\",", - " rule: " + ruleNameCode + ",", - " location: peg$computeLocation(startPos, startPos)", - " });", - "}", - "" - ].join("\n")); - } - - parts.push([ - " return cached.result;", - "}", - "" - ].join("\n")); - } - - return parts.join("\n"); - } - - function generateRuleFooter(ruleNameCode, resultCode) { - let parts = []; - - if (options.cache) { - parts.push([ - "", - "peg$resultsCache[key] = { nextPos: peg$currPos, result: " + resultCode + " };" - ].join("\n")); - } - - if (options.trace) { - parts.push([ - "", - "if (" + resultCode + " !== peg$FAILED) {", - " peg$tracer.trace({", - " type: \"rule.match\",", - " rule: " + ruleNameCode + ",", - " result: " + resultCode + ",", - " location: peg$computeLocation(startPos, peg$currPos)", - " });", - "} else {", - " peg$tracer.trace({", - " type: \"rule.fail\",", - " rule: " + ruleNameCode + ",", - " location: peg$computeLocation(startPos, startPos)", - " });", - "}" - ].join("\n")); - } - - parts.push([ - "", - "return " + resultCode + ";" - ].join("\n")); - - return parts.join("\n"); - } - - function generateInterpreter() { - let parts = []; - - function generateCondition(cond, argsLength) { - let baseLength = argsLength + 3; - let thenLengthCode = "bc[ip + " + (baseLength - 2) + "]"; - let elseLengthCode = "bc[ip + " + (baseLength - 1) + "]"; - - return [ - "ends.push(end);", - "ips.push(ip + " + baseLength + " + " + thenLengthCode + " + " + elseLengthCode + ");", - "", - "if (" + cond + ") {", - " end = ip + " + baseLength + " + " + thenLengthCode + ";", - " ip += " + baseLength + ";", - "} else {", - " end = ip + " + baseLength + " + " + thenLengthCode + " + " + elseLengthCode + ";", - " ip += " + baseLength + " + " + thenLengthCode + ";", - "}", - "", - "break;" - ].join("\n"); - } - - function generateLoop(cond) { - let baseLength = 2; - let bodyLengthCode = "bc[ip + " + (baseLength - 1) + "]"; - - return [ - "if (" + cond + ") {", - " ends.push(end);", - " ips.push(ip);", - "", - " end = ip + " + baseLength + " + " + bodyLengthCode + ";", - " ip += " + baseLength + ";", - "} else {", - " ip += " + baseLength + " + " + bodyLengthCode + ";", - "}", - "", - "break;" - ].join("\n"); - } - - function generateCall() { - let baseLength = 4; - let paramsLengthCode = "bc[ip + " + (baseLength - 1) + "]"; - - return [ - "params = bc.slice(ip + " + baseLength + ", ip + " + baseLength + " + " + paramsLengthCode + ")", - " .map(function(p) { return stack[stack.length - 1 - p]; });", - "", - "stack.splice(", - " stack.length - bc[ip + 2],", - " bc[ip + 2],", - " peg$consts[bc[ip + 1]].apply(null, params)", - ");", - "", - "ip += " + baseLength + " + " + paramsLengthCode + ";", - "break;" - ].join("\n"); - } - - parts.push([ - "function peg$decode(s) {", - " return s.split(\"\").map(function(ch) { return ch.charCodeAt(0) - 32; });", - "}", - "", - "function peg$parseRule(index) {" - ].join("\n")); - - if (options.trace) { - parts.push([ - " var bc = peg$bytecode[index];", - " var ip = 0;", - " var ips = [];", - " var end = bc.length;", - " var ends = [];", - " var stack = [];", - " var startPos = peg$currPos;", - " var params;" - ].join("\n")); - } else { - parts.push([ - " var bc = peg$bytecode[index];", - " var ip = 0;", - " var ips = [];", - " var end = bc.length;", - " var ends = [];", - " var stack = [];", - " var params;" - ].join("\n")); - } - - parts.push(indent2(generateRuleHeader("peg$ruleNames[index]", "index"))); - - parts.push([ - // The point of the outer loop and the |ips| & |ends| stacks is to avoid - // recursive calls for interpreting parts of bytecode. In other words, we - // implement the |interpret| operation of the abstract machine without - // function calls. Such calls would likely slow the parser down and more - // importantly cause stack overflows for complex grammars. - " while (true) {", - " while (ip < end) {", - " switch (bc[ip]) {", - " case " + op.PUSH + ":", // PUSH c - " stack.push(peg$consts[bc[ip + 1]]);", - " ip += 2;", - " break;", - "", - " case " + op.PUSH_UNDEFINED + ":", // PUSH_UNDEFINED - " stack.push(undefined);", - " ip++;", - " break;", - "", - " case " + op.PUSH_NULL + ":", // PUSH_NULL - " stack.push(null);", - " ip++;", - " break;", - "", - " case " + op.PUSH_FAILED + ":", // PUSH_FAILED - " stack.push(peg$FAILED);", - " ip++;", - " break;", - "", - " case " + op.PUSH_EMPTY_ARRAY + ":", // PUSH_EMPTY_ARRAY - " stack.push([]);", - " ip++;", - " break;", - "", - " case " + op.PUSH_CURR_POS + ":", // PUSH_CURR_POS - " stack.push(peg$currPos);", - " ip++;", - " break;", - "", - " case " + op.POP + ":", // POP - " stack.pop();", - " ip++;", - " break;", - "", - " case " + op.POP_CURR_POS + ":", // POP_CURR_POS - " peg$currPos = stack.pop();", - " ip++;", - " break;", - "", - " case " + op.POP_N + ":", // POP_N n - " stack.length -= bc[ip + 1];", - " ip += 2;", - " break;", - "", - " case " + op.NIP + ":", // NIP - " stack.splice(-2, 1);", - " ip++;", - " break;", - "", - " case " + op.APPEND + ":", // APPEND - " stack[stack.length - 2].push(stack.pop());", - " ip++;", - " break;", - "", - " case " + op.WRAP + ":", // WRAP n - " stack.push(stack.splice(stack.length - bc[ip + 1], bc[ip + 1]));", - " ip += 2;", - " break;", - "", - " case " + op.TEXT + ":", // TEXT - " stack.push(input.substring(stack.pop(), peg$currPos));", - " ip++;", - " break;", - "", - " case " + op.IF + ":", // IF t, f - indent10(generateCondition("stack[stack.length - 1]", 0)), - "", - " case " + op.IF_ERROR + ":", // IF_ERROR t, f - indent10(generateCondition( - "stack[stack.length - 1] === peg$FAILED", - 0 - )), - "", - " case " + op.IF_NOT_ERROR + ":", // IF_NOT_ERROR t, f - indent10( - generateCondition("stack[stack.length - 1] !== peg$FAILED", - 0 - )), - "", - " case " + op.WHILE_NOT_ERROR + ":", // WHILE_NOT_ERROR b - indent10(generateLoop("stack[stack.length - 1] !== peg$FAILED")), - "", - " case " + op.MATCH_ANY + ":", // MATCH_ANY a, f, ... - indent10(generateCondition("input.length > peg$currPos", 0)), - "", - " case " + op.MATCH_STRING + ":", // MATCH_STRING s, a, f, ... - indent10(generateCondition( - "input.substr(peg$currPos, peg$consts[bc[ip + 1]].length) === peg$consts[bc[ip + 1]]", - 1 - )), - "", - " case " + op.MATCH_STRING_IC + ":", // MATCH_STRING_IC s, a, f, ... - indent10(generateCondition( - "input.substr(peg$currPos, peg$consts[bc[ip + 1]].length).toLowerCase() === peg$consts[bc[ip + 1]]", - 1 - )), - "", - " case " + op.MATCH_REGEXP + ":", // MATCH_REGEXP r, a, f, ... - indent10(generateCondition( - "peg$consts[bc[ip + 1]].test(input.charAt(peg$currPos))", - 1 - )), - "", - " case " + op.ACCEPT_N + ":", // ACCEPT_N n - " stack.push(input.substr(peg$currPos, bc[ip + 1]));", - " peg$currPos += bc[ip + 1];", - " ip += 2;", - " break;", - "", - " case " + op.ACCEPT_STRING + ":", // ACCEPT_STRING s - " stack.push(peg$consts[bc[ip + 1]]);", - " peg$currPos += peg$consts[bc[ip + 1]].length;", - " ip += 2;", - " break;", - "", - " case " + op.FAIL + ":", // FAIL e - " stack.push(peg$FAILED);", - " if (peg$silentFails === 0) {", - " peg$fail(peg$consts[bc[ip + 1]]);", - " }", - " ip += 2;", - " break;", - "", - " case " + op.LOAD_SAVED_POS + ":", // LOAD_SAVED_POS p - " peg$savedPos = stack[stack.length - 1 - bc[ip + 1]];", - " ip += 2;", - " break;", - "", - " case " + op.UPDATE_SAVED_POS + ":", // UPDATE_SAVED_POS - " peg$savedPos = peg$currPos;", - " ip++;", - " break;", - "", - " case " + op.CALL + ":", // CALL f, n, pc, p1, p2, ..., pN - indent10(generateCall()), - "", - " case " + op.RULE + ":", // RULE r - " stack.push(peg$parseRule(bc[ip + 1]));", - " ip += 2;", - " break;", - "", - " case " + op.SILENT_FAILS_ON + ":", // SILENT_FAILS_ON - " peg$silentFails++;", - " ip++;", - " break;", - "", - " case " + op.SILENT_FAILS_OFF + ":", // SILENT_FAILS_OFF - " peg$silentFails--;", - " ip++;", - " break;", - "", - " default:", - " throw new Error(\"Invalid opcode: \" + bc[ip] + \".\");", - " }", - " }", - "", - " if (ends.length > 0) {", - " end = ends.pop();", - " ip = ips.pop();", - " } else {", - " break;", - " }", - " }" - ].join("\n")); - - parts.push(indent2(generateRuleFooter("peg$ruleNames[index]", "stack[0]"))); - parts.push("}"); - - return parts.join("\n"); - } - - function generateRuleFunction(rule) { - let parts = []; - let stackVars = []; - let code; - - function c(i) { return "peg$c" + i; } // |consts[i]| of the abstract machine - function s(i) { return "s" + i; } // |stack[i]| of the abstract machine - - let stack = { - sp: -1, - maxSp: -1, - - push(exprCode) { - let code = s(++this.sp) + " = " + exprCode + ";"; - - if (this.sp > this.maxSp) { this.maxSp = this.sp; } - - return code; - }, - - pop(n) { - if (n === undefined) { - return s(this.sp--); - } else { - let values = Array(n); - - for (let i = 0; i < n; i++) { - values[i] = s(this.sp - n + 1 + i); - } - - this.sp -= n; - - return values; - } - }, - - top() { - return s(this.sp); - }, - - index(i) { - return s(this.sp - i); - } - }; - - function compile(bc) { - let ip = 0; - let end = bc.length; - let parts = []; - let value; - - function compileCondition(cond, argCount) { - let baseLength = argCount + 3; - let thenLength = bc[ip + baseLength - 2]; - let elseLength = bc[ip + baseLength - 1]; - let baseSp = stack.sp; - let thenCode, elseCode, thenSp, elseSp; - - ip += baseLength; - thenCode = compile(bc.slice(ip, ip + thenLength)); - thenSp = stack.sp; - ip += thenLength; - - if (elseLength > 0) { - stack.sp = baseSp; - elseCode = compile(bc.slice(ip, ip + elseLength)); - elseSp = stack.sp; - ip += elseLength; - - if (thenSp !== elseSp) { - throw new Error( - "Branches of a condition must move the stack pointer in the same way." - ); - } - } - - parts.push("if (" + cond + ") {"); - parts.push(indent2(thenCode)); - if (elseLength > 0) { - parts.push("} else {"); - parts.push(indent2(elseCode)); - } - parts.push("}"); - } - - function compileLoop(cond) { - let baseLength = 2; - let bodyLength = bc[ip + baseLength - 1]; - let baseSp = stack.sp; - let bodyCode, bodySp; - - ip += baseLength; - bodyCode = compile(bc.slice(ip, ip + bodyLength)); - bodySp = stack.sp; - ip += bodyLength; - - if (bodySp !== baseSp) { - throw new Error("Body of a loop can't move the stack pointer."); - } - - parts.push("while (" + cond + ") {"); - parts.push(indent2(bodyCode)); - parts.push("}"); - } - - function compileCall() { - let baseLength = 4; - let paramsLength = bc[ip + baseLength - 1]; - - let value = c(bc[ip + 1]) + "(" + ).join(",\n")), + "];" + ].join("\n"); + } else { + return ast.consts.map((c, i) => "var peg$c" + i + " = " + c + ";").join("\n"); + } + } + + function generateRuleHeader(ruleNameCode, ruleIndexCode) { + let parts = []; + + parts.push(""); + + if (options.trace) { + parts.push([ + "peg$tracer.trace({", + " type: \"rule.enter\",", + " rule: " + ruleNameCode + ",", + " location: peg$computeLocation(startPos, startPos)", + "});", + "" + ].join("\n")); + } + + if (options.cache) { + parts.push([ + "var key = peg$currPos * " + ast.rules.length + " + " + ruleIndexCode + ";", + "var cached = peg$resultsCache[key];", + "", + "if (cached) {", + " peg$currPos = cached.nextPos;", + "" + ].join("\n")); + + if (options.trace) { + parts.push([ + "if (cached.result !== peg$FAILED) {", + " peg$tracer.trace({", + " type: \"rule.match\",", + " rule: " + ruleNameCode + ",", + " result: cached.result,", + " location: peg$computeLocation(startPos, peg$currPos)", + " });", + "} else {", + " peg$tracer.trace({", + " type: \"rule.fail\",", + " rule: " + ruleNameCode + ",", + " location: peg$computeLocation(startPos, startPos)", + " });", + "}", + "" + ].join("\n")); + } + + parts.push([ + " return cached.result;", + "}", + "" + ].join("\n")); + } + + return parts.join("\n"); + } + + function generateRuleFooter(ruleNameCode, resultCode) { + let parts = []; + + if (options.cache) { + parts.push([ + "", + "peg$resultsCache[key] = { nextPos: peg$currPos, result: " + resultCode + " };" + ].join("\n")); + } + + if (options.trace) { + parts.push([ + "", + "if (" + resultCode + " !== peg$FAILED) {", + " peg$tracer.trace({", + " type: \"rule.match\",", + " rule: " + ruleNameCode + ",", + " result: " + resultCode + ",", + " location: peg$computeLocation(startPos, peg$currPos)", + " });", + "} else {", + " peg$tracer.trace({", + " type: \"rule.fail\",", + " rule: " + ruleNameCode + ",", + " location: peg$computeLocation(startPos, startPos)", + " });", + "}" + ].join("\n")); + } + + parts.push([ + "", + "return " + resultCode + ";" + ].join("\n")); + + return parts.join("\n"); + } + + function generateInterpreter() { + let parts = []; + + function generateCondition(cond, argsLength) { + let baseLength = argsLength + 3; + let thenLengthCode = "bc[ip + " + (baseLength - 2) + "]"; + let elseLengthCode = "bc[ip + " + (baseLength - 1) + "]"; + + return [ + "ends.push(end);", + "ips.push(ip + " + baseLength + " + " + thenLengthCode + " + " + elseLengthCode + ");", + "", + "if (" + cond + ") {", + " end = ip + " + baseLength + " + " + thenLengthCode + ";", + " ip += " + baseLength + ";", + "} else {", + " end = ip + " + baseLength + " + " + thenLengthCode + " + " + elseLengthCode + ";", + " ip += " + baseLength + " + " + thenLengthCode + ";", + "}", + "", + "break;" + ].join("\n"); + } + + function generateLoop(cond) { + let baseLength = 2; + let bodyLengthCode = "bc[ip + " + (baseLength - 1) + "]"; + + return [ + "if (" + cond + ") {", + " ends.push(end);", + " ips.push(ip);", + "", + " end = ip + " + baseLength + " + " + bodyLengthCode + ";", + " ip += " + baseLength + ";", + "} else {", + " ip += " + baseLength + " + " + bodyLengthCode + ";", + "}", + "", + "break;" + ].join("\n"); + } + + function generateCall() { + let baseLength = 4; + let paramsLengthCode = "bc[ip + " + (baseLength - 1) + "]"; + + return [ + "params = bc.slice(ip + " + baseLength + ", ip + " + baseLength + " + " + paramsLengthCode + ")", + " .map(function(p) { return stack[stack.length - 1 - p]; });", + "", + "stack.splice(", + " stack.length - bc[ip + 2],", + " bc[ip + 2],", + " peg$consts[bc[ip + 1]].apply(null, params)", + ");", + "", + "ip += " + baseLength + " + " + paramsLengthCode + ";", + "break;" + ].join("\n"); + } + + parts.push([ + "function peg$decode(s) {", + " return s.split(\"\").map(function(ch) { return ch.charCodeAt(0) - 32; });", + "}", + "", + "function peg$parseRule(index) {" + ].join("\n")); + + if (options.trace) { + parts.push([ + " var bc = peg$bytecode[index];", + " var ip = 0;", + " var ips = [];", + " var end = bc.length;", + " var ends = [];", + " var stack = [];", + " var startPos = peg$currPos;", + " var params;" + ].join("\n")); + } else { + parts.push([ + " var bc = peg$bytecode[index];", + " var ip = 0;", + " var ips = [];", + " var end = bc.length;", + " var ends = [];", + " var stack = [];", + " var params;" + ].join("\n")); + } + + parts.push(indent2(generateRuleHeader("peg$ruleNames[index]", "index"))); + + parts.push([ + // The point of the outer loop and the |ips| & |ends| stacks is to avoid + // recursive calls for interpreting parts of bytecode. In other words, we + // implement the |interpret| operation of the abstract machine without + // function calls. Such calls would likely slow the parser down and more + // importantly cause stack overflows for complex grammars. + " while (true) {", + " while (ip < end) {", + " switch (bc[ip]) {", + " case " + op.PUSH + ":", // PUSH c + " stack.push(peg$consts[bc[ip + 1]]);", + " ip += 2;", + " break;", + "", + " case " + op.PUSH_UNDEFINED + ":", // PUSH_UNDEFINED + " stack.push(undefined);", + " ip++;", + " break;", + "", + " case " + op.PUSH_NULL + ":", // PUSH_NULL + " stack.push(null);", + " ip++;", + " break;", + "", + " case " + op.PUSH_FAILED + ":", // PUSH_FAILED + " stack.push(peg$FAILED);", + " ip++;", + " break;", + "", + " case " + op.PUSH_EMPTY_ARRAY + ":", // PUSH_EMPTY_ARRAY + " stack.push([]);", + " ip++;", + " break;", + "", + " case " + op.PUSH_CURR_POS + ":", // PUSH_CURR_POS + " stack.push(peg$currPos);", + " ip++;", + " break;", + "", + " case " + op.POP + ":", // POP + " stack.pop();", + " ip++;", + " break;", + "", + " case " + op.POP_CURR_POS + ":", // POP_CURR_POS + " peg$currPos = stack.pop();", + " ip++;", + " break;", + "", + " case " + op.POP_N + ":", // POP_N n + " stack.length -= bc[ip + 1];", + " ip += 2;", + " break;", + "", + " case " + op.NIP + ":", // NIP + " stack.splice(-2, 1);", + " ip++;", + " break;", + "", + " case " + op.APPEND + ":", // APPEND + " stack[stack.length - 2].push(stack.pop());", + " ip++;", + " break;", + "", + " case " + op.WRAP + ":", // WRAP n + " stack.push(stack.splice(stack.length - bc[ip + 1], bc[ip + 1]));", + " ip += 2;", + " break;", + "", + " case " + op.TEXT + ":", // TEXT + " stack.push(input.substring(stack.pop(), peg$currPos));", + " ip++;", + " break;", + "", + " case " + op.IF + ":", // IF t, f + indent10(generateCondition("stack[stack.length - 1]", 0)), + "", + " case " + op.IF_ERROR + ":", // IF_ERROR t, f + indent10(generateCondition( + "stack[stack.length - 1] === peg$FAILED", + 0 + )), + "", + " case " + op.IF_NOT_ERROR + ":", // IF_NOT_ERROR t, f + indent10( + generateCondition("stack[stack.length - 1] !== peg$FAILED", + 0 + )), + "", + " case " + op.WHILE_NOT_ERROR + ":", // WHILE_NOT_ERROR b + indent10(generateLoop("stack[stack.length - 1] !== peg$FAILED")), + "", + " case " + op.MATCH_ANY + ":", // MATCH_ANY a, f, ... + indent10(generateCondition("input.length > peg$currPos", 0)), + "", + " case " + op.MATCH_STRING + ":", // MATCH_STRING s, a, f, ... + indent10(generateCondition( + "input.substr(peg$currPos, peg$consts[bc[ip + 1]].length) === peg$consts[bc[ip + 1]]", + 1 + )), + "", + " case " + op.MATCH_STRING_IC + ":", // MATCH_STRING_IC s, a, f, ... + indent10(generateCondition( + "input.substr(peg$currPos, peg$consts[bc[ip + 1]].length).toLowerCase() === peg$consts[bc[ip + 1]]", + 1 + )), + "", + " case " + op.MATCH_REGEXP + ":", // MATCH_REGEXP r, a, f, ... + indent10(generateCondition( + "peg$consts[bc[ip + 1]].test(input.charAt(peg$currPos))", + 1 + )), + "", + " case " + op.ACCEPT_N + ":", // ACCEPT_N n + " stack.push(input.substr(peg$currPos, bc[ip + 1]));", + " peg$currPos += bc[ip + 1];", + " ip += 2;", + " break;", + "", + " case " + op.ACCEPT_STRING + ":", // ACCEPT_STRING s + " stack.push(peg$consts[bc[ip + 1]]);", + " peg$currPos += peg$consts[bc[ip + 1]].length;", + " ip += 2;", + " break;", + "", + " case " + op.FAIL + ":", // FAIL e + " stack.push(peg$FAILED);", + " if (peg$silentFails === 0) {", + " peg$fail(peg$consts[bc[ip + 1]]);", + " }", + " ip += 2;", + " break;", + "", + " case " + op.LOAD_SAVED_POS + ":", // LOAD_SAVED_POS p + " peg$savedPos = stack[stack.length - 1 - bc[ip + 1]];", + " ip += 2;", + " break;", + "", + " case " + op.UPDATE_SAVED_POS + ":", // UPDATE_SAVED_POS + " peg$savedPos = peg$currPos;", + " ip++;", + " break;", + "", + " case " + op.CALL + ":", // CALL f, n, pc, p1, p2, ..., pN + indent10(generateCall()), + "", + " case " + op.RULE + ":", // RULE r + " stack.push(peg$parseRule(bc[ip + 1]));", + " ip += 2;", + " break;", + "", + " case " + op.SILENT_FAILS_ON + ":", // SILENT_FAILS_ON + " peg$silentFails++;", + " ip++;", + " break;", + "", + " case " + op.SILENT_FAILS_OFF + ":", // SILENT_FAILS_OFF + " peg$silentFails--;", + " ip++;", + " break;", + "", + " default:", + " throw new Error(\"Invalid opcode: \" + bc[ip] + \".\");", + " }", + " }", + "", + " if (ends.length > 0) {", + " end = ends.pop();", + " ip = ips.pop();", + " } else {", + " break;", + " }", + " }" + ].join("\n")); + + parts.push(indent2(generateRuleFooter("peg$ruleNames[index]", "stack[0]"))); + parts.push("}"); + + return parts.join("\n"); + } + + function generateRuleFunction(rule) { + let parts = []; + let stackVars = []; + let code; + + function c(i) { return "peg$c" + i; } // |consts[i]| of the abstract machine + function s(i) { return "s" + i; } // |stack[i]| of the abstract machine + + let stack = { + sp: -1, + maxSp: -1, + + push(exprCode) { + let code = s(++this.sp) + " = " + exprCode + ";"; + + if (this.sp > this.maxSp) { this.maxSp = this.sp; } + + return code; + }, + + pop(n) { + if (n === undefined) { + return s(this.sp--); + } else { + let values = Array(n); + + for (let i = 0; i < n; i++) { + values[i] = s(this.sp - n + 1 + i); + } + + this.sp -= n; + + return values; + } + }, + + top() { + return s(this.sp); + }, + + index(i) { + return s(this.sp - i); + } + }; + + function compile(bc) { + let ip = 0; + let end = bc.length; + let parts = []; + let value; + + function compileCondition(cond, argCount) { + let baseLength = argCount + 3; + let thenLength = bc[ip + baseLength - 2]; + let elseLength = bc[ip + baseLength - 1]; + let baseSp = stack.sp; + let thenCode, elseCode, thenSp, elseSp; + + ip += baseLength; + thenCode = compile(bc.slice(ip, ip + thenLength)); + thenSp = stack.sp; + ip += thenLength; + + if (elseLength > 0) { + stack.sp = baseSp; + elseCode = compile(bc.slice(ip, ip + elseLength)); + elseSp = stack.sp; + ip += elseLength; + + if (thenSp !== elseSp) { + throw new Error( + "Branches of a condition must move the stack pointer in the same way." + ); + } + } + + parts.push("if (" + cond + ") {"); + parts.push(indent2(thenCode)); + if (elseLength > 0) { + parts.push("} else {"); + parts.push(indent2(elseCode)); + } + parts.push("}"); + } + + function compileLoop(cond) { + let baseLength = 2; + let bodyLength = bc[ip + baseLength - 1]; + let baseSp = stack.sp; + let bodyCode, bodySp; + + ip += baseLength; + bodyCode = compile(bc.slice(ip, ip + bodyLength)); + bodySp = stack.sp; + ip += bodyLength; + + if (bodySp !== baseSp) { + throw new Error("Body of a loop can't move the stack pointer."); + } + + parts.push("while (" + cond + ") {"); + parts.push(indent2(bodyCode)); + parts.push("}"); + } + + function compileCall() { + let baseLength = 4; + let paramsLength = bc[ip + baseLength - 1]; + + let value = c(bc[ip + 1]) + "(" + bc.slice(ip + baseLength, ip + baseLength + paramsLength).map( - p => stack.index(p) - ).join(", ") + p => stack.index(p) + ).join(", ") + ")"; - stack.pop(bc[ip + 2]); - parts.push(stack.push(value)); - ip += baseLength + paramsLength; - } - - while (ip < end) { - switch (bc[ip]) { - case op.PUSH: // PUSH c - parts.push(stack.push(c(bc[ip + 1]))); - ip += 2; - break; - - case op.PUSH_CURR_POS: // PUSH_CURR_POS - parts.push(stack.push("peg$currPos")); - ip++; - break; - - case op.PUSH_UNDEFINED: // PUSH_UNDEFINED - parts.push(stack.push("undefined")); - ip++; - break; - - case op.PUSH_NULL: // PUSH_NULL - parts.push(stack.push("null")); - ip++; - break; - - case op.PUSH_FAILED: // PUSH_FAILED - parts.push(stack.push("peg$FAILED")); - ip++; - break; - - case op.PUSH_EMPTY_ARRAY: // PUSH_EMPTY_ARRAY - parts.push(stack.push("[]")); - ip++; - break; - - case op.POP: // POP - stack.pop(); - ip++; - break; - - case op.POP_CURR_POS: // POP_CURR_POS - parts.push("peg$currPos = " + stack.pop() + ";"); - ip++; - break; - - case op.POP_N: // POP_N n - stack.pop(bc[ip + 1]); - ip += 2; - break; - - case op.NIP: // NIP - value = stack.pop(); - stack.pop(); - parts.push(stack.push(value)); - ip++; - break; - - case op.APPEND: // APPEND - value = stack.pop(); - parts.push(stack.top() + ".push(" + value + ");"); - ip++; - break; - - case op.WRAP: // WRAP n - parts.push( - stack.push("[" + stack.pop(bc[ip + 1]).join(", ") + "]") - ); - ip += 2; - break; - - case op.TEXT: // TEXT - parts.push( - stack.push("input.substring(" + stack.pop() + ", peg$currPos)") - ); - ip++; - break; - - case op.IF: // IF t, f - compileCondition(stack.top(), 0); - break; - - case op.IF_ERROR: // IF_ERROR t, f - compileCondition(stack.top() + " === peg$FAILED", 0); - break; - - case op.IF_NOT_ERROR: // IF_NOT_ERROR t, f - compileCondition(stack.top() + " !== peg$FAILED", 0); - break; - - case op.WHILE_NOT_ERROR: // WHILE_NOT_ERROR b - compileLoop(stack.top() + " !== peg$FAILED", 0); - break; - - case op.MATCH_ANY: // MATCH_ANY a, f, ... - compileCondition("input.length > peg$currPos", 0); - break; - - case op.MATCH_STRING: // MATCH_STRING s, a, f, ... - compileCondition( - eval(ast.consts[bc[ip + 1]]).length > 1 - ? "input.substr(peg$currPos, " + stack.pop(bc[ip + 2]); + parts.push(stack.push(value)); + ip += baseLength + paramsLength; + } + + while (ip < end) { + switch (bc[ip]) { + case op.PUSH: // PUSH c + parts.push(stack.push(c(bc[ip + 1]))); + ip += 2; + break; + + case op.PUSH_CURR_POS: // PUSH_CURR_POS + parts.push(stack.push("peg$currPos")); + ip++; + break; + + case op.PUSH_UNDEFINED: // PUSH_UNDEFINED + parts.push(stack.push("undefined")); + ip++; + break; + + case op.PUSH_NULL: // PUSH_NULL + parts.push(stack.push("null")); + ip++; + break; + + case op.PUSH_FAILED: // PUSH_FAILED + parts.push(stack.push("peg$FAILED")); + ip++; + break; + + case op.PUSH_EMPTY_ARRAY: // PUSH_EMPTY_ARRAY + parts.push(stack.push("[]")); + ip++; + break; + + case op.POP: // POP + stack.pop(); + ip++; + break; + + case op.POP_CURR_POS: // POP_CURR_POS + parts.push("peg$currPos = " + stack.pop() + ";"); + ip++; + break; + + case op.POP_N: // POP_N n + stack.pop(bc[ip + 1]); + ip += 2; + break; + + case op.NIP: // NIP + value = stack.pop(); + stack.pop(); + parts.push(stack.push(value)); + ip++; + break; + + case op.APPEND: // APPEND + value = stack.pop(); + parts.push(stack.top() + ".push(" + value + ");"); + ip++; + break; + + case op.WRAP: // WRAP n + parts.push( + stack.push("[" + stack.pop(bc[ip + 1]).join(", ") + "]") + ); + ip += 2; + break; + + case op.TEXT: // TEXT + parts.push( + stack.push("input.substring(" + stack.pop() + ", peg$currPos)") + ); + ip++; + break; + + case op.IF: // IF t, f + compileCondition(stack.top(), 0); + break; + + case op.IF_ERROR: // IF_ERROR t, f + compileCondition(stack.top() + " === peg$FAILED", 0); + break; + + case op.IF_NOT_ERROR: // IF_NOT_ERROR t, f + compileCondition(stack.top() + " !== peg$FAILED", 0); + break; + + case op.WHILE_NOT_ERROR: // WHILE_NOT_ERROR b + compileLoop(stack.top() + " !== peg$FAILED", 0); + break; + + case op.MATCH_ANY: // MATCH_ANY a, f, ... + compileCondition("input.length > peg$currPos", 0); + break; + + case op.MATCH_STRING: // MATCH_STRING s, a, f, ... + compileCondition( + eval(ast.consts[bc[ip + 1]]).length > 1 + ? "input.substr(peg$currPos, " + eval(ast.consts[bc[ip + 1]]).length + ") === " + c(bc[ip + 1]) - : "input.charCodeAt(peg$currPos) === " + : "input.charCodeAt(peg$currPos) === " + eval(ast.consts[bc[ip + 1]]).charCodeAt(0), - 1 - ); - break; + 1 + ); + break; - case op.MATCH_STRING_IC: // MATCH_STRING_IC s, a, f, ... - compileCondition( - "input.substr(peg$currPos, " + case op.MATCH_STRING_IC: // MATCH_STRING_IC s, a, f, ... + compileCondition( + "input.substr(peg$currPos, " + eval(ast.consts[bc[ip + 1]]).length + ").toLowerCase() === " + c(bc[ip + 1]), - 1 - ); - break; - - case op.MATCH_REGEXP: // MATCH_REGEXP r, a, f, ... - compileCondition( - c(bc[ip + 1]) + ".test(input.charAt(peg$currPos))", - 1 - ); - break; - - case op.ACCEPT_N: // ACCEPT_N n - parts.push(stack.push( - bc[ip + 1] > 1 - ? "input.substr(peg$currPos, " + bc[ip + 1] + ")" - : "input.charAt(peg$currPos)" - )); - parts.push( - bc[ip + 1] > 1 - ? "peg$currPos += " + bc[ip + 1] + ";" - : "peg$currPos++;" - ); - ip += 2; - break; - - case op.ACCEPT_STRING: // ACCEPT_STRING s - parts.push(stack.push(c(bc[ip + 1]))); - parts.push( - eval(ast.consts[bc[ip + 1]]).length > 1 - ? "peg$currPos += " + eval(ast.consts[bc[ip + 1]]).length + ";" - : "peg$currPos++;" - ); - ip += 2; - break; - - case op.FAIL: // FAIL e - parts.push(stack.push("peg$FAILED")); - parts.push("if (peg$silentFails === 0) { peg$fail(" + c(bc[ip + 1]) + "); }"); - ip += 2; - break; - - case op.LOAD_SAVED_POS: // LOAD_SAVED_POS p - parts.push("peg$savedPos = " + stack.index(bc[ip + 1]) + ";"); - ip += 2; - break; - - case op.UPDATE_SAVED_POS: // UPDATE_SAVED_POS - parts.push("peg$savedPos = peg$currPos;"); - ip++; - break; - - case op.CALL: // CALL f, n, pc, p1, p2, ..., pN - compileCall(); - break; - - case op.RULE: // RULE r - parts.push(stack.push("peg$parse" + ast.rules[bc[ip + 1]].name + "()")); - ip += 2; - break; - - case op.SILENT_FAILS_ON: // SILENT_FAILS_ON - parts.push("peg$silentFails++;"); - ip++; - break; - - case op.SILENT_FAILS_OFF: // SILENT_FAILS_OFF - parts.push("peg$silentFails--;"); - ip++; - break; - - default: - throw new Error("Invalid opcode: " + bc[ip] + "."); - } - } - - return parts.join("\n"); - } - - code = compile(rule.bytecode); - - parts.push("function peg$parse" + rule.name + "() {"); - - if (options.trace) { - parts.push(" var startPos = peg$currPos;"); - } - - for (let i = 0; i <= stack.maxSp; i++) { - stackVars[i] = s(i); - } - - parts.push(" var " + stackVars.join(", ") + ";"); - - parts.push(indent2(generateRuleHeader( - "\"" + js.stringEscape(rule.name) + "\"", - asts.indexOfRule(ast, rule.name) - ))); - parts.push(indent2(code)); - parts.push(indent2(generateRuleFooter( - "\"" + js.stringEscape(rule.name) + "\"", - s(0) - ))); - - parts.push("}"); - - return parts.join("\n"); - } - - function generateToplevel() { - let parts = []; - - parts.push([ - "function peg$subclass(child, parent) {", - " function C() { this.constructor = child; }", - " C.prototype = parent.prototype;", - " child.prototype = new C();", - "}", - "", - "function peg$SyntaxError(message, expected, found, location) {", - " this.message = message;", - " this.expected = expected;", - " this.found = found;", - " this.location = location;", - " this.name = \"SyntaxError\";", - "", - " if (typeof Error.captureStackTrace === \"function\") {", - " Error.captureStackTrace(this, peg$SyntaxError);", - " }", - "}", - "", - "peg$subclass(peg$SyntaxError, Error);", - "", - "peg$SyntaxError.buildMessage = function(expected, found) {", - " var DESCRIBE_EXPECTATION_FNS = {", - " literal: function(expectation) {", - " return \"\\\"\" + literalEscape(expectation.text) + \"\\\"\";", - " },", - "", - " class: function(expectation) {", - " var escapedParts = expectation.parts.map(function(part) {", - " return Array.isArray(part)", - " ? classEscape(part[0]) + \"-\" + classEscape(part[1])", - " : classEscape(part);", - " });", - "", - " return \"[\" + (expectation.inverted ? \"^\" : \"\") + escapedParts + \"]\";", - " },", - "", - " any: function() {", - " return \"any character\";", - " },", - "", - " end: function() {", - " return \"end of input\";", - " },", - "", - " other: function(expectation) {", - " return expectation.description;", - " }", - " };", - "", - " function hex(ch) {", - " return ch.charCodeAt(0).toString(16).toUpperCase();", - " }", - "", - " function literalEscape(s) {", - " return s", - " .replace(/\\\\/g, \"\\\\\\\\\")", // backslash - " .replace(/\"/g, \"\\\\\\\"\")", // closing double quote - " .replace(/\\0/g, \"\\\\0\")", // null - " .replace(/\\t/g, \"\\\\t\")", // horizontal tab - " .replace(/\\n/g, \"\\\\n\")", // line feed - " .replace(/\\r/g, \"\\\\r\")", // carriage return - " .replace(/[\\x00-\\x0F]/g, function(ch) { return \"\\\\x0\" + hex(ch); })", - " .replace(/[\\x10-\\x1F\\x7F-\\x9F]/g, function(ch) { return \"\\\\x\" + hex(ch); });", - " }", - "", - " function classEscape(s) {", - " return s", - " .replace(/\\\\/g, \"\\\\\\\\\")", // backslash - " .replace(/\\]/g, \"\\\\]\")", // closing bracket - " .replace(/\\^/g, \"\\\\^\")", // caret - " .replace(/-/g, \"\\\\-\")", // dash - " .replace(/\\0/g, \"\\\\0\")", // null - " .replace(/\\t/g, \"\\\\t\")", // horizontal tab - " .replace(/\\n/g, \"\\\\n\")", // line feed - " .replace(/\\r/g, \"\\\\r\")", // carriage return - " .replace(/[\\x00-\\x0F]/g, function(ch) { return \"\\\\x0\" + hex(ch); })", - " .replace(/[\\x10-\\x1F\\x7F-\\x9F]/g, function(ch) { return \"\\\\x\" + hex(ch); });", - " }", - "", - " function describeExpectation(expectation) {", - " return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);", - " }", - "", - " function describeExpected(expected) {", - " var descriptions = expected.map(describeExpectation);", - " var i, j;", - "", - " descriptions.sort();", - "", - " if (descriptions.length > 0) {", - " for (i = 1, j = 1; i < descriptions.length; i++) {", - " if (descriptions[i - 1] !== descriptions[i]) {", - " descriptions[j] = descriptions[i];", - " j++;", - " }", - " }", - " descriptions.length = j;", - " }", - "", - " switch (descriptions.length) {", - " case 1:", - " return descriptions[0];", - "", - " case 2:", - " return descriptions[0] + \" or \" + descriptions[1];", - "", - " default:", - " return descriptions.slice(0, -1).join(\", \")", - " + \", or \"", - " + descriptions[descriptions.length - 1];", - " }", - " }", - "", - " function describeFound(found) {", - " return found ? \"\\\"\" + literalEscape(found) + \"\\\"\" : \"end of input\";", - " }", - "", - " return \"Expected \" + describeExpected(expected) + \" but \" + describeFound(found) + \" found.\";", - "};", - "" - ].join("\n")); - - if (options.trace) { - parts.push([ - "function peg$DefaultTracer() {", - " this.indentLevel = 0;", - "}", - "", - "peg$DefaultTracer.prototype.trace = function(event) {", - " var that = this;", - "", - " function log(event) {", - " function repeat(string, n) {", - " var result = \"\", i;", - "", - " for (i = 0; i < n; i++) {", - " result += string;", - " }", - "", - " return result;", - " }", - "", - " function pad(string, length) {", - " return string + repeat(\" \", length - string.length);", - " }", - "", - " if (typeof console === \"object\") {", // IE 8-10 - " console.log(", - " event.location.start.line + \":\" + event.location.start.column + \"-\"", - " + event.location.end.line + \":\" + event.location.end.column + \" \"", - " + pad(event.type, 10) + \" \"", - " + repeat(\" \", that.indentLevel) + event.rule", - " );", - " }", - " }", - "", - " switch (event.type) {", - " case \"rule.enter\":", - " log(event);", - " this.indentLevel++;", - " break;", - "", - " case \"rule.match\":", - " this.indentLevel--;", - " log(event);", - " break;", - "", - " case \"rule.fail\":", - " this.indentLevel--;", - " log(event);", - " break;", - "", - " default:", - " throw new Error(\"Invalid event type: \" + event.type + \".\");", - " }", - "};", - "" - ].join("\n")); - } - - parts.push([ - "function peg$parse(input, options) {", - " options = options !== undefined ? options : {};", - "", - " var peg$FAILED = {};", - "" - ].join("\n")); - - if (options.optimize === "size") { - let startRuleIndices = "{ " + 1 + ); + break; + + case op.MATCH_REGEXP: // MATCH_REGEXP r, a, f, ... + compileCondition( + c(bc[ip + 1]) + ".test(input.charAt(peg$currPos))", + 1 + ); + break; + + case op.ACCEPT_N: // ACCEPT_N n + parts.push(stack.push( + bc[ip + 1] > 1 + ? "input.substr(peg$currPos, " + bc[ip + 1] + ")" + : "input.charAt(peg$currPos)" + )); + parts.push( + bc[ip + 1] > 1 + ? "peg$currPos += " + bc[ip + 1] + ";" + : "peg$currPos++;" + ); + ip += 2; + break; + + case op.ACCEPT_STRING: // ACCEPT_STRING s + parts.push(stack.push(c(bc[ip + 1]))); + parts.push( + eval(ast.consts[bc[ip + 1]]).length > 1 + ? "peg$currPos += " + eval(ast.consts[bc[ip + 1]]).length + ";" + : "peg$currPos++;" + ); + ip += 2; + break; + + case op.FAIL: // FAIL e + parts.push(stack.push("peg$FAILED")); + parts.push("if (peg$silentFails === 0) { peg$fail(" + c(bc[ip + 1]) + "); }"); + ip += 2; + break; + + case op.LOAD_SAVED_POS: // LOAD_SAVED_POS p + parts.push("peg$savedPos = " + stack.index(bc[ip + 1]) + ";"); + ip += 2; + break; + + case op.UPDATE_SAVED_POS: // UPDATE_SAVED_POS + parts.push("peg$savedPos = peg$currPos;"); + ip++; + break; + + case op.CALL: // CALL f, n, pc, p1, p2, ..., pN + compileCall(); + break; + + case op.RULE: // RULE r + parts.push(stack.push("peg$parse" + ast.rules[bc[ip + 1]].name + "()")); + ip += 2; + break; + + case op.SILENT_FAILS_ON: // SILENT_FAILS_ON + parts.push("peg$silentFails++;"); + ip++; + break; + + case op.SILENT_FAILS_OFF: // SILENT_FAILS_OFF + parts.push("peg$silentFails--;"); + ip++; + break; + + default: + throw new Error("Invalid opcode: " + bc[ip] + "."); + } + } + + return parts.join("\n"); + } + + code = compile(rule.bytecode); + + parts.push("function peg$parse" + rule.name + "() {"); + + if (options.trace) { + parts.push(" var startPos = peg$currPos;"); + } + + for (let i = 0; i <= stack.maxSp; i++) { + stackVars[i] = s(i); + } + + parts.push(" var " + stackVars.join(", ") + ";"); + + parts.push(indent2(generateRuleHeader( + "\"" + js.stringEscape(rule.name) + "\"", + asts.indexOfRule(ast, rule.name) + ))); + parts.push(indent2(code)); + parts.push(indent2(generateRuleFooter( + "\"" + js.stringEscape(rule.name) + "\"", + s(0) + ))); + + parts.push("}"); + + return parts.join("\n"); + } + + function generateToplevel() { + let parts = []; + + parts.push([ + "function peg$subclass(child, parent) {", + " function C() { this.constructor = child; }", + " C.prototype = parent.prototype;", + " child.prototype = new C();", + "}", + "", + "function peg$SyntaxError(message, expected, found, location) {", + " this.message = message;", + " this.expected = expected;", + " this.found = found;", + " this.location = location;", + " this.name = \"SyntaxError\";", + "", + " if (typeof Error.captureStackTrace === \"function\") {", + " Error.captureStackTrace(this, peg$SyntaxError);", + " }", + "}", + "", + "peg$subclass(peg$SyntaxError, Error);", + "", + "peg$SyntaxError.buildMessage = function(expected, found) {", + " var DESCRIBE_EXPECTATION_FNS = {", + " literal: function(expectation) {", + " return \"\\\"\" + literalEscape(expectation.text) + \"\\\"\";", + " },", + "", + " class: function(expectation) {", + " var escapedParts = expectation.parts.map(function(part) {", + " return Array.isArray(part)", + " ? classEscape(part[0]) + \"-\" + classEscape(part[1])", + " : classEscape(part);", + " });", + "", + " return \"[\" + (expectation.inverted ? \"^\" : \"\") + escapedParts + \"]\";", + " },", + "", + " any: function() {", + " return \"any character\";", + " },", + "", + " end: function() {", + " return \"end of input\";", + " },", + "", + " other: function(expectation) {", + " return expectation.description;", + " }", + " };", + "", + " function hex(ch) {", + " return ch.charCodeAt(0).toString(16).toUpperCase();", + " }", + "", + " function literalEscape(s) {", + " return s", + " .replace(/\\\\/g, \"\\\\\\\\\")", // backslash + " .replace(/\"/g, \"\\\\\\\"\")", // closing double quote + " .replace(/\\0/g, \"\\\\0\")", // null + " .replace(/\\t/g, \"\\\\t\")", // horizontal tab + " .replace(/\\n/g, \"\\\\n\")", // line feed + " .replace(/\\r/g, \"\\\\r\")", // carriage return + " .replace(/[\\x00-\\x0F]/g, function(ch) { return \"\\\\x0\" + hex(ch); })", + " .replace(/[\\x10-\\x1F\\x7F-\\x9F]/g, function(ch) { return \"\\\\x\" + hex(ch); });", + " }", + "", + " function classEscape(s) {", + " return s", + " .replace(/\\\\/g, \"\\\\\\\\\")", // backslash + " .replace(/\\]/g, \"\\\\]\")", // closing bracket + " .replace(/\\^/g, \"\\\\^\")", // caret + " .replace(/-/g, \"\\\\-\")", // dash + " .replace(/\\0/g, \"\\\\0\")", // null + " .replace(/\\t/g, \"\\\\t\")", // horizontal tab + " .replace(/\\n/g, \"\\\\n\")", // line feed + " .replace(/\\r/g, \"\\\\r\")", // carriage return + " .replace(/[\\x00-\\x0F]/g, function(ch) { return \"\\\\x0\" + hex(ch); })", + " .replace(/[\\x10-\\x1F\\x7F-\\x9F]/g, function(ch) { return \"\\\\x\" + hex(ch); });", + " }", + "", + " function describeExpectation(expectation) {", + " return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);", + " }", + "", + " function describeExpected(expected) {", + " var descriptions = expected.map(describeExpectation);", + " var i, j;", + "", + " descriptions.sort();", + "", + " if (descriptions.length > 0) {", + " for (i = 1, j = 1; i < descriptions.length; i++) {", + " if (descriptions[i - 1] !== descriptions[i]) {", + " descriptions[j] = descriptions[i];", + " j++;", + " }", + " }", + " descriptions.length = j;", + " }", + "", + " switch (descriptions.length) {", + " case 1:", + " return descriptions[0];", + "", + " case 2:", + " return descriptions[0] + \" or \" + descriptions[1];", + "", + " default:", + " return descriptions.slice(0, -1).join(\", \")", + " + \", or \"", + " + descriptions[descriptions.length - 1];", + " }", + " }", + "", + " function describeFound(found) {", + " return found ? \"\\\"\" + literalEscape(found) + \"\\\"\" : \"end of input\";", + " }", + "", + " return \"Expected \" + describeExpected(expected) + \" but \" + describeFound(found) + \" found.\";", + "};", + "" + ].join("\n")); + + if (options.trace) { + parts.push([ + "function peg$DefaultTracer() {", + " this.indentLevel = 0;", + "}", + "", + "peg$DefaultTracer.prototype.trace = function(event) {", + " var that = this;", + "", + " function log(event) {", + " function repeat(string, n) {", + " var result = \"\", i;", + "", + " for (i = 0; i < n; i++) {", + " result += string;", + " }", + "", + " return result;", + " }", + "", + " function pad(string, length) {", + " return string + repeat(\" \", length - string.length);", + " }", + "", + " if (typeof console === \"object\") {", // IE 8-10 + " console.log(", + " event.location.start.line + \":\" + event.location.start.column + \"-\"", + " + event.location.end.line + \":\" + event.location.end.column + \" \"", + " + pad(event.type, 10) + \" \"", + " + repeat(\" \", that.indentLevel) + event.rule", + " );", + " }", + " }", + "", + " switch (event.type) {", + " case \"rule.enter\":", + " log(event);", + " this.indentLevel++;", + " break;", + "", + " case \"rule.match\":", + " this.indentLevel--;", + " log(event);", + " break;", + "", + " case \"rule.fail\":", + " this.indentLevel--;", + " log(event);", + " break;", + "", + " default:", + " throw new Error(\"Invalid event type: \" + event.type + \".\");", + " }", + "};", + "" + ].join("\n")); + } + + parts.push([ + "function peg$parse(input, options) {", + " options = options !== undefined ? options : {};", + "", + " var peg$FAILED = {};", + "" + ].join("\n")); + + if (options.optimize === "size") { + let startRuleIndices = "{ " + options.allowedStartRules.map( - r => r + ": " + asts.indexOfRule(ast, r) - ).join(", ") + r => r + ": " + asts.indexOfRule(ast, r) + ).join(", ") + " }"; - let startRuleIndex = asts.indexOfRule(ast, options.allowedStartRules[0]); - - parts.push([ - " var peg$startRuleIndices = " + startRuleIndices + ";", - " var peg$startRuleIndex = " + startRuleIndex + ";" - ].join("\n")); - } else { - let startRuleFunctions = "{ " + let startRuleIndex = asts.indexOfRule(ast, options.allowedStartRules[0]); + + parts.push([ + " var peg$startRuleIndices = " + startRuleIndices + ";", + " var peg$startRuleIndex = " + startRuleIndex + ";" + ].join("\n")); + } else { + let startRuleFunctions = "{ " + options.allowedStartRules.map( - r => r + ": peg$parse" + r - ).join(", ") + r => r + ": peg$parse" + r + ).join(", ") + " }"; - let startRuleFunction = "peg$parse" + options.allowedStartRules[0]; - - parts.push([ - " var peg$startRuleFunctions = " + startRuleFunctions + ";", - " var peg$startRuleFunction = " + startRuleFunction + ";" - ].join("\n")); - } - - parts.push(""); - - parts.push(indent2(generateTables())); - - parts.push([ - "", - " var peg$currPos = 0;", - " var peg$savedPos = 0;", - " var peg$posDetailsCache = [{ line: 1, column: 1 }];", - " var peg$maxFailPos = 0;", - " var peg$maxFailExpected = [];", - " var peg$silentFails = 0;", // 0 = report failures, > 0 = silence failures - "" - ].join("\n")); - - if (options.cache) { - parts.push([ - " var peg$resultsCache = {};", - "" - ].join("\n")); - } - - if (options.trace) { - if (options.optimize === "size") { - let ruleNames = "[" + let startRuleFunction = "peg$parse" + options.allowedStartRules[0]; + + parts.push([ + " var peg$startRuleFunctions = " + startRuleFunctions + ";", + " var peg$startRuleFunction = " + startRuleFunction + ";" + ].join("\n")); + } + + parts.push(""); + + parts.push(indent2(generateTables())); + + parts.push([ + "", + " var peg$currPos = 0;", + " var peg$savedPos = 0;", + " var peg$posDetailsCache = [{ line: 1, column: 1 }];", + " var peg$maxFailPos = 0;", + " var peg$maxFailExpected = [];", + " var peg$silentFails = 0;", // 0 = report failures, > 0 = silence failures + "" + ].join("\n")); + + if (options.cache) { + parts.push([ + " var peg$resultsCache = {};", + "" + ].join("\n")); + } + + if (options.trace) { + if (options.optimize === "size") { + let ruleNames = "[" + ast.rules.map( - r => "\"" + js.stringEscape(r.name) + "\"" - ).join(", ") + r => "\"" + js.stringEscape(r.name) + "\"" + ).join(", ") + "]"; - parts.push([ - " var peg$ruleNames = " + ruleNames + ";", - "" - ].join("\n")); - } - - parts.push([ - " var peg$tracer = \"tracer\" in options ? options.tracer : new peg$DefaultTracer();", - "" - ].join("\n")); - } - - parts.push([ - " var peg$result;", - "" - ].join("\n")); - - if (options.optimize === "size") { - parts.push([ - " if (\"startRule\" in options) {", - " if (!(options.startRule in peg$startRuleIndices)) {", - " throw new Error(\"Can't start parsing from rule \\\"\" + options.startRule + \"\\\".\");", - " }", - "", - " peg$startRuleIndex = peg$startRuleIndices[options.startRule];", - " }" - ].join("\n")); - } else { - parts.push([ - " if (\"startRule\" in options) {", - " if (!(options.startRule in peg$startRuleFunctions)) {", - " throw new Error(\"Can't start parsing from rule \\\"\" + options.startRule + \"\\\".\");", - " }", - "", - " peg$startRuleFunction = peg$startRuleFunctions[options.startRule];", - " }" - ].join("\n")); - } - - parts.push([ - "", - " function text() {", - " return input.substring(peg$savedPos, peg$currPos);", - " }", - "", - " function location() {", - " return peg$computeLocation(peg$savedPos, peg$currPos);", - " }", - "", - " function expected(description, location) {", - " location = location !== undefined", - " ? location", - " : peg$computeLocation(peg$savedPos, peg$currPos);", - "", - " throw peg$buildStructuredError(", - " [peg$otherExpectation(description)],", - " input.substring(peg$savedPos, peg$currPos),", - " location", - " );", - " }", - "", - " function error(message, location) {", - " location = location !== undefined", - " ? location", - " : peg$computeLocation(peg$savedPos, peg$currPos);", - "", - " throw peg$buildSimpleError(message, location);", - " }", - "", - " function peg$literalExpectation(text, ignoreCase) {", - " return { type: \"literal\", text: text, ignoreCase: ignoreCase };", - " }", - "", - " function peg$classExpectation(parts, inverted, ignoreCase) {", - " return { type: \"class\", parts: parts, inverted: inverted, ignoreCase: ignoreCase };", - " }", - "", - " function peg$anyExpectation() {", - " return { type: \"any\" };", - " }", - "", - " function peg$endExpectation() {", - " return { type: \"end\" };", - " }", - "", - " function peg$otherExpectation(description) {", - " return { type: \"other\", description: description };", - " }", - "", - " function peg$computePosDetails(pos) {", - " var details = peg$posDetailsCache[pos];", - " var p;", - "", - " if (details) {", - " return details;", - " } else {", - " p = pos - 1;", - " while (!peg$posDetailsCache[p]) {", - " p--;", - " }", - "", - " details = peg$posDetailsCache[p];", - " details = {", - " line: details.line,", - " column: details.column", - " };", - "", - " while (p < pos) {", - " if (input.charCodeAt(p) === 10) {", - " details.line++;", - " details.column = 1;", - " } else {", - " details.column++;", - " }", - "", - " p++;", - " }", - "", - " peg$posDetailsCache[pos] = details;", - "", - " return details;", - " }", - " }", - "", - " function peg$computeLocation(startPos, endPos) {", - " var startPosDetails = peg$computePosDetails(startPos);", - " var endPosDetails = peg$computePosDetails(endPos);", - "", - " return {", - " start: {", - " offset: startPos,", - " line: startPosDetails.line,", - " column: startPosDetails.column", - " },", - " end: {", - " offset: endPos,", - " line: endPosDetails.line,", - " column: endPosDetails.column", - " }", - " };", - " }", - "", - " function peg$fail(expected) {", - " if (peg$currPos < peg$maxFailPos) { return; }", - "", - " if (peg$currPos > peg$maxFailPos) {", - " peg$maxFailPos = peg$currPos;", - " peg$maxFailExpected = [];", - " }", - "", - " peg$maxFailExpected.push(expected);", - " }", - "", - " function peg$buildSimpleError(message, location) {", - " return new peg$SyntaxError(message, null, null, location);", - " }", - "", - " function peg$buildStructuredError(expected, found, location) {", - " return new peg$SyntaxError(", - " peg$SyntaxError.buildMessage(expected, found),", - " expected,", - " found,", - " location", - " );", - " }", - "" - ].join("\n")); - - if (options.optimize === "size") { - parts.push(indent2(generateInterpreter())); - parts.push(""); - } else { - ast.rules.forEach(rule => { - parts.push(indent2(generateRuleFunction(rule))); - parts.push(""); - }); - } - - if (ast.initializer) { - parts.push(indent2(ast.initializer.code)); - parts.push(""); - } - - if (options.optimize === "size") { - parts.push(" peg$result = peg$parseRule(peg$startRuleIndex);"); - } else { - parts.push(" peg$result = peg$startRuleFunction();"); - } - - parts.push([ - "", - " if (peg$result !== peg$FAILED && peg$currPos === input.length) {", - " return peg$result;", - " } else {", - " if (peg$result !== peg$FAILED && peg$currPos < input.length) {", - " peg$fail(peg$endExpectation());", - " }", - "", - " throw peg$buildStructuredError(", - " peg$maxFailExpected,", - " peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,", - " peg$maxFailPos < input.length", - " ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)", - " : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)", - " );", - " }", - "}" - ].join("\n")); - - return parts.join("\n"); - } - - function generateWrapper(toplevelCode) { - function generateGeneratedByComment() { - return [ - "// Generated by PEG.js 0.10.0.", - "//", - "// https://pegjs.org/" - ].join("\n"); - } - - function generateParserObject() { - return options.trace - ? [ - "{", - " SyntaxError: peg$SyntaxError,", - " DefaultTracer: peg$DefaultTracer,", - " parse: peg$parse", - "}" - ].join("\n") - : [ - "{", - " SyntaxError: peg$SyntaxError,", - " parse: peg$parse", - "}" - ].join("\n"); - } - - let generators = { - bare() { - return [ - generateGeneratedByComment(), - "(function() {", - " \"use strict\";", - "", - indent2(toplevelCode), - "", - indent2("return " + generateParserObject() + ";"), - "})()" - ].join("\n"); - }, - - commonjs() { - let parts = []; - let dependencyVars = Object.keys(options.dependencies); - - parts.push([ - generateGeneratedByComment(), - "", - "\"use strict\";", - "" - ].join("\n")); - - if (dependencyVars.length > 0) { - dependencyVars.forEach(variable => { - parts.push("var " + variable + parts.push([ + " var peg$ruleNames = " + ruleNames + ";", + "" + ].join("\n")); + } + + parts.push([ + " var peg$tracer = \"tracer\" in options ? options.tracer : new peg$DefaultTracer();", + "" + ].join("\n")); + } + + parts.push([ + " var peg$result;", + "" + ].join("\n")); + + if (options.optimize === "size") { + parts.push([ + " if (\"startRule\" in options) {", + " if (!(options.startRule in peg$startRuleIndices)) {", + " throw new Error(\"Can't start parsing from rule \\\"\" + options.startRule + \"\\\".\");", + " }", + "", + " peg$startRuleIndex = peg$startRuleIndices[options.startRule];", + " }" + ].join("\n")); + } else { + parts.push([ + " if (\"startRule\" in options) {", + " if (!(options.startRule in peg$startRuleFunctions)) {", + " throw new Error(\"Can't start parsing from rule \\\"\" + options.startRule + \"\\\".\");", + " }", + "", + " peg$startRuleFunction = peg$startRuleFunctions[options.startRule];", + " }" + ].join("\n")); + } + + parts.push([ + "", + " function text() {", + " return input.substring(peg$savedPos, peg$currPos);", + " }", + "", + " function location() {", + " return peg$computeLocation(peg$savedPos, peg$currPos);", + " }", + "", + " function expected(description, location) {", + " location = location !== undefined", + " ? location", + " : peg$computeLocation(peg$savedPos, peg$currPos);", + "", + " throw peg$buildStructuredError(", + " [peg$otherExpectation(description)],", + " input.substring(peg$savedPos, peg$currPos),", + " location", + " );", + " }", + "", + " function error(message, location) {", + " location = location !== undefined", + " ? location", + " : peg$computeLocation(peg$savedPos, peg$currPos);", + "", + " throw peg$buildSimpleError(message, location);", + " }", + "", + " function peg$literalExpectation(text, ignoreCase) {", + " return { type: \"literal\", text: text, ignoreCase: ignoreCase };", + " }", + "", + " function peg$classExpectation(parts, inverted, ignoreCase) {", + " return { type: \"class\", parts: parts, inverted: inverted, ignoreCase: ignoreCase };", + " }", + "", + " function peg$anyExpectation() {", + " return { type: \"any\" };", + " }", + "", + " function peg$endExpectation() {", + " return { type: \"end\" };", + " }", + "", + " function peg$otherExpectation(description) {", + " return { type: \"other\", description: description };", + " }", + "", + " function peg$computePosDetails(pos) {", + " var details = peg$posDetailsCache[pos];", + " var p;", + "", + " if (details) {", + " return details;", + " } else {", + " p = pos - 1;", + " while (!peg$posDetailsCache[p]) {", + " p--;", + " }", + "", + " details = peg$posDetailsCache[p];", + " details = {", + " line: details.line,", + " column: details.column", + " };", + "", + " while (p < pos) {", + " if (input.charCodeAt(p) === 10) {", + " details.line++;", + " details.column = 1;", + " } else {", + " details.column++;", + " }", + "", + " p++;", + " }", + "", + " peg$posDetailsCache[pos] = details;", + "", + " return details;", + " }", + " }", + "", + " function peg$computeLocation(startPos, endPos) {", + " var startPosDetails = peg$computePosDetails(startPos);", + " var endPosDetails = peg$computePosDetails(endPos);", + "", + " return {", + " start: {", + " offset: startPos,", + " line: startPosDetails.line,", + " column: startPosDetails.column", + " },", + " end: {", + " offset: endPos,", + " line: endPosDetails.line,", + " column: endPosDetails.column", + " }", + " };", + " }", + "", + " function peg$fail(expected) {", + " if (peg$currPos < peg$maxFailPos) { return; }", + "", + " if (peg$currPos > peg$maxFailPos) {", + " peg$maxFailPos = peg$currPos;", + " peg$maxFailExpected = [];", + " }", + "", + " peg$maxFailExpected.push(expected);", + " }", + "", + " function peg$buildSimpleError(message, location) {", + " return new peg$SyntaxError(message, null, null, location);", + " }", + "", + " function peg$buildStructuredError(expected, found, location) {", + " return new peg$SyntaxError(", + " peg$SyntaxError.buildMessage(expected, found),", + " expected,", + " found,", + " location", + " );", + " }", + "" + ].join("\n")); + + if (options.optimize === "size") { + parts.push(indent2(generateInterpreter())); + parts.push(""); + } else { + ast.rules.forEach(rule => { + parts.push(indent2(generateRuleFunction(rule))); + parts.push(""); + }); + } + + if (ast.initializer) { + parts.push(indent2(ast.initializer.code)); + parts.push(""); + } + + if (options.optimize === "size") { + parts.push(" peg$result = peg$parseRule(peg$startRuleIndex);"); + } else { + parts.push(" peg$result = peg$startRuleFunction();"); + } + + parts.push([ + "", + " if (peg$result !== peg$FAILED && peg$currPos === input.length) {", + " return peg$result;", + " } else {", + " if (peg$result !== peg$FAILED && peg$currPos < input.length) {", + " peg$fail(peg$endExpectation());", + " }", + "", + " throw peg$buildStructuredError(", + " peg$maxFailExpected,", + " peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,", + " peg$maxFailPos < input.length", + " ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)", + " : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)", + " );", + " }", + "}" + ].join("\n")); + + return parts.join("\n"); + } + + function generateWrapper(toplevelCode) { + function generateGeneratedByComment() { + return [ + "// Generated by PEG.js 0.10.0.", + "//", + "// https://pegjs.org/" + ].join("\n"); + } + + function generateParserObject() { + return options.trace + ? [ + "{", + " SyntaxError: peg$SyntaxError,", + " DefaultTracer: peg$DefaultTracer,", + " parse: peg$parse", + "}" + ].join("\n") + : [ + "{", + " SyntaxError: peg$SyntaxError,", + " parse: peg$parse", + "}" + ].join("\n"); + } + + let generators = { + bare() { + return [ + generateGeneratedByComment(), + "(function() {", + " \"use strict\";", + "", + indent2(toplevelCode), + "", + indent2("return " + generateParserObject() + ";"), + "})()" + ].join("\n"); + }, + + commonjs() { + let parts = []; + let dependencyVars = Object.keys(options.dependencies); + + parts.push([ + generateGeneratedByComment(), + "", + "\"use strict\";", + "" + ].join("\n")); + + if (dependencyVars.length > 0) { + dependencyVars.forEach(variable => { + parts.push("var " + variable + " = require(\"" + js.stringEscape(options.dependencies[variable]) + "\");" - ); - }); - parts.push(""); - } - - parts.push([ - toplevelCode, - "", - "module.exports = " + generateParserObject() + ";", - "" - ].join("\n")); - - return parts.join("\n"); - }, - - amd() { - let dependencyVars = Object.keys(options.dependencies); - let dependencyIds = dependencyVars.map(v => options.dependencies[v]); - let dependencies = "[" + ); + }); + parts.push(""); + } + + parts.push([ + toplevelCode, + "", + "module.exports = " + generateParserObject() + ";", + "" + ].join("\n")); + + return parts.join("\n"); + }, + + amd() { + let dependencyVars = Object.keys(options.dependencies); + let dependencyIds = dependencyVars.map(v => options.dependencies[v]); + let dependencies = "[" + dependencyIds.map( - id => "\"" + js.stringEscape(id) + "\"" - ).join(", ") + id => "\"" + js.stringEscape(id) + "\"" + ).join(", ") + "]"; - let params = dependencyVars.join(", "); - - return [ - generateGeneratedByComment(), - "define(" + dependencies + ", function(" + params + ") {", - " \"use strict\";", - "", - indent2(toplevelCode), - "", - indent2("return " + generateParserObject() + ";"), - "});", - "" - ].join("\n"); - }, - - globals() { - return [ - generateGeneratedByComment(), - "(function(root) {", - " \"use strict\";", - "", - indent2(toplevelCode), - "", - indent2("root." + options.exportVar + " = " + generateParserObject() + ";"), - "})(this);", - "" - ].join("\n"); - }, - - umd() { - let parts = []; - let dependencyVars = Object.keys(options.dependencies); - let dependencyIds = dependencyVars.map(v => options.dependencies[v]); - let dependencies = "[" + let params = dependencyVars.join(", "); + + return [ + generateGeneratedByComment(), + "define(" + dependencies + ", function(" + params + ") {", + " \"use strict\";", + "", + indent2(toplevelCode), + "", + indent2("return " + generateParserObject() + ";"), + "});", + "" + ].join("\n"); + }, + + globals() { + return [ + generateGeneratedByComment(), + "(function(root) {", + " \"use strict\";", + "", + indent2(toplevelCode), + "", + indent2("root." + options.exportVar + " = " + generateParserObject() + ";"), + "})(this);", + "" + ].join("\n"); + }, + + umd() { + let parts = []; + let dependencyVars = Object.keys(options.dependencies); + let dependencyIds = dependencyVars.map(v => options.dependencies[v]); + let dependencies = "[" + dependencyIds.map( - id => "\"" + js.stringEscape(id) + "\"" - ).join(", ") + id => "\"" + js.stringEscape(id) + "\"" + ).join(", ") + "]"; - let requires = dependencyIds.map( - id => "require(\"" + js.stringEscape(id) + "\")" - ).join(", "); - let params = dependencyVars.join(", "); - - parts.push([ - generateGeneratedByComment(), - "(function(root, factory) {", - " if (typeof define === \"function\" && define.amd) {", - " define(" + dependencies + ", factory);", - " } else if (typeof module === \"object\" && module.exports) {", - " module.exports = factory(" + requires + ");" - ].join("\n")); - - if (options.exportVar !== null) { - parts.push([ - " } else {", - " root." + options.exportVar + " = factory();" - ].join("\n")); - } - - parts.push([ - " }", - "})(this, function(" + params + ") {", - " \"use strict\";", - "", - indent2(toplevelCode), - "", - indent2("return " + generateParserObject() + ";"), - "});", - "" - ].join("\n")); - - return parts.join("\n"); - } - }; - - return generators[options.format](); - } - - ast.code = generateWrapper(generateToplevel()); + let requires = dependencyIds.map( + id => "require(\"" + js.stringEscape(id) + "\")" + ).join(", "); + let params = dependencyVars.join(", "); + + parts.push([ + generateGeneratedByComment(), + "(function(root, factory) {", + " if (typeof define === \"function\" && define.amd) {", + " define(" + dependencies + ", factory);", + " } else if (typeof module === \"object\" && module.exports) {", + " module.exports = factory(" + requires + ");" + ].join("\n")); + + if (options.exportVar !== null) { + parts.push([ + " } else {", + " root." + options.exportVar + " = factory();" + ].join("\n")); + } + + parts.push([ + " }", + "})(this, function(" + params + ") {", + " \"use strict\";", + "", + indent2(toplevelCode), + "", + indent2("return " + generateParserObject() + ";"), + "});", + "" + ].join("\n")); + + return parts.join("\n"); + } + }; + + return generators[options.format](); + } + + ast.code = generateWrapper(generateToplevel()); } module.exports = generateJS; diff --git a/lib/compiler/passes/remove-proxy-rules.js b/lib/compiler/passes/remove-proxy-rules.js index a95cc3c..75b4492 100644 --- a/lib/compiler/passes/remove-proxy-rules.js +++ b/lib/compiler/passes/remove-proxy-rules.js @@ -4,36 +4,36 @@ let visitor = require("../visitor"); // Removes proxy rules -- that is, rules that only delegate to other rule. function removeProxyRules(ast, options) { - function isProxyRule(node) { - return node.type === "rule" && node.expression.type === "rule_ref"; - } - - function replaceRuleRefs(ast, from, to) { - let replace = visitor.build({ - rule_ref(node) { - if (node.name === from) { - node.name = to; - } - } - }); - - replace(ast); - } - - let indices = []; - - ast.rules.forEach((rule, i) => { - if (isProxyRule(rule)) { - replaceRuleRefs(ast, rule.name, rule.expression.name); - if (options.allowedStartRules.indexOf(rule.name) === -1) { - indices.push(i); - } - } - }); - - indices.reverse(); - - indices.forEach(i => { ast.rules.splice(i, 1); }); + function isProxyRule(node) { + return node.type === "rule" && node.expression.type === "rule_ref"; + } + + function replaceRuleRefs(ast, from, to) { + let replace = visitor.build({ + rule_ref(node) { + if (node.name === from) { + node.name = to; + } + } + }); + + replace(ast); + } + + let indices = []; + + ast.rules.forEach((rule, i) => { + if (isProxyRule(rule)) { + replaceRuleRefs(ast, rule.name, rule.expression.name); + if (options.allowedStartRules.indexOf(rule.name) === -1) { + indices.push(i); + } + } + }); + + indices.reverse(); + + indices.forEach(i => { ast.rules.splice(i, 1); }); } module.exports = removeProxyRules; diff --git a/lib/compiler/passes/report-duplicate-labels.js b/lib/compiler/passes/report-duplicate-labels.js index 7e15be6..8bce1f8 100644 --- a/lib/compiler/passes/report-duplicate-labels.js +++ b/lib/compiler/passes/report-duplicate-labels.js @@ -5,58 +5,58 @@ let visitor = require("../visitor"); // Checks that each label is defined only once within each scope. function reportDuplicateLabels(ast) { - function cloneEnv(env) { - let clone = {}; + function cloneEnv(env) { + let clone = {}; - Object.keys(env).forEach(name => { - clone[name] = env[name]; - }); + Object.keys(env).forEach(name => { + clone[name] = env[name]; + }); - return clone; - } + return clone; + } - function checkExpressionWithClonedEnv(node, env) { - check(node.expression, cloneEnv(env)); - } + function checkExpressionWithClonedEnv(node, env) { + check(node.expression, cloneEnv(env)); + } - let check = visitor.build({ - rule(node) { - check(node.expression, { }); - }, + let check = visitor.build({ + rule(node) { + check(node.expression, { }); + }, - choice(node, env) { - node.alternatives.forEach(alternative => { - check(alternative, cloneEnv(env)); - }); - }, + choice(node, env) { + node.alternatives.forEach(alternative => { + check(alternative, cloneEnv(env)); + }); + }, - action: checkExpressionWithClonedEnv, + action: checkExpressionWithClonedEnv, - labeled(node, env) { - if (Object.prototype.hasOwnProperty.call(env, node.label)) { - throw new GrammarError( - "Label \"" + node.label + "\" is already defined " + labeled(node, env) { + if (Object.prototype.hasOwnProperty.call(env, node.label)) { + throw new GrammarError( + "Label \"" + node.label + "\" is already defined " + "at line " + env[node.label].start.line + ", " + "column " + env[node.label].start.column + ".", - node.location - ); - } + node.location + ); + } - check(node.expression, env); + check(node.expression, env); - env[node.label] = node.location; - }, + env[node.label] = node.location; + }, - text: checkExpressionWithClonedEnv, - simple_and: checkExpressionWithClonedEnv, - simple_not: checkExpressionWithClonedEnv, - optional: checkExpressionWithClonedEnv, - zero_or_more: checkExpressionWithClonedEnv, - one_or_more: checkExpressionWithClonedEnv, - group: checkExpressionWithClonedEnv - }); + text: checkExpressionWithClonedEnv, + simple_and: checkExpressionWithClonedEnv, + simple_not: checkExpressionWithClonedEnv, + optional: checkExpressionWithClonedEnv, + zero_or_more: checkExpressionWithClonedEnv, + one_or_more: checkExpressionWithClonedEnv, + group: checkExpressionWithClonedEnv + }); - check(ast); + check(ast); } module.exports = reportDuplicateLabels; diff --git a/lib/compiler/passes/report-duplicate-rules.js b/lib/compiler/passes/report-duplicate-rules.js index ba318bb..775b5a1 100644 --- a/lib/compiler/passes/report-duplicate-rules.js +++ b/lib/compiler/passes/report-duplicate-rules.js @@ -5,24 +5,24 @@ let visitor = require("../visitor"); // Checks that each rule is defined only once. function reportDuplicateRules(ast) { - let rules = {}; + let rules = {}; - let check = visitor.build({ - rule(node) { - if (Object.prototype.hasOwnProperty.call(rules, node.name)) { - throw new GrammarError( - "Rule \"" + node.name + "\" is already defined " + let check = visitor.build({ + rule(node) { + if (Object.prototype.hasOwnProperty.call(rules, node.name)) { + throw new GrammarError( + "Rule \"" + node.name + "\" is already defined " + "at line " + rules[node.name].start.line + ", " + "column " + rules[node.name].start.column + ".", - node.location - ); - } + node.location + ); + } - rules[node.name] = node.location; - } - }); + rules[node.name] = node.location; + } + }); - check(ast); + check(ast); } module.exports = reportDuplicateRules; diff --git a/lib/compiler/passes/report-infinite-recursion.js b/lib/compiler/passes/report-infinite-recursion.js index d5ba312..77c09ff 100644 --- a/lib/compiler/passes/report-infinite-recursion.js +++ b/lib/compiler/passes/report-infinite-recursion.js @@ -15,40 +15,40 @@ let visitor = require("../visitor"); // In general, if a rule reference can be reached without consuming any input, // it can lead to left recursion. function reportInfiniteRecursion(ast) { - let visitedRules = []; + let visitedRules = []; - let check = visitor.build({ - rule(node) { - visitedRules.push(node.name); - check(node.expression); - visitedRules.pop(node.name); - }, + let check = visitor.build({ + rule(node) { + visitedRules.push(node.name); + check(node.expression); + visitedRules.pop(node.name); + }, - sequence(node) { - node.elements.every(element => { - check(element); + sequence(node) { + node.elements.every(element => { + check(element); - return !asts.alwaysConsumesOnSuccess(ast, element); - }); - }, + return !asts.alwaysConsumesOnSuccess(ast, element); + }); + }, - rule_ref(node) { - if (visitedRules.indexOf(node.name) !== -1) { - visitedRules.push(node.name); + rule_ref(node) { + if (visitedRules.indexOf(node.name) !== -1) { + visitedRules.push(node.name); - throw new GrammarError( - "Possible infinite loop when parsing (left recursion: " + throw new GrammarError( + "Possible infinite loop when parsing (left recursion: " + visitedRules.join(" -> ") + ").", - node.location - ); - } + node.location + ); + } - check(asts.findRule(ast, node.name)); - } - }); + check(asts.findRule(ast, node.name)); + } + }); - check(ast); + check(ast); } module.exports = reportInfiniteRecursion; diff --git a/lib/compiler/passes/report-infinite-repetition.js b/lib/compiler/passes/report-infinite-repetition.js index f521931..7b52439 100644 --- a/lib/compiler/passes/report-infinite-repetition.js +++ b/lib/compiler/passes/report-infinite-repetition.js @@ -7,27 +7,27 @@ let visitor = require("../visitor"); // Reports expressions that don't consume any input inside |*| or |+| in the // grammar, which prevents infinite loops in the generated parser. function reportInfiniteRepetition(ast) { - let check = visitor.build({ - zero_or_more(node) { - if (!asts.alwaysConsumesOnSuccess(ast, node.expression)) { - throw new GrammarError( - "Possible infinite loop when parsing (repetition used with an expression that may not consume any input).", - node.location - ); - } - }, + let check = visitor.build({ + zero_or_more(node) { + if (!asts.alwaysConsumesOnSuccess(ast, node.expression)) { + throw new GrammarError( + "Possible infinite loop when parsing (repetition used with an expression that may not consume any input).", + node.location + ); + } + }, - one_or_more(node) { - if (!asts.alwaysConsumesOnSuccess(ast, node.expression)) { - throw new GrammarError( - "Possible infinite loop when parsing (repetition used with an expression that may not consume any input).", - node.location - ); - } - } - }); + one_or_more(node) { + if (!asts.alwaysConsumesOnSuccess(ast, node.expression)) { + throw new GrammarError( + "Possible infinite loop when parsing (repetition used with an expression that may not consume any input).", + node.location + ); + } + } + }); - check(ast); + check(ast); } module.exports = reportInfiniteRepetition; diff --git a/lib/compiler/passes/report-undefined-rules.js b/lib/compiler/passes/report-undefined-rules.js index 0f5d941..2ed7f7f 100644 --- a/lib/compiler/passes/report-undefined-rules.js +++ b/lib/compiler/passes/report-undefined-rules.js @@ -6,18 +6,18 @@ let visitor = require("../visitor"); // Checks that all referenced rules exist. function reportUndefinedRules(ast) { - let check = visitor.build({ - rule_ref(node) { - if (!asts.findRule(ast, node.name)) { - throw new GrammarError( - "Rule \"" + node.name + "\" is not defined.", - node.location - ); - } - } - }); + let check = visitor.build({ + rule_ref(node) { + if (!asts.findRule(ast, node.name)) { + throw new GrammarError( + "Rule \"" + node.name + "\" is not defined.", + node.location + ); + } + } + }); - check(ast); + check(ast); } module.exports = reportUndefinedRules; diff --git a/lib/compiler/visitor.js b/lib/compiler/visitor.js index 8f5d08a..a42c324 100644 --- a/lib/compiler/visitor.js +++ b/lib/compiler/visitor.js @@ -2,74 +2,74 @@ // Simple AST node visitor builder. let visitor = { - build(functions) { - function visit(node) { - return functions[node.type].apply(null, arguments); - } + build(functions) { + function visit(node) { + return functions[node.type].apply(null, arguments); + } - function visitNop() { - // Do nothing. - } + function visitNop() { + // Do nothing. + } - function visitExpression(node) { - let extraArgs = Array.prototype.slice.call(arguments, 1); + function visitExpression(node) { + let extraArgs = Array.prototype.slice.call(arguments, 1); - visit.apply(null, [node.expression].concat(extraArgs)); - } + visit.apply(null, [node.expression].concat(extraArgs)); + } - function visitChildren(property) { - return function(node) { - let extraArgs = Array.prototype.slice.call(arguments, 1); + function visitChildren(property) { + return function(node) { + let extraArgs = Array.prototype.slice.call(arguments, 1); - node[property].forEach(child => { - visit.apply(null, [child].concat(extraArgs)); - }); - }; - } + node[property].forEach(child => { + visit.apply(null, [child].concat(extraArgs)); + }); + }; + } - const DEFAULT_FUNCTIONS = { - grammar(node) { - let extraArgs = Array.prototype.slice.call(arguments, 1); + const DEFAULT_FUNCTIONS = { + grammar(node) { + let extraArgs = Array.prototype.slice.call(arguments, 1); - if (node.initializer) { - visit.apply(null, [node.initializer].concat(extraArgs)); - } + if (node.initializer) { + visit.apply(null, [node.initializer].concat(extraArgs)); + } - node.rules.forEach(rule => { - visit.apply(null, [rule].concat(extraArgs)); - }); - }, + node.rules.forEach(rule => { + visit.apply(null, [rule].concat(extraArgs)); + }); + }, - initializer: visitNop, - rule: visitExpression, - named: visitExpression, - choice: visitChildren("alternatives"), - action: visitExpression, - sequence: visitChildren("elements"), - labeled: visitExpression, - text: visitExpression, - simple_and: visitExpression, - simple_not: visitExpression, - optional: visitExpression, - zero_or_more: visitExpression, - one_or_more: visitExpression, - group: visitExpression, - semantic_and: visitNop, - semantic_not: visitNop, - rule_ref: visitNop, - literal: visitNop, - class: visitNop, - any: visitNop - }; + initializer: visitNop, + rule: visitExpression, + named: visitExpression, + choice: visitChildren("alternatives"), + action: visitExpression, + sequence: visitChildren("elements"), + labeled: visitExpression, + text: visitExpression, + simple_and: visitExpression, + simple_not: visitExpression, + optional: visitExpression, + zero_or_more: visitExpression, + one_or_more: visitExpression, + group: visitExpression, + semantic_and: visitNop, + semantic_not: visitNop, + rule_ref: visitNop, + literal: visitNop, + class: visitNop, + any: visitNop + }; - Object.keys(DEFAULT_FUNCTIONS).forEach(type => { - if (!Object.prototype.hasOwnProperty.call(functions, type)) { - functions[type] = DEFAULT_FUNCTIONS[type]; - } - }); + Object.keys(DEFAULT_FUNCTIONS).forEach(type => { + if (!Object.prototype.hasOwnProperty.call(functions, type)) { + functions[type] = DEFAULT_FUNCTIONS[type]; + } + }); - return visit; - } + return visit; + } }; module.exports = visitor; diff --git a/lib/grammar-error.js b/lib/grammar-error.js index ce3fb98..8b698c6 100644 --- a/lib/grammar-error.js +++ b/lib/grammar-error.js @@ -2,15 +2,15 @@ // Thrown when the grammar contains an error. class GrammarError { - constructor(message, location) { - this.name = "GrammarError"; - this.message = message; - this.location = location; + constructor(message, location) { + this.name = "GrammarError"; + this.message = message; + this.location = location; - if (typeof Error.captureStackTrace === "function") { - Error.captureStackTrace(this, GrammarError); - } - } + if (typeof Error.captureStackTrace === "function") { + Error.captureStackTrace(this, GrammarError); + } + } } module.exports = GrammarError; diff --git a/lib/peg.js b/lib/peg.js index a4639fe..4da4195 100644 --- a/lib/peg.js +++ b/lib/peg.js @@ -5,50 +5,50 @@ let compiler = require("./compiler"); let parser = require("./parser"); let peg = { - // PEG.js version (uses semantic versioning). - VERSION: "0.10.0", - - GrammarError: GrammarError, - parser: parser, - compiler: compiler, - - // Generates a parser from a specified grammar and returns it. - // - // The grammar must be a string in the format described by the metagramar in - // the parser.pegjs file. - // - // Throws |peg.parser.SyntaxError| if the grammar contains a syntax error or - // |peg.GrammarError| if it contains a semantic error. Note that not all - // errors are detected during the generation and some may protrude to the - // generated parser and cause its malfunction. - generate(grammar, options) { - options = options !== undefined ? options : {}; - - function convertPasses(passes) { - let converted = {}; - - Object.keys(passes).forEach(stage => { - converted[stage] = Object.keys(passes[stage]) - .map(name => passes[stage][name]); - }); - - return converted; - } - - let plugins = "plugins" in options ? options.plugins : []; - let config = { - parser: peg.parser, - passes: convertPasses(peg.compiler.passes) - }; - - plugins.forEach(p => { p.use(config, options); }); - - return peg.compiler.compile( - config.parser.parse(grammar), - config.passes, - options - ); - } + // PEG.js version (uses semantic versioning). + VERSION: "0.10.0", + + GrammarError: GrammarError, + parser: parser, + compiler: compiler, + + // Generates a parser from a specified grammar and returns it. + // + // The grammar must be a string in the format described by the metagramar in + // the parser.pegjs file. + // + // Throws |peg.parser.SyntaxError| if the grammar contains a syntax error or + // |peg.GrammarError| if it contains a semantic error. Note that not all + // errors are detected during the generation and some may protrude to the + // generated parser and cause its malfunction. + generate(grammar, options) { + options = options !== undefined ? options : {}; + + function convertPasses(passes) { + let converted = {}; + + Object.keys(passes).forEach(stage => { + converted[stage] = Object.keys(passes[stage]) + .map(name => passes[stage][name]); + }); + + return converted; + } + + let plugins = "plugins" in options ? options.plugins : []; + let config = { + parser: peg.parser, + passes: convertPasses(peg.compiler.passes) + }; + + plugins.forEach(p => { p.use(config, options); }); + + return peg.compiler.compile( + config.parser.parse(grammar), + config.passes, + options + ); + } }; module.exports = peg; diff --git a/test/api/generated-parser-api.spec.js b/test/api/generated-parser-api.spec.js index b1827bb..23cc461 100644 --- a/test/api/generated-parser-api.spec.js +++ b/test/api/generated-parser-api.spec.js @@ -9,160 +9,160 @@ let sinon = require("sinon"); let expect = chai.expect; describe("generated parser API", function() { - describe("parse", function() { - it("parses input", function() { - let parser = peg.generate("start = 'a'"); - - expect(parser.parse("a")).to.equal("a"); - }); - - it("throws an exception on syntax error", function() { - let parser = peg.generate("start = 'a'"); - - expect(() => { parser.parse("b"); }).to.throw(); - }); - - describe("start rule", function() { - let parser = peg.generate([ - "a = 'x' { return 'a'; }", - "b = 'x' { return 'b'; }", - "c = 'x' { return 'c'; }" - ].join("\n"), { allowedStartRules: ["b", "c"] }); - - describe("when |startRule| is not set", function() { - it("starts parsing from the first allowed rule", function() { - expect(parser.parse("x")).to.equal("b"); - }); - }); - - describe("when |startRule| is set to an allowed rule", function() { - it("starts parsing from specified rule", function() { - expect(parser.parse("x", { startRule: "b" })).to.equal("b"); - expect(parser.parse("x", { startRule: "c" })).to.equal("c"); - }); - }); - - describe("when |startRule| is set to a disallowed start rule", function() { - it("throws an exception", function() { - expect(() => { parser.parse("x", { startRule: "a" }); }).to.throw(); - }); - }); - }); - - describe("tracing", function() { - let parser = peg.generate([ - "start = a / b", - "a = 'a'", - "b = 'b'" - ].join("\n"), { trace: true }); - - describe("default tracer", function() { - it("traces using console.log (if console is defined)", function() { - let messages = [ - "1:1-1:1 rule.enter start", - "1:1-1:1 rule.enter a", - "1:1-1:1 rule.fail a", - "1:1-1:1 rule.enter b", - "1:1-1:2 rule.match b", - "1:1-1:2 rule.match start" - ]; - - if (typeof console === "object") { - sinon.stub(console, "log"); - } - - try { - parser.parse("b"); - - if (typeof console === "object") { - expect(console.log.callCount).to.equal(messages.length); - messages.forEach((message, index) => { - let call = console.log.getCall(index); - expect(call.calledWithExactly(message)).to.equal(true); - }); - } - } finally { - if (typeof console === "object") { - console.log.restore(); - } - } - }); - }); - - describe("custom tracers", function() { - describe("trace", function() { - it("receives tracing events", function() { - let events = [ - { - type: "rule.enter", - rule: "start", - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 0, line: 1, column: 1 } - } - }, - { - type: "rule.enter", - rule: "a", - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 0, line: 1, column: 1 } - } - }, - { - type: "rule.fail", - rule: "a", - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 0, line: 1, column: 1 } - } - }, - { - type: "rule.enter", - rule: "b", - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 0, line: 1, column: 1 } - } - }, - { - type: "rule.match", - rule: "b", - result: "b", - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 1, line: 1, column: 2 } - } - }, - { - type: "rule.match", - rule: "start", - result: "b", - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 1, line: 1, column: 2 } - } - } - ]; - - let tracer = { trace: sinon.spy() }; - - parser.parse("b", { tracer: tracer }); - - expect(tracer.trace.callCount).to.equal(events.length); - events.forEach((event, index) => { - let call = tracer.trace.getCall(index); - expect(call.calledWithExactly(event)).to.equal(true); - }); - }); - }); - }); - }); - - it("accepts custom options", function() { - let parser = peg.generate("start = 'a'"); - - parser.parse("a", { foo: 42 }); - }); - }); + describe("parse", function() { + it("parses input", function() { + let parser = peg.generate("start = 'a'"); + + expect(parser.parse("a")).to.equal("a"); + }); + + it("throws an exception on syntax error", function() { + let parser = peg.generate("start = 'a'"); + + expect(() => { parser.parse("b"); }).to.throw(); + }); + + describe("start rule", function() { + let parser = peg.generate([ + "a = 'x' { return 'a'; }", + "b = 'x' { return 'b'; }", + "c = 'x' { return 'c'; }" + ].join("\n"), { allowedStartRules: ["b", "c"] }); + + describe("when |startRule| is not set", function() { + it("starts parsing from the first allowed rule", function() { + expect(parser.parse("x")).to.equal("b"); + }); + }); + + describe("when |startRule| is set to an allowed rule", function() { + it("starts parsing from specified rule", function() { + expect(parser.parse("x", { startRule: "b" })).to.equal("b"); + expect(parser.parse("x", { startRule: "c" })).to.equal("c"); + }); + }); + + describe("when |startRule| is set to a disallowed start rule", function() { + it("throws an exception", function() { + expect(() => { parser.parse("x", { startRule: "a" }); }).to.throw(); + }); + }); + }); + + describe("tracing", function() { + let parser = peg.generate([ + "start = a / b", + "a = 'a'", + "b = 'b'" + ].join("\n"), { trace: true }); + + describe("default tracer", function() { + it("traces using console.log (if console is defined)", function() { + let messages = [ + "1:1-1:1 rule.enter start", + "1:1-1:1 rule.enter a", + "1:1-1:1 rule.fail a", + "1:1-1:1 rule.enter b", + "1:1-1:2 rule.match b", + "1:1-1:2 rule.match start" + ]; + + if (typeof console === "object") { + sinon.stub(console, "log"); + } + + try { + parser.parse("b"); + + if (typeof console === "object") { + expect(console.log.callCount).to.equal(messages.length); + messages.forEach((message, index) => { + let call = console.log.getCall(index); + expect(call.calledWithExactly(message)).to.equal(true); + }); + } + } finally { + if (typeof console === "object") { + console.log.restore(); + } + } + }); + }); + + describe("custom tracers", function() { + describe("trace", function() { + it("receives tracing events", function() { + let events = [ + { + type: "rule.enter", + rule: "start", + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 0, line: 1, column: 1 } + } + }, + { + type: "rule.enter", + rule: "a", + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 0, line: 1, column: 1 } + } + }, + { + type: "rule.fail", + rule: "a", + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 0, line: 1, column: 1 } + } + }, + { + type: "rule.enter", + rule: "b", + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 0, line: 1, column: 1 } + } + }, + { + type: "rule.match", + rule: "b", + result: "b", + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 1, line: 1, column: 2 } + } + }, + { + type: "rule.match", + rule: "start", + result: "b", + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 1, line: 1, column: 2 } + } + } + ]; + + let tracer = { trace: sinon.spy() }; + + parser.parse("b", { tracer: tracer }); + + expect(tracer.trace.callCount).to.equal(events.length); + events.forEach((event, index) => { + let call = tracer.trace.getCall(index); + expect(call.calledWithExactly(event)).to.equal(true); + }); + }); + }); + }); + }); + + it("accepts custom options", function() { + let parser = peg.generate("start = 'a'"); + + parser.parse("a", { foo: 42 }); + }); + }); }); diff --git a/test/api/pegjs-api.spec.js b/test/api/pegjs-api.spec.js index 75a4cbb..df2f4bd 100644 --- a/test/api/pegjs-api.spec.js +++ b/test/api/pegjs-api.spec.js @@ -7,194 +7,194 @@ let sinon = require("sinon"); let expect = chai.expect; describe("PEG.js API", function() { - describe("generate", function() { - it("generates a parser", function() { - let parser = peg.generate("start = 'a'"); - - expect(parser).to.be.an("object"); - expect(parser.parse("a")).to.equal("a"); - }); - - it("throws an exception on syntax error", function() { - expect(() => { peg.generate("start = @"); }).to.throw(); - }); - - it("throws an exception on semantic error", function() { - expect(() => { peg.generate("start = undefined"); }).to.throw(); - }); - - describe("allowed start rules", function() { - let grammar = [ - "a = 'x'", - "b = 'x'", - "c = 'x'" - ].join("\n"); - - // The |allowedStartRules| option is implemented separately for each - // optimization mode, so we need to test it in both. - - describe("when optimizing for parsing speed", function() { - describe("when |allowedStartRules| is not set", function() { - it("generated parser can start only from the first rule", function() { - let parser = peg.generate(grammar, { optimize: "speed" }); - - expect(parser.parse("x", { startRule: "a" })).to.equal("x"); - expect(() => { parser.parse("x", { startRule: "b" }); }).to.throw(); - expect(() => { parser.parse("x", { startRule: "c" }); }).to.throw(); - }); - }); - - describe("when |allowedStartRules| is set", function() { - it("generated parser can start only from specified rules", function() { - let parser = peg.generate(grammar, { - optimize: "speed", - allowedStartRules: ["b", "c"] - }); - - expect(() => { parser.parse("x", { startRule: "a" }); }).to.throw(); - expect(parser.parse("x", { startRule: "b" })).to.equal("x"); - expect(parser.parse("x", { startRule: "c" })).to.equal("x"); - }); - }); - }); - - describe("when optimizing for code size", function() { - describe("when |allowedStartRules| is not set", function() { - it("generated parser can start only from the first rule", function() { - let parser = peg.generate(grammar, { optimize: "size" }); - - expect(parser.parse("x", { startRule: "a" })).to.equal("x"); - expect(() => { parser.parse("x", { startRule: "b" }); }).to.throw(); - expect(() => { parser.parse("x", { startRule: "c" }); }).to.throw(); - }); - }); - - describe("when |allowedStartRules| is set", function() { - it("generated parser can start only from specified rules", function() { - let parser = peg.generate(grammar, { - optimize: "size", - allowedStartRules: ["b", "c"] - }); - - expect(() => { parser.parse("x", { startRule: "a" }); }).to.throw(); - expect(parser.parse("x", { startRule: "b" })).to.equal("x"); - expect(parser.parse("x", { startRule: "c" })).to.equal("x"); - }); - }); - }); - }); - - describe("intermediate results caching", function() { - let grammar = [ - "{ var n = 0; }", - "start = (a 'b') / (a 'c') { return n; }", - "a = 'a' { n++; }" - ].join("\n"); - - describe("when |cache| is not set", function() { - it("generated parser doesn't cache intermediate parse results", function() { - let parser = peg.generate(grammar); - - expect(parser.parse("ac")).to.equal(2); - }); - }); - - describe("when |cache| is set to |false|", function() { - it("generated parser doesn't cache intermediate parse results", function() { - let parser = peg.generate(grammar, { cache: false }); - - expect(parser.parse("ac")).to.equal(2); - }); - }); - - describe("when |cache| is set to |true|", function() { - it("generated parser caches intermediate parse results", function() { - let parser = peg.generate(grammar, { cache: true }); - - expect(parser.parse("ac")).to.equal(1); - }); - }); - }); - - describe("tracing", function() { - let grammar = "start = 'a'"; - - describe("when |trace| is not set", function() { - it("generated parser doesn't trace", function() { - let parser = peg.generate(grammar); - let tracer = { trace: sinon.spy() }; - - parser.parse("a", { tracer: tracer }); - - expect(tracer.trace.called).to.equal(false); - }); - }); - - describe("when |trace| is set to |false|", function() { - it("generated parser doesn't trace", function() { - let parser = peg.generate(grammar, { trace: false }); - let tracer = { trace: sinon.spy() }; - - parser.parse("a", { tracer: tracer }); - - expect(tracer.trace.called).to.equal(false); - }); - }); - - describe("when |trace| is set to |true|", function() { - it("generated parser traces", function() { - let parser = peg.generate(grammar, { trace: true }); - let tracer = { trace: sinon.spy() }; - - parser.parse("a", { tracer: tracer }); - - expect(tracer.trace.called).to.equal(true); - }); - }); - }); - - // The |optimize| option isn't tested because there is no meaningful way to - // write the tests without turning this into a performance test. - - describe("output", function() { - let grammar = "start = 'a'"; - - describe("when |output| is not set", function() { - it("returns generated parser object", function() { - let parser = peg.generate(grammar); - - expect(parser).to.be.an("object"); - expect(parser.parse("a")).to.equal("a"); - }); - }); - - describe("when |output| is set to |\"parser\"|", function() { - it("returns generated parser object", function() { - let parser = peg.generate(grammar, { output: "parser" }); - - expect(parser).to.be.an("object"); - expect(parser.parse("a")).to.equal("a"); - }); - }); - - describe("when |output| is set to |\"source\"|", function() { - it("returns generated parser source code", function() { - let source = peg.generate(grammar, { output: "source" }); - - expect(source).to.be.a("string"); - expect(eval(source).parse("a")).to.equal("a"); - }); - }); - }); + describe("generate", function() { + it("generates a parser", function() { + let parser = peg.generate("start = 'a'"); + + expect(parser).to.be.an("object"); + expect(parser.parse("a")).to.equal("a"); + }); + + it("throws an exception on syntax error", function() { + expect(() => { peg.generate("start = @"); }).to.throw(); + }); + + it("throws an exception on semantic error", function() { + expect(() => { peg.generate("start = undefined"); }).to.throw(); + }); + + describe("allowed start rules", function() { + let grammar = [ + "a = 'x'", + "b = 'x'", + "c = 'x'" + ].join("\n"); + + // The |allowedStartRules| option is implemented separately for each + // optimization mode, so we need to test it in both. + + describe("when optimizing for parsing speed", function() { + describe("when |allowedStartRules| is not set", function() { + it("generated parser can start only from the first rule", function() { + let parser = peg.generate(grammar, { optimize: "speed" }); + + expect(parser.parse("x", { startRule: "a" })).to.equal("x"); + expect(() => { parser.parse("x", { startRule: "b" }); }).to.throw(); + expect(() => { parser.parse("x", { startRule: "c" }); }).to.throw(); + }); + }); + + describe("when |allowedStartRules| is set", function() { + it("generated parser can start only from specified rules", function() { + let parser = peg.generate(grammar, { + optimize: "speed", + allowedStartRules: ["b", "c"] + }); + + expect(() => { parser.parse("x", { startRule: "a" }); }).to.throw(); + expect(parser.parse("x", { startRule: "b" })).to.equal("x"); + expect(parser.parse("x", { startRule: "c" })).to.equal("x"); + }); + }); + }); + + describe("when optimizing for code size", function() { + describe("when |allowedStartRules| is not set", function() { + it("generated parser can start only from the first rule", function() { + let parser = peg.generate(grammar, { optimize: "size" }); + + expect(parser.parse("x", { startRule: "a" })).to.equal("x"); + expect(() => { parser.parse("x", { startRule: "b" }); }).to.throw(); + expect(() => { parser.parse("x", { startRule: "c" }); }).to.throw(); + }); + }); + + describe("when |allowedStartRules| is set", function() { + it("generated parser can start only from specified rules", function() { + let parser = peg.generate(grammar, { + optimize: "size", + allowedStartRules: ["b", "c"] + }); + + expect(() => { parser.parse("x", { startRule: "a" }); }).to.throw(); + expect(parser.parse("x", { startRule: "b" })).to.equal("x"); + expect(parser.parse("x", { startRule: "c" })).to.equal("x"); + }); + }); + }); + }); + + describe("intermediate results caching", function() { + let grammar = [ + "{ var n = 0; }", + "start = (a 'b') / (a 'c') { return n; }", + "a = 'a' { n++; }" + ].join("\n"); + + describe("when |cache| is not set", function() { + it("generated parser doesn't cache intermediate parse results", function() { + let parser = peg.generate(grammar); + + expect(parser.parse("ac")).to.equal(2); + }); + }); + + describe("when |cache| is set to |false|", function() { + it("generated parser doesn't cache intermediate parse results", function() { + let parser = peg.generate(grammar, { cache: false }); + + expect(parser.parse("ac")).to.equal(2); + }); + }); + + describe("when |cache| is set to |true|", function() { + it("generated parser caches intermediate parse results", function() { + let parser = peg.generate(grammar, { cache: true }); + + expect(parser.parse("ac")).to.equal(1); + }); + }); + }); + + describe("tracing", function() { + let grammar = "start = 'a'"; + + describe("when |trace| is not set", function() { + it("generated parser doesn't trace", function() { + let parser = peg.generate(grammar); + let tracer = { trace: sinon.spy() }; + + parser.parse("a", { tracer: tracer }); + + expect(tracer.trace.called).to.equal(false); + }); + }); + + describe("when |trace| is set to |false|", function() { + it("generated parser doesn't trace", function() { + let parser = peg.generate(grammar, { trace: false }); + let tracer = { trace: sinon.spy() }; + + parser.parse("a", { tracer: tracer }); + + expect(tracer.trace.called).to.equal(false); + }); + }); + + describe("when |trace| is set to |true|", function() { + it("generated parser traces", function() { + let parser = peg.generate(grammar, { trace: true }); + let tracer = { trace: sinon.spy() }; + + parser.parse("a", { tracer: tracer }); + + expect(tracer.trace.called).to.equal(true); + }); + }); + }); + + // The |optimize| option isn't tested because there is no meaningful way to + // write the tests without turning this into a performance test. + + describe("output", function() { + let grammar = "start = 'a'"; + + describe("when |output| is not set", function() { + it("returns generated parser object", function() { + let parser = peg.generate(grammar); + + expect(parser).to.be.an("object"); + expect(parser.parse("a")).to.equal("a"); + }); + }); + + describe("when |output| is set to |\"parser\"|", function() { + it("returns generated parser object", function() { + let parser = peg.generate(grammar, { output: "parser" }); + + expect(parser).to.be.an("object"); + expect(parser.parse("a")).to.equal("a"); + }); + }); + + describe("when |output| is set to |\"source\"|", function() { + it("returns generated parser source code", function() { + let source = peg.generate(grammar, { output: "source" }); + + expect(source).to.be.a("string"); + expect(eval(source).parse("a")).to.equal("a"); + }); + }); + }); - // The |format|, |exportVars|, and |dependencies| options are not tested - // becasue there is no meaningful way to thest their effects without turning - // this into an integration test. + // The |format|, |exportVars|, and |dependencies| options are not tested + // becasue there is no meaningful way to thest their effects without turning + // this into an integration test. - // The |plugins| option is tested in plugin API tests. + // The |plugins| option is tested in plugin API tests. - it("accepts custom options", function() { - peg.generate("start = 'a'", { foo: 42 }); - }); - }); + it("accepts custom options", function() { + peg.generate("start = 'a'", { foo: 42 }); + }); + }); }); diff --git a/test/api/plugin-api.spec.js b/test/api/plugin-api.spec.js index 84a5bf4..d1738e6 100644 --- a/test/api/plugin-api.spec.js +++ b/test/api/plugin-api.spec.js @@ -6,123 +6,123 @@ let peg = require("../../lib/peg"); let expect = chai.expect; describe("plugin API", function() { - describe("use", function() { - let grammar = "start = 'a'"; - - it("is called for each plugin", function() { - let pluginsUsed = [false, false, false]; - let plugins = [ - { use() { pluginsUsed[0] = true; } }, - { use() { pluginsUsed[1] = true; } }, - { use() { pluginsUsed[2] = true; } } - ]; - - peg.generate(grammar, { plugins: plugins }); - - expect(pluginsUsed).to.deep.equal([true, true, true]); - }); - - it("receives configuration", function() { - let plugin = { - use(config) { - expect(config).to.be.an("object"); - - expect(config.parser).to.be.an("object"); - expect(config.parser.parse("start = 'a'")).to.be.an("object"); - - expect(config.passes).to.be.an("object"); - - expect(config.passes.check).to.be.an("array"); - config.passes.check.forEach(pass => { - expect(pass).to.be.a("function"); - }); - - expect(config.passes.transform).to.be.an("array"); - config.passes.transform.forEach(pass => { - expect(pass).to.be.a("function"); - }); - - expect(config.passes.generate).to.be.an("array"); - config.passes.generate.forEach(pass => { - expect(pass).to.be.a("function"); - }); - } - }; - - peg.generate(grammar, { plugins: [plugin] }); - }); - - it("receives options", function() { - let plugin = { - use(config, options) { - expect(options).to.equal(generateOptions); - } - }; - let generateOptions = { plugins: [plugin], foo: 42 }; - - peg.generate(grammar, generateOptions); - }); - - it("can replace parser", function() { - let plugin = { - use(config) { - let parser = peg.generate([ - "start = .* {", - " return {", - " type: 'grammar',", - " rules: [", - " {", - " type: 'rule',", - " name: 'start',", - " expression: { type: 'literal', value: text(), ignoreCase: false }", - " }", - " ]", - " };", - "}" - ].join("\n")); - - config.parser = parser; - } - }; - let parser = peg.generate("a", { plugins: [plugin] }); - - expect(parser.parse("a")).to.equal("a"); - }); - - it("can change compiler passes", function() { - let plugin = { - use(config) { - function pass(ast) { - ast.code = "({ parse: function() { return 42; } })"; - } - - config.passes.generate = [pass]; - } - }; - let parser = peg.generate(grammar, { plugins: [plugin] }); - - expect(parser.parse("a")).to.equal(42); - }); - - it("can change options", function() { - let grammar = [ - "a = 'x'", - "b = 'x'", - "c = 'x'" - ].join("\n"); - let plugin = { - use(config, options) { - options.allowedStartRules = ["b", "c"]; - } - }; - let parser = peg.generate(grammar, { - allowedStartRules: ["a"], - plugins: [plugin] - }); - - expect(() => { parser.parse("x", { startRule: "a" }); }).to.throw(); - expect(parser.parse("x", { startRule: "b" })).to.equal("x"); - expect(parser.parse("x", { startRule: "c" })).to.equal("x"); - }); - }); + describe("use", function() { + let grammar = "start = 'a'"; + + it("is called for each plugin", function() { + let pluginsUsed = [false, false, false]; + let plugins = [ + { use() { pluginsUsed[0] = true; } }, + { use() { pluginsUsed[1] = true; } }, + { use() { pluginsUsed[2] = true; } } + ]; + + peg.generate(grammar, { plugins: plugins }); + + expect(pluginsUsed).to.deep.equal([true, true, true]); + }); + + it("receives configuration", function() { + let plugin = { + use(config) { + expect(config).to.be.an("object"); + + expect(config.parser).to.be.an("object"); + expect(config.parser.parse("start = 'a'")).to.be.an("object"); + + expect(config.passes).to.be.an("object"); + + expect(config.passes.check).to.be.an("array"); + config.passes.check.forEach(pass => { + expect(pass).to.be.a("function"); + }); + + expect(config.passes.transform).to.be.an("array"); + config.passes.transform.forEach(pass => { + expect(pass).to.be.a("function"); + }); + + expect(config.passes.generate).to.be.an("array"); + config.passes.generate.forEach(pass => { + expect(pass).to.be.a("function"); + }); + } + }; + + peg.generate(grammar, { plugins: [plugin] }); + }); + + it("receives options", function() { + let plugin = { + use(config, options) { + expect(options).to.equal(generateOptions); + } + }; + let generateOptions = { plugins: [plugin], foo: 42 }; + + peg.generate(grammar, generateOptions); + }); + + it("can replace parser", function() { + let plugin = { + use(config) { + let parser = peg.generate([ + "start = .* {", + " return {", + " type: 'grammar',", + " rules: [", + " {", + " type: 'rule',", + " name: 'start',", + " expression: { type: 'literal', value: text(), ignoreCase: false }", + " }", + " ]", + " };", + "}" + ].join("\n")); + + config.parser = parser; + } + }; + let parser = peg.generate("a", { plugins: [plugin] }); + + expect(parser.parse("a")).to.equal("a"); + }); + + it("can change compiler passes", function() { + let plugin = { + use(config) { + function pass(ast) { + ast.code = "({ parse: function() { return 42; } })"; + } + + config.passes.generate = [pass]; + } + }; + let parser = peg.generate(grammar, { plugins: [plugin] }); + + expect(parser.parse("a")).to.equal(42); + }); + + it("can change options", function() { + let grammar = [ + "a = 'x'", + "b = 'x'", + "c = 'x'" + ].join("\n"); + let plugin = { + use(config, options) { + options.allowedStartRules = ["b", "c"]; + } + }; + let parser = peg.generate(grammar, { + allowedStartRules: ["a"], + plugins: [plugin] + }); + + expect(() => { parser.parse("x", { startRule: "a" }); }).to.throw(); + expect(parser.parse("x", { startRule: "b" })).to.equal("x"); + expect(parser.parse("x", { startRule: "c" })).to.equal("x"); + }); + }); }); diff --git a/test/behavior/generated-parser-behavior.spec.js b/test/behavior/generated-parser-behavior.spec.js index 0ef1d56..cb71b99 100644 --- a/test/behavior/generated-parser-behavior.spec.js +++ b/test/behavior/generated-parser-behavior.spec.js @@ -9,1548 +9,1548 @@ let sinon = require("sinon"); let expect = chai.expect; describe("generated parser behavior", function() { - function varyOptimizationOptions(block) { - function clone(object) { - let result = {}; - - Object.keys(object).forEach(key => { - result[key] = object[key]; - }); - - return result; - } - - let optionsVariants = [ - { cache: false, optimize: "speed", trace: false }, - { cache: false, optimize: "speed", trace: true }, - { cache: false, optimize: "size", trace: false }, - { cache: false, optimize: "size", trace: true }, - { cache: true, optimize: "speed", trace: false }, - { cache: true, optimize: "speed", trace: true }, - { cache: true, optimize: "size", trace: false }, - { cache: true, optimize: "size", trace: true } - ]; - - optionsVariants.forEach(variant => { - describe( - "with options " + chai.util.inspect(variant), - function() { block(clone(variant)); } - ); - }); - } - - function withConsoleStub(block) { - if (typeof console === "object") { - sinon.stub(console, "log"); - } - - try { - return block(); - } finally { - if (typeof console === "object") { - console.log.restore(); - } - } - } - - function helpers(chai, utils) { - let Assertion = chai.Assertion; - - Assertion.addMethod("parse", function(input, expected, options) { - options = options !== undefined ? options : {}; - - let result = withConsoleStub(() => - utils.flag(this, "object").parse(input, options) - ); - - if (expected !== undefined) { - this.assert( - utils.eql(result, expected), - "expected #{this} to parse input as #{exp} but got #{act}", - "expected #{this} to not parse input as #{exp}", - expected, - result, - !utils.flag(this, "negate") - ); - } - }); - - Assertion.addMethod("failToParse", function(input, props, options) { - options = options !== undefined ? options : {}; - - let passed, result; - - try { - result = withConsoleStub(() => - utils.flag(this, "object").parse(input, options) - ); - passed = true; - } catch (e) { - result = e; - passed = false; - } - - this.assert( - !passed, - "expected #{this} to fail to parse input but got #{act}", - "expected #{this} to not fail to parse input but #{act} was thrown", - null, - result - ); - - if (!passed && props !== undefined) { - Object.keys(props).forEach(key => { - new Assertion(result).to.have.property(key) - .that.is.deep.equal(props[key]); - }); - } - }); - } - - // Helper activation needs to put inside a |beforeEach| block because the - // helpers conflict with the ones in test/unit/parser.spec.js. - beforeEach(function() { - chai.use(helpers); - }); - - varyOptimizationOptions(function(options) { - describe("initializer", function() { - it("executes the code before parsing starts", function() { - let parser = peg.generate([ - "{ var result = 42; }", - "start = 'a' { return result; }" - ].join("\n"), options); - - expect(parser).to.parse("a", 42); - }); - - describe("available variables and functions", function() { - it("|options| contains options", function() { - let parser = peg.generate([ - "{ var result = options; }", - "start = 'a' { return result; }" - ].join("\n"), options); - - expect(parser).to.parse("a", { a: 42 }, { a: 42 }); - }); - }); - }); - - describe("rule", function() { - if (options.cache) { - it("caches rule match results", function() { - let parser = peg.generate([ - "{ var n = 0; }", - "start = (a 'b') / (a 'c') { return n; }", - "a = 'a' { n++; }" - ].join("\n"), options); - - expect(parser).to.parse("ac", 1); - }); - } else { - it("doesn't cache rule match results", function() { - let parser = peg.generate([ - "{ var n = 0; }", - "start = (a 'b') / (a 'c') { return n; }", - "a = 'a' { n++; }" - ].join("\n"), options); - - expect(parser).to.parse("ac", 2); - }); - } - - describe("when the expression matches", function() { - it("returns its match result", function() { - let parser = peg.generate("start = 'a'"); - - expect(parser).to.parse("a", "a"); - }); - }); - - describe("when the expression doesn't match", function() { - describe("without display name", function() { - it("reports match failure and doesn't record any expectation", function() { - let parser = peg.generate("start = 'a'"); - - expect(parser).to.failToParse("b", { - expected: [{ type: "literal", text: "a", ignoreCase: false }] - }); - }); - }); - - describe("with display name", function() { - it("reports match failure and records an expectation of type \"other\"", function() { - let parser = peg.generate("start 'start' = 'a'"); - - expect(parser).to.failToParse("b", { - expected: [{ type: "other", description: "start" }] - }); - }); - - it("discards any expectations recorded when matching the expression", function() { - let parser = peg.generate("start 'start' = 'a'"); - - expect(parser).to.failToParse("b", { - expected: [{ type: "other", description: "start" }] - }); - }); - }); - }); - }); - - describe("literal", function() { - describe("matching", function() { - it("matches empty literals", function() { - let parser = peg.generate("start = ''", options); - - expect(parser).to.parse(""); - }); - - it("matches one-character literals", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.parse("a"); - expect(parser).to.failToParse("b"); - }); - - it("matches multi-character literals", function() { - let parser = peg.generate("start = 'abcd'", options); - - expect(parser).to.parse("abcd"); - expect(parser).to.failToParse("efgh"); - }); - - it("is case sensitive without the \"i\" flag", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.parse("a"); - expect(parser).to.failToParse("A"); - }); - - it("is case insensitive with the \"i\" flag", function() { - let parser = peg.generate("start = 'a'i", options); - - expect(parser).to.parse("a"); - expect(parser).to.parse("A"); - }); - }); - - describe("when it matches", function() { - it("returns the matched text", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.parse("a", "a"); - }); - - it("consumes the matched text", function() { - let parser = peg.generate("start = 'a' .", options); - - expect(parser).to.parse("ab"); - }); - }); - - describe("when it doesn't match", function() { - it("reports match failure and records an expectation of type \"literal\"", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("b", { - expected: [{ type: "literal", text: "a", ignoreCase: false }] - }); - }); - }); - }); - - describe("character class", function() { - describe("matching", function() { - it("matches empty classes", function() { - let parser = peg.generate("start = []", options); - - expect(parser).to.failToParse("a"); - }); - - it("matches classes with a character list", function() { - let parser = peg.generate("start = [abc]", options); - - expect(parser).to.parse("a"); - expect(parser).to.parse("b"); - expect(parser).to.parse("c"); - expect(parser).to.failToParse("d"); - }); - - it("matches classes with a character range", function() { - let parser = peg.generate("start = [a-c]", options); - - expect(parser).to.parse("a"); - expect(parser).to.parse("b"); - expect(parser).to.parse("c"); - expect(parser).to.failToParse("d"); - }); - - it("matches inverted classes", function() { - let parser = peg.generate("start = [^a]", options); - - expect(parser).to.failToParse("a"); - expect(parser).to.parse("b"); - }); - - it("is case sensitive without the \"i\" flag", function() { - let parser = peg.generate("start = [a]", options); - - expect(parser).to.parse("a"); - expect(parser).to.failToParse("A"); - }); - - it("is case insensitive with the \"i\" flag", function() { - let parser = peg.generate("start = [a]i", options); - - expect(parser).to.parse("a"); - expect(parser).to.parse("A"); - }); - }); - - describe("when it matches", function() { - it("returns the matched character", function() { - let parser = peg.generate("start = [a]", options); - - expect(parser).to.parse("a", "a"); - }); - - it("consumes the matched character", function() { - let parser = peg.generate("start = [a] .", options); - - expect(parser).to.parse("ab"); - }); - }); - - describe("when it doesn't match", function() { - it("reports match failure and records an expectation of type \"class\"", function() { - let parser = peg.generate("start = [a]", options); - - expect(parser).to.failToParse("b", { - expected: [{ type: "class", parts: ["a"], inverted: false, ignoreCase: false }] - }); - }); - }); - }); - - describe("dot", function() { - describe("matching", function() { - it("matches any character", function() { - let parser = peg.generate("start = .", options); - - expect(parser).to.parse("a"); - expect(parser).to.parse("b"); - expect(parser).to.parse("c"); - }); - }); - - describe("when it matches", function() { - it("returns the matched character", function() { - let parser = peg.generate("start = .", options); - - expect(parser).to.parse("a", "a"); - }); - - it("consumes the matched character", function() { - let parser = peg.generate("start = . .", options); - - expect(parser).to.parse("ab"); - }); - }); - - describe("when it doesn't match", function() { - it("reports match failure and records an expectation of type \"any\"", function() { - let parser = peg.generate("start = .", options); - - expect(parser).to.failToParse("", { - expected: [{ type: "any" }] - }); - }); - }); - }); - - describe("rule reference", function() { - describe("when referenced rule's expression matches", function() { - it("returns its result", function() { - let parser = peg.generate([ - "start = a", - "a = 'a'" - ].join("\n"), options); - - expect(parser).to.parse("a", "a"); - }); - }); - - describe("when referenced rule's expression doesn't match", function() { - it("reports match failure", function() { - let parser = peg.generate([ - "start = a", - "a = 'a'" - ].join("\n"), options); - - expect(parser).to.failToParse("b"); - }); - }); - }); - - describe("positive semantic predicate", function() { - describe("when the code returns a truthy value", function() { - it("returns |undefined|", function() { - // The |""| is needed so that the parser doesn't return just - // |undefined| which we can't compare against in |toParse| due to the - // way optional parameters work. - let parser = peg.generate("start = &{ return true; } ''", options); - - expect(parser).to.parse("", [undefined, ""]); - }); - }); - - describe("when the code returns a falsey value", function() { - it("reports match failure", function() { - let parser = peg.generate("start = &{ return false; }", options); - - expect(parser).to.failToParse(""); - }); - }); - - describe("label variables", function() { - describe("in containing sequence", function() { - it("can access variables defined by preceding labeled elements", function() { - let parser = peg.generate( - "start = a:'a' &{ return a === 'a'; }", - options - ); - - expect(parser).to.parse("a"); - }); - - it("cannot access variable defined by labeled predicate element", function() { - let parser = peg.generate( - "start = 'a' b:&{ return b === undefined; } 'c'", - options - ); - - expect(parser).to.failToParse("ac"); - }); - - it("cannot access variables defined by following labeled elements", function() { - let parser = peg.generate( - "start = &{ return a === 'a'; } a:'a'", - options - ); - - expect(parser).to.failToParse("a"); - }); - - it("cannot access variables defined by subexpressions", function() { - let testcases = [ - { - grammar: "start = (a:'a') &{ return a === 'a'; }", - input: "a" - }, - { - grammar: "start = (a:'a')? &{ return a === 'a'; }", - input: "a" - }, - { - grammar: "start = (a:'a')* &{ return a === 'a'; }", - input: "a" - }, - { - grammar: "start = (a:'a')+ &{ return a === 'a'; }", - input: "a" - }, - { - grammar: "start = $(a:'a') &{ return a === 'a'; }", - input: "a" - }, - { - grammar: "start = &(a:'a') 'a' &{ return a === 'a'; }", - input: "a" - }, - { - grammar: "start = !(a:'a') 'b' &{ return a === 'a'; }", - input: "b" - }, - { - grammar: "start = b:(a:'a') &{ return a === 'a'; }", - input: "a" - }, - { - grammar: "start = ('a' b:'b' 'c') &{ return b === 'b'; }", - input: "abc" - }, - { - grammar: "start = (a:'a' { return a; }) &{ return a === 'a'; }", - input: "a" - }, - { - grammar: "start = ('a' / b:'b' / 'c') &{ return b === 'b'; }", - input: "b" - } - ]; - - testcases.forEach(testcase => { - let parser = peg.generate(testcase.grammar, options); - expect(parser).to.failToParse(testcase.input); - }); - }); - }); - - describe("in outer sequence", function() { - it("can access variables defined by preceding labeled elements", function() { - let parser = peg.generate( - "start = a:'a' ('b' &{ return a === 'a'; })", - options - ); - - expect(parser).to.parse("ab"); - }); - - it("cannot access variable defined by labeled predicate element", function() { - let parser = peg.generate( - "start = 'a' b:('b' &{ return b === undefined; }) 'c'", - options - ); - - expect(parser).to.failToParse("abc"); - }); - - it("cannot access variables defined by following labeled elements", function() { - let parser = peg.generate( - "start = ('a' &{ return b === 'b'; }) b:'b'", - options - ); - - expect(parser).to.failToParse("ab"); - }); - }); - }); - - describe("initializer variables & functions", function() { - it("can access variables defined in the initializer", function() { - let parser = peg.generate([ - "{ var v = 42 }", - "start = &{ return v === 42; }" - ].join("\n"), options); - - expect(parser).to.parse(""); - }); - - it("can access functions defined in the initializer", function() { - let parser = peg.generate([ - "{ function f() { return 42; } }", - "start = &{ return f() === 42; }" - ].join("\n"), options); - - expect(parser).to.parse(""); - }); - }); - - describe("available variables & functions", function() { - it("|options| contains options", function() { - let parser = peg.generate([ - "{ var result; }", - "start = &{ result = options; return true; } { return result; }" - ].join("\n"), options); - - expect(parser).to.parse("", { a: 42 }, { a: 42 }); - }); - - it("|location| returns current location info", function() { - let parser = peg.generate([ - "{ var result; }", - "start = line (nl+ line)* { return result; }", - "line = thing (' '+ thing)*", - "thing = digit / mark", - "digit = [0-9]", - "mark = &{ result = location(); return true; } 'x'", - "nl = '\\r'? '\\n'" - ].join("\n"), options); - - expect(parser).to.parse("1\n2\n\n3\n\n\n4 5 x", { - start: { offset: 13, line: 7, column: 5 }, - end: { offset: 13, line: 7, column: 5 } - }); - - // Newline representations - expect(parser).to.parse("1\nx", { // Unix - start: { offset: 2, line: 2, column: 1 }, - end: { offset: 2, line: 2, column: 1 } - }); - expect(parser).to.parse("1\r\nx", { // Windows - start: { offset: 3, line: 2, column: 1 }, - end: { offset: 3, line: 2, column: 1 } - }); - }); - }); - }); - - describe("negative semantic predicate", function() { - describe("when the code returns a falsey value", function() { - it("returns |undefined|", function() { - // The |""| is needed so that the parser doesn't return just - // |undefined| which we can't compare against in |toParse| due to the - // way optional parameters work. - let parser = peg.generate("start = !{ return false; } ''", options); - - expect(parser).to.parse("", [undefined, ""]); - }); - }); - - describe("when the code returns a truthy value", function() { - it("reports match failure", function() { - let parser = peg.generate("start = !{ return true; }", options); - - expect(parser).to.failToParse(""); - }); - }); - - describe("label variables", function() { - describe("in containing sequence", function() { - it("can access variables defined by preceding labeled elements", function() { - let parser = peg.generate( - "start = a:'a' !{ return a !== 'a'; }", - options - ); - - expect(parser).to.parse("a"); - }); - - it("cannot access variable defined by labeled predicate element", function() { - let parser = peg.generate( - "start = 'a' b:!{ return b !== undefined; } 'c'", - options - ); - - expect(parser).to.failToParse("ac"); - }); - - it("cannot access variables defined by following labeled elements", function() { - let parser = peg.generate( - "start = !{ return a !== 'a'; } a:'a'", - options - ); - - expect(parser).to.failToParse("a"); - }); - - it("cannot access variables defined by subexpressions", function() { - let testcases = [ - { - grammar: "start = (a:'a') !{ return a !== 'a'; }", - input: "a" - }, - { - grammar: "start = (a:'a')? !{ return a !== 'a'; }", - input: "a" - }, - { - grammar: "start = (a:'a')* !{ return a !== 'a'; }", - input: "a" - }, - { - grammar: "start = (a:'a')+ !{ return a !== 'a'; }", - input: "a" - }, - { - grammar: "start = $(a:'a') !{ return a !== 'a'; }", - input: "a" - }, - { - grammar: "start = &(a:'a') 'a' !{ return a !== 'a'; }", - input: "a" - }, - { - grammar: "start = !(a:'a') 'b' !{ return a !== 'a'; }", - input: "b" - }, - { - grammar: "start = b:(a:'a') !{ return a !== 'a'; }", - input: "a" - }, - { - grammar: "start = ('a' b:'b' 'c') !{ return b !== 'b'; }", - input: "abc" - }, - { - grammar: "start = (a:'a' { return a; }) !{ return a !== 'a'; }", - input: "a" - }, - { - grammar: "start = ('a' / b:'b' / 'c') !{ return b !== 'b'; }", - input: "b" - } - ]; - - testcases.forEach(testcase => { - let parser = peg.generate(testcase.grammar, options); - expect(parser).to.failToParse(testcase.input); - }); - }); - }); - - describe("in outer sequence", function() { - it("can access variables defined by preceding labeled elements", function() { - let parser = peg.generate( - "start = a:'a' ('b' !{ return a !== 'a'; })", - options - ); - - expect(parser).to.parse("ab"); - }); - - it("cannot access variable defined by labeled predicate element", function() { - let parser = peg.generate( - "start = 'a' b:('b' !{ return b !== undefined; }) 'c'", - options - ); - - expect(parser).to.failToParse("abc"); - }); - - it("cannot access variables defined by following labeled elements", function() { - let parser = peg.generate( - "start = ('a' !{ return b !== 'b'; }) b:'b'", - options - ); - - expect(parser).to.failToParse("ab"); - }); - }); - }); - - describe("initializer variables & functions", function() { - it("can access variables defined in the initializer", function() { - let parser = peg.generate([ - "{ var v = 42 }", - "start = !{ return v !== 42; }" - ].join("\n"), options); - - expect(parser).to.parse(""); - }); - - it("can access functions defined in the initializer", function() { - let parser = peg.generate([ - "{ function f() { return 42; } }", - "start = !{ return f() !== 42; }" - ].join("\n"), options); - - expect(parser).to.parse(""); - }); - }); - - describe("available variables & functions", function() { - it("|options| contains options", function() { - let parser = peg.generate([ - "{ var result; }", - "start = !{ result = options; return false; } { return result; }" - ].join("\n"), options); - - expect(parser).to.parse("", { a: 42 }, { a: 42 }); - }); - - it("|location| returns current location info", function() { - let parser = peg.generate([ - "{ var result; }", - "start = line (nl+ line)* { return result; }", - "line = thing (' '+ thing)*", - "thing = digit / mark", - "digit = [0-9]", - "mark = !{ result = location(); return false; } 'x'", - "nl = '\\r'? '\\n'" - ].join("\n"), options); - - expect(parser).to.parse("1\n2\n\n3\n\n\n4 5 x", { - start: { offset: 13, line: 7, column: 5 }, - end: { offset: 13, line: 7, column: 5 } - }); - - // Newline representations - expect(parser).to.parse("1\nx", { // Unix - start: { offset: 2, line: 2, column: 1 }, - end: { offset: 2, line: 2, column: 1 } - }); - expect(parser).to.parse("1\r\nx", { // Windows - start: { offset: 3, line: 2, column: 1 }, - end: { offset: 3, line: 2, column: 1 } - }); - }); - }); - }); - - describe("group", function() { - describe("when the expression matches", function() { - it("returns its match result", function() { - let parser = peg.generate("start = ('a')", options); - - expect(parser).to.parse("a", "a"); - }); - }); - - describe("when the expression doesn't match", function() { - it("reports match failure", function() { - let parser = peg.generate("start = ('a')", options); - - expect(parser).to.failToParse("b"); - }); - }); - }); - - describe("optional", function() { - describe("when the expression matches", function() { - it("returns its match result", function() { - let parser = peg.generate("start = 'a'?", options); - - expect(parser).to.parse("a", "a"); - }); - }); - - describe("when the expression doesn't match", function() { - it("returns |null|", function() { - let parser = peg.generate("start = 'a'?", options); - - expect(parser).to.parse("", null); - }); - }); - }); - - describe("zero or more", function() { - describe("when the expression matches zero or more times", function() { - it("returns an array of its match results", function() { - let parser = peg.generate("start = 'a'*", options); - - expect(parser).to.parse("", []); - expect(parser).to.parse("a", ["a"]); - expect(parser).to.parse("aaa", ["a", "a", "a"]); - }); - }); - }); - - describe("one or more", function() { - describe("when the expression matches one or more times", function() { - it("returns an array of its match results", function() { - let parser = peg.generate("start = 'a'+", options); - - expect(parser).to.parse("a", ["a"]); - expect(parser).to.parse("aaa", ["a", "a", "a"]); - }); - }); - - describe("when the expression doesn't match", function() { - it("reports match failure", function() { - let parser = peg.generate("start = 'a'+", options); - - expect(parser).to.failToParse(""); - }); - }); - }); - - describe("text", function() { - describe("when the expression matches", function() { - it("returns the matched text", function() { - let parser = peg.generate("start = $('a' 'b' 'c')", options); - - expect(parser).to.parse("abc", "abc"); - }); - }); - - describe("when the expression doesn't match", function() { - it("reports match failure", function() { - let parser = peg.generate("start = $('a')", options); - - expect(parser).to.failToParse("b"); - }); - }); - }); - - describe("positive simple predicate", function() { - describe("when the expression matches", function() { - it("returns |undefined|", function() { - let parser = peg.generate("start = &'a' 'a'", options); - - expect(parser).to.parse("a", [undefined, "a"]); - }); - - it("resets parse position", function() { - let parser = peg.generate("start = &'a' 'a'", options); - - expect(parser).to.parse("a"); - }); - }); - - describe("when the expression doesn't match", function() { - it("reports match failure", function() { - let parser = peg.generate("start = &'a'", options); - - expect(parser).to.failToParse("b"); - }); - - it("discards any expectations recorded when matching the expression", function() { - let parser = peg.generate("start = 'a' / &'b' / 'c'", options); - - expect(parser).to.failToParse("d", { - expected: [ - { type: "literal", text: "a", ignoreCase: false }, - { type: "literal", text: "c", ignoreCase: false } - ] - }); - }); - }); - }); - - describe("negative simple predicate", function() { - describe("when the expression matches", function() { - it("reports match failure", function() { - let parser = peg.generate("start = !'a'", options); - - expect(parser).to.failToParse("a"); - }); - }); - - describe("when the expression doesn't match", function() { - it("returns |undefined|", function() { - let parser = peg.generate("start = !'a' 'b'", options); - - expect(parser).to.parse("b", [undefined, "b"]); - }); - - it("resets parse position", function() { - let parser = peg.generate("start = !'a' 'b'", options); - - expect(parser).to.parse("b"); - }); - - it("discards any expectations recorded when matching the expression", function() { - let parser = peg.generate("start = 'a' / !'b' / 'c'", options); - - expect(parser).to.failToParse("b", { - expected: [ - { type: "literal", text: "a", ignoreCase: false }, - { type: "literal", text: "c", ignoreCase: false } - ] - }); - }); - }); - }); - - describe("label", function() { - describe("when the expression matches", function() { - it("returns its match result", function() { - let parser = peg.generate("start = a:'a'", options); - - expect(parser).to.parse("a", "a"); - }); - }); - - describe("when the expression doesn't match", function() { - it("reports match failure", function() { - let parser = peg.generate("start = a:'a'", options); - - expect(parser).to.failToParse("b"); - }); - }); - }); - - describe("sequence", function() { - describe("when all expressions match", function() { - it("returns an array of their match results", function() { - let parser = peg.generate("start = 'a' 'b' 'c'", options); - - expect(parser).to.parse("abc", ["a", "b", "c"]); - }); - }); - - describe("when any expression doesn't match", function() { - it("reports match failure", function() { - let parser = peg.generate("start = 'a' 'b' 'c'", options); - - expect(parser).to.failToParse("dbc"); - expect(parser).to.failToParse("adc"); - expect(parser).to.failToParse("abd"); - }); - - it("resets parse position", function() { - let parser = peg.generate("start = 'a' 'b' / 'a'", options); - - expect(parser).to.parse("a", "a"); - }); - }); - }); - - describe("action", function() { - describe("when the expression matches", function() { - it("returns the value returned by the code", function() { - let parser = peg.generate("start = 'a' { return 42; }", options); - - expect(parser).to.parse("a", 42); - }); - - describe("label variables", function() { - describe("in the expression", function() { - it("can access variable defined by labeled expression", function() { - let parser = peg.generate("start = a:'a' { return a; }", options); - - expect(parser).to.parse("a", "a"); - }); - - it("can access variables defined by labeled sequence elements", function() { - let parser = peg.generate( - "start = a:'a' b:'b' c:'c' { return [a, b, c]; }", - options - ); - - expect(parser).to.parse("abc", ["a", "b", "c"]); - }); - - it("cannot access variables defined by subexpressions", function() { - let testcases = [ - { - grammar: "start = (a:'a') { return a; }", - input: "a" - }, - { - grammar: "start = (a:'a')? { return a; }", - input: "a" - }, - { - grammar: "start = (a:'a')* { return a; }", - input: "a" - }, - { - grammar: "start = (a:'a')+ { return a; }", - input: "a" - }, - { - grammar: "start = $(a:'a') { return a; }", - input: "a" - }, - { - grammar: "start = &(a:'a') 'a' { return a; }", - input: "a" - }, - { - grammar: "start = !(a:'a') 'b' { return a; }", - input: "b" - }, - { - grammar: "start = b:(a:'a') { return a; }", - input: "a" - }, - { - grammar: "start = ('a' b:'b' 'c') { return b; }", - input: "abc" - }, - { - grammar: "start = (a:'a' { return a; }) { return a; }", - input: "a" - }, - { - grammar: "start = ('a' / b:'b' / 'c') { return b; }", - input: "b" - } - ]; - - testcases.forEach(testcase => { - let parser = peg.generate(testcase.grammar, options); - expect(parser).to.failToParse(testcase.input); - }); - }); - }); - - describe("in outer sequence", function() { - it("can access variables defined by preceding labeled elements", function() { - let parser = peg.generate( - "start = a:'a' ('b' { return a; })", - options - ); - - expect(parser).to.parse("ab", ["a", "a"]); - }); - - it("cannot access variable defined by labeled action element", function() { - let parser = peg.generate( - "start = 'a' b:('b' { return b; }) c:'c'", - options - ); - - expect(parser).to.failToParse("abc"); - }); - - it("cannot access variables defined by following labeled elements", function() { - let parser = peg.generate( - "start = ('a' { return b; }) b:'b'", - options - ); - - expect(parser).to.failToParse("ab"); - }); - }); - }); - - describe("initializer variables & functions", function() { - it("can access variables defined in the initializer", function() { - let parser = peg.generate([ - "{ var v = 42 }", - "start = 'a' { return v; }" - ].join("\n"), options); - - expect(parser).to.parse("a", 42); - }); - - it("can access functions defined in the initializer", function() { - let parser = peg.generate([ - "{ function f() { return 42; } }", - "start = 'a' { return f(); }" - ].join("\n"), options); - - expect(parser).to.parse("a", 42); - }); - }); - - describe("available variables & functions", function() { - it("|options| contains options", function() { - let parser = peg.generate( - "start = 'a' { return options; }", - options - ); - - expect(parser).to.parse("a", { a: 42 }, { a: 42 }); - }); - - it("|text| returns text matched by the expression", function() { - let parser = peg.generate( - "start = 'a' { return text(); }", - options - ); - - expect(parser).to.parse("a", "a"); - }); - - it("|location| returns location info of the expression", function() { - let parser = peg.generate([ - "{ var result; }", - "start = line (nl+ line)* { return result; }", - "line = thing (' '+ thing)*", - "thing = digit / mark", - "digit = [0-9]", - "mark = 'x' { result = location(); }", - "nl = '\\r'? '\\n'" - ].join("\n"), options); - - expect(parser).to.parse("1\n2\n\n3\n\n\n4 5 x", { - start: { offset: 13, line: 7, column: 5 }, - end: { offset: 14, line: 7, column: 6 } - }); - - // Newline representations - expect(parser).to.parse("1\nx", { // Unix - start: { offset: 2, line: 2, column: 1 }, - end: { offset: 3, line: 2, column: 2 } - }); - expect(parser).to.parse("1\r\nx", { // Windows - start: { offset: 3, line: 2, column: 1 }, - end: { offset: 4, line: 2, column: 2 } - }); - }); - - describe("|expected|", function() { - it("terminates parsing and throws an exception", function() { - let parser = peg.generate( - "start = 'a' { expected('a'); }", - options - ); - - expect(parser).to.failToParse("a", { - message: "Expected a but \"a\" found.", - expected: [{ type: "other", description: "a" }], - found: "a", - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 1, line: 1, column: 2 } - } - }); - }); - - it("allows to set custom location info", function() { - let parser = peg.generate([ - "start = 'a' {", - " expected('a', {", - " start: { offset: 1, line: 1, column: 2 },", - " end: { offset: 2, line: 1, column: 3 }", - " });", - "}" - ].join("\n"), options); - - expect(parser).to.failToParse("a", { - message: "Expected a but \"a\" found.", - expected: [{ type: "other", description: "a" }], - found: "a", - location: { - start: { offset: 1, line: 1, column: 2 }, - end: { offset: 2, line: 1, column: 3 } - } - }); - }); - }); - - describe("|error|", function() { - it("terminates parsing and throws an exception", function() { - let parser = peg.generate( - "start = 'a' { error('a'); }", - options - ); - - expect(parser).to.failToParse("a", { - message: "a", - found: null, - expected: null, - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 1, line: 1, column: 2 } - } - }); - }); - - it("allows to set custom location info", function() { - let parser = peg.generate([ - "start = 'a' {", - " error('a', {", - " start: { offset: 1, line: 1, column: 2 },", - " end: { offset: 2, line: 1, column: 3 }", - " });", - "}" - ].join("\n"), options); - - expect(parser).to.failToParse("a", { - message: "a", - expected: null, - found: null, - location: { - start: { offset: 1, line: 1, column: 2 }, - end: { offset: 2, line: 1, column: 3 } - } - }); - }); - }); - }); - }); - - describe("when the expression doesn't match", function() { - it("reports match failure", function() { - let parser = peg.generate("start = 'a' { return 42; }", options); - - expect(parser).to.failToParse("b"); - }); - - it("doesn't execute the code", function() { - let parser = peg.generate( - "start = 'a' { throw 'Boom!'; } / 'b'", - options - ); - - expect(parser).to.parse("b"); - }); - }); - }); - - describe("choice", function() { - describe("when any expression matches", function() { - it("returns its match result", function() { - let parser = peg.generate("start = 'a' / 'b' / 'c'", options); - - expect(parser).to.parse("a", "a"); - expect(parser).to.parse("b", "b"); - expect(parser).to.parse("c", "c"); - }); - }); - - describe("when all expressions don't match", function() { - it("reports match failure", function() { - let parser = peg.generate("start = 'a' / 'b' / 'c'", options); - - expect(parser).to.failToParse("d"); - }); - }); - }); - - describe("error reporting", function() { - describe("behavior", function() { - it("reports only the rightmost error", function() { - let parser = peg.generate("start = 'a' 'b' / 'a' 'c' 'd'", options); - - expect(parser).to.failToParse("ace", { - expected: [{ type: "literal", text: "d", ignoreCase: false }] - }); - }); - }); - - describe("expectations reporting", function() { - it("reports expectations correctly with no alternative", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("ab", { - expected: [{ type: "end" }] - }); - }); - - it("reports expectations correctly with one alternative", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("b", { - expected: [{ type: "literal", text: "a", ignoreCase: false }] - }); - }); - - it("reports expectations correctly with multiple alternatives", function() { - let parser = peg.generate("start = 'a' / 'b' / 'c'", options); - - expect(parser).to.failToParse("d", { - expected: [ - { type: "literal", text: "a", ignoreCase: false }, - { type: "literal", text: "b", ignoreCase: false }, - { type: "literal", text: "c", ignoreCase: false } - ] - }); - }); - }); - - describe("found string reporting", function() { - it("reports found string correctly at the end of input", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("", { found: null }); - }); - - it("reports found string correctly in the middle of input", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("b", { found: "b" }); - }); - }); - - describe("message building", function() { - it("builds message correctly with no alternative", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("ab", { - message: "Expected end of input but \"b\" found." - }); - }); - - it("builds message correctly with one alternative", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("b", { - message: "Expected \"a\" but \"b\" found." - }); - }); - - it("builds message correctly with multiple alternatives", function() { - let parser = peg.generate("start = 'a' / 'b' / 'c'", options); - - expect(parser).to.failToParse("d", { - message: "Expected \"a\", \"b\", or \"c\" but \"d\" found." - }); - }); - - it("builds message correctly at the end of input", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("", { - message: "Expected \"a\" but end of input found." - }); - }); - - it("builds message correctly in the middle of input", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("b", { - message: "Expected \"a\" but \"b\" found." - }); - }); - - it("removes duplicates from expectations", function() { - let parser = peg.generate("start = 'a' / 'a'", options); - - expect(parser).to.failToParse("b", { - message: "Expected \"a\" but \"b\" found." - }); - }); - - it("sorts expectations", function() { - let parser = peg.generate("start = 'c' / 'b' / 'a'", options); - - expect(parser).to.failToParse("d", { - message: "Expected \"a\", \"b\", or \"c\" but \"d\" found." - }); - }); - }); - - describe("position reporting", function() { - it("reports position correctly at the end of input", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("", { - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 0, line: 1, column: 1 } - } - }); - }); - - it("reports position correctly in the middle of input", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("b", { - location: { - start: { offset: 0, line: 1, column: 1 }, - end: { offset: 1, line: 1, column: 2 } - } - }); - }); - - it("reports position correctly with trailing input", function() { - let parser = peg.generate("start = 'a'", options); - - expect(parser).to.failToParse("aa", { - location: { - start: { offset: 1, line: 1, column: 2 }, - end: { offset: 2, line: 1, column: 3 } - } - }); - }); - - it("reports position correctly in complex cases", function() { - let parser = peg.generate([ - "start = line (nl+ line)*", - "line = digit (' '+ digit)*", - "digit = [0-9]", - "nl = '\\r'? '\\n'" - ].join("\n"), options); - - expect(parser).to.failToParse("1\n2\n\n3\n\n\n4 5 x", { - location: { - start: { offset: 13, line: 7, column: 5 }, - end: { offset: 14, line: 7, column: 6 } - } - }); - - // Newline representations - expect(parser).to.failToParse("1\nx", { // Old Mac - location: { - start: { offset: 2, line: 2, column: 1 }, - end: { offset: 3, line: 2, column: 2 } - } - }); - expect(parser).to.failToParse("1\r\nx", { // Windows - location: { - start: { offset: 3, line: 2, column: 1 }, - end: { offset: 4, line: 2, column: 2 } - } - }); - }); - }); - }); - - // Following examples are from Wikipedia, see - // http://en.wikipedia.org/w/index.php?title=Parsing_expression_grammar&oldid=335106938. - describe("complex examples", function() { - it("handles arithmetics example correctly", function() { - // Value ← [0-9]+ / '(' Expr ')' - // Product ← Value (('*' / '/') Value)* - // Sum ← Product (('+' / '-') Product)* - // Expr ← Sum - let parser = peg.generate([ - "Expr = Sum", - "Sum = head:Product tail:(('+' / '-') Product)* {", - " return tail.reduce(function(result, element) {", - " if (element[0] === '+') { return result + element[1]; }", - " if (element[0] === '-') { return result - element[1]; }", - " }, head);", - " }", - "Product = head:Value tail:(('*' / '/') Value)* {", - " return tail.reduce(function(result, element) {", - " if (element[0] === '*') { return result * element[1]; }", - " if (element[0] === '/') { return result / element[1]; }", - " }, head);", - " }", - "Value = digits:[0-9]+ { return parseInt(digits.join(''), 10); }", - " / '(' expr:Expr ')' { return expr; }" - ].join("\n"), options); - - // The "value" rule - expect(parser).to.parse("0", 0); - expect(parser).to.parse("123", 123); - expect(parser).to.parse("(42+43)", 42 + 43); - - // The "product" rule - expect(parser).to.parse("42", 42); - expect(parser).to.parse("42*43", 42 * 43); - expect(parser).to.parse("42*43*44*45", 42 * 43 * 44 * 45); - expect(parser).to.parse("42/43", 42 / 43); - expect(parser).to.parse("42/43/44/45", 42 / 43 / 44 / 45); - - // The "sum" rule - expect(parser).to.parse("42*43", 42 * 43); - expect(parser).to.parse("42*43+44*45", 42 * 43 + 44 * 45); - expect(parser).to.parse("42*43+44*45+46*47+48*49", 42 * 43 + 44 * 45 + 46 * 47 + 48 * 49); - expect(parser).to.parse("42*43-44*45", 42 * 43 - 44 * 45); - expect(parser).to.parse("42*43-44*45-46*47-48*49", 42 * 43 - 44 * 45 - 46 * 47 - 48 * 49); - - // The "expr" rule - expect(parser).to.parse("42+43", 42 + 43); - - // Complex test - expect(parser).to.parse("(1+2)*(3+4)", (1 + 2) * (3 + 4)); - }); - - it("handles non-context-free language correctly", function() { - // The following parsing expression grammar describes the classic - // non-context-free language { a^n b^n c^n : n >= 1 }: - // - // S ← &(A c) a+ B !(a/b/c) - // A ← a A? b - // B ← b B? c - let parser = peg.generate([ - "S = &(A 'c') a:'a'+ B:B !('a' / 'b' / 'c') { return a.join('') + B; }", - "A = a:'a' A:A? b:'b' { return [a, A, b].join(''); }", - "B = b:'b' B:B? c:'c' { return [b, B, c].join(''); }" - ].join("\n"), options); - - expect(parser).to.parse("abc", "abc"); - expect(parser).to.parse("aaabbbccc", "aaabbbccc"); - expect(parser).to.failToParse("aabbbccc"); - expect(parser).to.failToParse("aaaabbbccc"); - expect(parser).to.failToParse("aaabbccc"); - expect(parser).to.failToParse("aaabbbbccc"); - expect(parser).to.failToParse("aaabbbcc"); - expect(parser).to.failToParse("aaabbbcccc"); - }); - - it("handles nested comments example correctly", function() { - // Begin ← "(*" - // End ← "*)" - // C ← Begin N* End - // N ← C / (!Begin !End Z) - // Z ← any single character - let parser = peg.generate([ - "C = begin:Begin ns:N* end:End { return begin + ns.join('') + end; }", - "N = C", - " / !Begin !End z:Z { return z; }", - "Z = .", - "Begin = '(*'", - "End = '*)'" - ].join("\n"), options); - - expect(parser).to.parse("(**)", "(**)"); - expect(parser).to.parse("(*abc*)", "(*abc*)"); - expect(parser).to.parse("(*(**)*)", "(*(**)*)"); - expect(parser).to.parse( - "(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)", - "(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)" - ); - }); - }); - }); + function varyOptimizationOptions(block) { + function clone(object) { + let result = {}; + + Object.keys(object).forEach(key => { + result[key] = object[key]; + }); + + return result; + } + + let optionsVariants = [ + { cache: false, optimize: "speed", trace: false }, + { cache: false, optimize: "speed", trace: true }, + { cache: false, optimize: "size", trace: false }, + { cache: false, optimize: "size", trace: true }, + { cache: true, optimize: "speed", trace: false }, + { cache: true, optimize: "speed", trace: true }, + { cache: true, optimize: "size", trace: false }, + { cache: true, optimize: "size", trace: true } + ]; + + optionsVariants.forEach(variant => { + describe( + "with options " + chai.util.inspect(variant), + function() { block(clone(variant)); } + ); + }); + } + + function withConsoleStub(block) { + if (typeof console === "object") { + sinon.stub(console, "log"); + } + + try { + return block(); + } finally { + if (typeof console === "object") { + console.log.restore(); + } + } + } + + function helpers(chai, utils) { + let Assertion = chai.Assertion; + + Assertion.addMethod("parse", function(input, expected, options) { + options = options !== undefined ? options : {}; + + let result = withConsoleStub(() => + utils.flag(this, "object").parse(input, options) + ); + + if (expected !== undefined) { + this.assert( + utils.eql(result, expected), + "expected #{this} to parse input as #{exp} but got #{act}", + "expected #{this} to not parse input as #{exp}", + expected, + result, + !utils.flag(this, "negate") + ); + } + }); + + Assertion.addMethod("failToParse", function(input, props, options) { + options = options !== undefined ? options : {}; + + let passed, result; + + try { + result = withConsoleStub(() => + utils.flag(this, "object").parse(input, options) + ); + passed = true; + } catch (e) { + result = e; + passed = false; + } + + this.assert( + !passed, + "expected #{this} to fail to parse input but got #{act}", + "expected #{this} to not fail to parse input but #{act} was thrown", + null, + result + ); + + if (!passed && props !== undefined) { + Object.keys(props).forEach(key => { + new Assertion(result).to.have.property(key) + .that.is.deep.equal(props[key]); + }); + } + }); + } + + // Helper activation needs to put inside a |beforeEach| block because the + // helpers conflict with the ones in test/unit/parser.spec.js. + beforeEach(function() { + chai.use(helpers); + }); + + varyOptimizationOptions(function(options) { + describe("initializer", function() { + it("executes the code before parsing starts", function() { + let parser = peg.generate([ + "{ var result = 42; }", + "start = 'a' { return result; }" + ].join("\n"), options); + + expect(parser).to.parse("a", 42); + }); + + describe("available variables and functions", function() { + it("|options| contains options", function() { + let parser = peg.generate([ + "{ var result = options; }", + "start = 'a' { return result; }" + ].join("\n"), options); + + expect(parser).to.parse("a", { a: 42 }, { a: 42 }); + }); + }); + }); + + describe("rule", function() { + if (options.cache) { + it("caches rule match results", function() { + let parser = peg.generate([ + "{ var n = 0; }", + "start = (a 'b') / (a 'c') { return n; }", + "a = 'a' { n++; }" + ].join("\n"), options); + + expect(parser).to.parse("ac", 1); + }); + } else { + it("doesn't cache rule match results", function() { + let parser = peg.generate([ + "{ var n = 0; }", + "start = (a 'b') / (a 'c') { return n; }", + "a = 'a' { n++; }" + ].join("\n"), options); + + expect(parser).to.parse("ac", 2); + }); + } + + describe("when the expression matches", function() { + it("returns its match result", function() { + let parser = peg.generate("start = 'a'"); + + expect(parser).to.parse("a", "a"); + }); + }); + + describe("when the expression doesn't match", function() { + describe("without display name", function() { + it("reports match failure and doesn't record any expectation", function() { + let parser = peg.generate("start = 'a'"); + + expect(parser).to.failToParse("b", { + expected: [{ type: "literal", text: "a", ignoreCase: false }] + }); + }); + }); + + describe("with display name", function() { + it("reports match failure and records an expectation of type \"other\"", function() { + let parser = peg.generate("start 'start' = 'a'"); + + expect(parser).to.failToParse("b", { + expected: [{ type: "other", description: "start" }] + }); + }); + + it("discards any expectations recorded when matching the expression", function() { + let parser = peg.generate("start 'start' = 'a'"); + + expect(parser).to.failToParse("b", { + expected: [{ type: "other", description: "start" }] + }); + }); + }); + }); + }); + + describe("literal", function() { + describe("matching", function() { + it("matches empty literals", function() { + let parser = peg.generate("start = ''", options); + + expect(parser).to.parse(""); + }); + + it("matches one-character literals", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.parse("a"); + expect(parser).to.failToParse("b"); + }); + + it("matches multi-character literals", function() { + let parser = peg.generate("start = 'abcd'", options); + + expect(parser).to.parse("abcd"); + expect(parser).to.failToParse("efgh"); + }); + + it("is case sensitive without the \"i\" flag", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.parse("a"); + expect(parser).to.failToParse("A"); + }); + + it("is case insensitive with the \"i\" flag", function() { + let parser = peg.generate("start = 'a'i", options); + + expect(parser).to.parse("a"); + expect(parser).to.parse("A"); + }); + }); + + describe("when it matches", function() { + it("returns the matched text", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.parse("a", "a"); + }); + + it("consumes the matched text", function() { + let parser = peg.generate("start = 'a' .", options); + + expect(parser).to.parse("ab"); + }); + }); + + describe("when it doesn't match", function() { + it("reports match failure and records an expectation of type \"literal\"", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("b", { + expected: [{ type: "literal", text: "a", ignoreCase: false }] + }); + }); + }); + }); + + describe("character class", function() { + describe("matching", function() { + it("matches empty classes", function() { + let parser = peg.generate("start = []", options); + + expect(parser).to.failToParse("a"); + }); + + it("matches classes with a character list", function() { + let parser = peg.generate("start = [abc]", options); + + expect(parser).to.parse("a"); + expect(parser).to.parse("b"); + expect(parser).to.parse("c"); + expect(parser).to.failToParse("d"); + }); + + it("matches classes with a character range", function() { + let parser = peg.generate("start = [a-c]", options); + + expect(parser).to.parse("a"); + expect(parser).to.parse("b"); + expect(parser).to.parse("c"); + expect(parser).to.failToParse("d"); + }); + + it("matches inverted classes", function() { + let parser = peg.generate("start = [^a]", options); + + expect(parser).to.failToParse("a"); + expect(parser).to.parse("b"); + }); + + it("is case sensitive without the \"i\" flag", function() { + let parser = peg.generate("start = [a]", options); + + expect(parser).to.parse("a"); + expect(parser).to.failToParse("A"); + }); + + it("is case insensitive with the \"i\" flag", function() { + let parser = peg.generate("start = [a]i", options); + + expect(parser).to.parse("a"); + expect(parser).to.parse("A"); + }); + }); + + describe("when it matches", function() { + it("returns the matched character", function() { + let parser = peg.generate("start = [a]", options); + + expect(parser).to.parse("a", "a"); + }); + + it("consumes the matched character", function() { + let parser = peg.generate("start = [a] .", options); + + expect(parser).to.parse("ab"); + }); + }); + + describe("when it doesn't match", function() { + it("reports match failure and records an expectation of type \"class\"", function() { + let parser = peg.generate("start = [a]", options); + + expect(parser).to.failToParse("b", { + expected: [{ type: "class", parts: ["a"], inverted: false, ignoreCase: false }] + }); + }); + }); + }); + + describe("dot", function() { + describe("matching", function() { + it("matches any character", function() { + let parser = peg.generate("start = .", options); + + expect(parser).to.parse("a"); + expect(parser).to.parse("b"); + expect(parser).to.parse("c"); + }); + }); + + describe("when it matches", function() { + it("returns the matched character", function() { + let parser = peg.generate("start = .", options); + + expect(parser).to.parse("a", "a"); + }); + + it("consumes the matched character", function() { + let parser = peg.generate("start = . .", options); + + expect(parser).to.parse("ab"); + }); + }); + + describe("when it doesn't match", function() { + it("reports match failure and records an expectation of type \"any\"", function() { + let parser = peg.generate("start = .", options); + + expect(parser).to.failToParse("", { + expected: [{ type: "any" }] + }); + }); + }); + }); + + describe("rule reference", function() { + describe("when referenced rule's expression matches", function() { + it("returns its result", function() { + let parser = peg.generate([ + "start = a", + "a = 'a'" + ].join("\n"), options); + + expect(parser).to.parse("a", "a"); + }); + }); + + describe("when referenced rule's expression doesn't match", function() { + it("reports match failure", function() { + let parser = peg.generate([ + "start = a", + "a = 'a'" + ].join("\n"), options); + + expect(parser).to.failToParse("b"); + }); + }); + }); + + describe("positive semantic predicate", function() { + describe("when the code returns a truthy value", function() { + it("returns |undefined|", function() { + // The |""| is needed so that the parser doesn't return just + // |undefined| which we can't compare against in |toParse| due to the + // way optional parameters work. + let parser = peg.generate("start = &{ return true; } ''", options); + + expect(parser).to.parse("", [undefined, ""]); + }); + }); + + describe("when the code returns a falsey value", function() { + it("reports match failure", function() { + let parser = peg.generate("start = &{ return false; }", options); + + expect(parser).to.failToParse(""); + }); + }); + + describe("label variables", function() { + describe("in containing sequence", function() { + it("can access variables defined by preceding labeled elements", function() { + let parser = peg.generate( + "start = a:'a' &{ return a === 'a'; }", + options + ); + + expect(parser).to.parse("a"); + }); + + it("cannot access variable defined by labeled predicate element", function() { + let parser = peg.generate( + "start = 'a' b:&{ return b === undefined; } 'c'", + options + ); + + expect(parser).to.failToParse("ac"); + }); + + it("cannot access variables defined by following labeled elements", function() { + let parser = peg.generate( + "start = &{ return a === 'a'; } a:'a'", + options + ); + + expect(parser).to.failToParse("a"); + }); + + it("cannot access variables defined by subexpressions", function() { + let testcases = [ + { + grammar: "start = (a:'a') &{ return a === 'a'; }", + input: "a" + }, + { + grammar: "start = (a:'a')? &{ return a === 'a'; }", + input: "a" + }, + { + grammar: "start = (a:'a')* &{ return a === 'a'; }", + input: "a" + }, + { + grammar: "start = (a:'a')+ &{ return a === 'a'; }", + input: "a" + }, + { + grammar: "start = $(a:'a') &{ return a === 'a'; }", + input: "a" + }, + { + grammar: "start = &(a:'a') 'a' &{ return a === 'a'; }", + input: "a" + }, + { + grammar: "start = !(a:'a') 'b' &{ return a === 'a'; }", + input: "b" + }, + { + grammar: "start = b:(a:'a') &{ return a === 'a'; }", + input: "a" + }, + { + grammar: "start = ('a' b:'b' 'c') &{ return b === 'b'; }", + input: "abc" + }, + { + grammar: "start = (a:'a' { return a; }) &{ return a === 'a'; }", + input: "a" + }, + { + grammar: "start = ('a' / b:'b' / 'c') &{ return b === 'b'; }", + input: "b" + } + ]; + + testcases.forEach(testcase => { + let parser = peg.generate(testcase.grammar, options); + expect(parser).to.failToParse(testcase.input); + }); + }); + }); + + describe("in outer sequence", function() { + it("can access variables defined by preceding labeled elements", function() { + let parser = peg.generate( + "start = a:'a' ('b' &{ return a === 'a'; })", + options + ); + + expect(parser).to.parse("ab"); + }); + + it("cannot access variable defined by labeled predicate element", function() { + let parser = peg.generate( + "start = 'a' b:('b' &{ return b === undefined; }) 'c'", + options + ); + + expect(parser).to.failToParse("abc"); + }); + + it("cannot access variables defined by following labeled elements", function() { + let parser = peg.generate( + "start = ('a' &{ return b === 'b'; }) b:'b'", + options + ); + + expect(parser).to.failToParse("ab"); + }); + }); + }); + + describe("initializer variables & functions", function() { + it("can access variables defined in the initializer", function() { + let parser = peg.generate([ + "{ var v = 42 }", + "start = &{ return v === 42; }" + ].join("\n"), options); + + expect(parser).to.parse(""); + }); + + it("can access functions defined in the initializer", function() { + let parser = peg.generate([ + "{ function f() { return 42; } }", + "start = &{ return f() === 42; }" + ].join("\n"), options); + + expect(parser).to.parse(""); + }); + }); + + describe("available variables & functions", function() { + it("|options| contains options", function() { + let parser = peg.generate([ + "{ var result; }", + "start = &{ result = options; return true; } { return result; }" + ].join("\n"), options); + + expect(parser).to.parse("", { a: 42 }, { a: 42 }); + }); + + it("|location| returns current location info", function() { + let parser = peg.generate([ + "{ var result; }", + "start = line (nl+ line)* { return result; }", + "line = thing (' '+ thing)*", + "thing = digit / mark", + "digit = [0-9]", + "mark = &{ result = location(); return true; } 'x'", + "nl = '\\r'? '\\n'" + ].join("\n"), options); + + expect(parser).to.parse("1\n2\n\n3\n\n\n4 5 x", { + start: { offset: 13, line: 7, column: 5 }, + end: { offset: 13, line: 7, column: 5 } + }); + + // Newline representations + expect(parser).to.parse("1\nx", { // Unix + start: { offset: 2, line: 2, column: 1 }, + end: { offset: 2, line: 2, column: 1 } + }); + expect(parser).to.parse("1\r\nx", { // Windows + start: { offset: 3, line: 2, column: 1 }, + end: { offset: 3, line: 2, column: 1 } + }); + }); + }); + }); + + describe("negative semantic predicate", function() { + describe("when the code returns a falsey value", function() { + it("returns |undefined|", function() { + // The |""| is needed so that the parser doesn't return just + // |undefined| which we can't compare against in |toParse| due to the + // way optional parameters work. + let parser = peg.generate("start = !{ return false; } ''", options); + + expect(parser).to.parse("", [undefined, ""]); + }); + }); + + describe("when the code returns a truthy value", function() { + it("reports match failure", function() { + let parser = peg.generate("start = !{ return true; }", options); + + expect(parser).to.failToParse(""); + }); + }); + + describe("label variables", function() { + describe("in containing sequence", function() { + it("can access variables defined by preceding labeled elements", function() { + let parser = peg.generate( + "start = a:'a' !{ return a !== 'a'; }", + options + ); + + expect(parser).to.parse("a"); + }); + + it("cannot access variable defined by labeled predicate element", function() { + let parser = peg.generate( + "start = 'a' b:!{ return b !== undefined; } 'c'", + options + ); + + expect(parser).to.failToParse("ac"); + }); + + it("cannot access variables defined by following labeled elements", function() { + let parser = peg.generate( + "start = !{ return a !== 'a'; } a:'a'", + options + ); + + expect(parser).to.failToParse("a"); + }); + + it("cannot access variables defined by subexpressions", function() { + let testcases = [ + { + grammar: "start = (a:'a') !{ return a !== 'a'; }", + input: "a" + }, + { + grammar: "start = (a:'a')? !{ return a !== 'a'; }", + input: "a" + }, + { + grammar: "start = (a:'a')* !{ return a !== 'a'; }", + input: "a" + }, + { + grammar: "start = (a:'a')+ !{ return a !== 'a'; }", + input: "a" + }, + { + grammar: "start = $(a:'a') !{ return a !== 'a'; }", + input: "a" + }, + { + grammar: "start = &(a:'a') 'a' !{ return a !== 'a'; }", + input: "a" + }, + { + grammar: "start = !(a:'a') 'b' !{ return a !== 'a'; }", + input: "b" + }, + { + grammar: "start = b:(a:'a') !{ return a !== 'a'; }", + input: "a" + }, + { + grammar: "start = ('a' b:'b' 'c') !{ return b !== 'b'; }", + input: "abc" + }, + { + grammar: "start = (a:'a' { return a; }) !{ return a !== 'a'; }", + input: "a" + }, + { + grammar: "start = ('a' / b:'b' / 'c') !{ return b !== 'b'; }", + input: "b" + } + ]; + + testcases.forEach(testcase => { + let parser = peg.generate(testcase.grammar, options); + expect(parser).to.failToParse(testcase.input); + }); + }); + }); + + describe("in outer sequence", function() { + it("can access variables defined by preceding labeled elements", function() { + let parser = peg.generate( + "start = a:'a' ('b' !{ return a !== 'a'; })", + options + ); + + expect(parser).to.parse("ab"); + }); + + it("cannot access variable defined by labeled predicate element", function() { + let parser = peg.generate( + "start = 'a' b:('b' !{ return b !== undefined; }) 'c'", + options + ); + + expect(parser).to.failToParse("abc"); + }); + + it("cannot access variables defined by following labeled elements", function() { + let parser = peg.generate( + "start = ('a' !{ return b !== 'b'; }) b:'b'", + options + ); + + expect(parser).to.failToParse("ab"); + }); + }); + }); + + describe("initializer variables & functions", function() { + it("can access variables defined in the initializer", function() { + let parser = peg.generate([ + "{ var v = 42 }", + "start = !{ return v !== 42; }" + ].join("\n"), options); + + expect(parser).to.parse(""); + }); + + it("can access functions defined in the initializer", function() { + let parser = peg.generate([ + "{ function f() { return 42; } }", + "start = !{ return f() !== 42; }" + ].join("\n"), options); + + expect(parser).to.parse(""); + }); + }); + + describe("available variables & functions", function() { + it("|options| contains options", function() { + let parser = peg.generate([ + "{ var result; }", + "start = !{ result = options; return false; } { return result; }" + ].join("\n"), options); + + expect(parser).to.parse("", { a: 42 }, { a: 42 }); + }); + + it("|location| returns current location info", function() { + let parser = peg.generate([ + "{ var result; }", + "start = line (nl+ line)* { return result; }", + "line = thing (' '+ thing)*", + "thing = digit / mark", + "digit = [0-9]", + "mark = !{ result = location(); return false; } 'x'", + "nl = '\\r'? '\\n'" + ].join("\n"), options); + + expect(parser).to.parse("1\n2\n\n3\n\n\n4 5 x", { + start: { offset: 13, line: 7, column: 5 }, + end: { offset: 13, line: 7, column: 5 } + }); + + // Newline representations + expect(parser).to.parse("1\nx", { // Unix + start: { offset: 2, line: 2, column: 1 }, + end: { offset: 2, line: 2, column: 1 } + }); + expect(parser).to.parse("1\r\nx", { // Windows + start: { offset: 3, line: 2, column: 1 }, + end: { offset: 3, line: 2, column: 1 } + }); + }); + }); + }); + + describe("group", function() { + describe("when the expression matches", function() { + it("returns its match result", function() { + let parser = peg.generate("start = ('a')", options); + + expect(parser).to.parse("a", "a"); + }); + }); + + describe("when the expression doesn't match", function() { + it("reports match failure", function() { + let parser = peg.generate("start = ('a')", options); + + expect(parser).to.failToParse("b"); + }); + }); + }); + + describe("optional", function() { + describe("when the expression matches", function() { + it("returns its match result", function() { + let parser = peg.generate("start = 'a'?", options); + + expect(parser).to.parse("a", "a"); + }); + }); + + describe("when the expression doesn't match", function() { + it("returns |null|", function() { + let parser = peg.generate("start = 'a'?", options); + + expect(parser).to.parse("", null); + }); + }); + }); + + describe("zero or more", function() { + describe("when the expression matches zero or more times", function() { + it("returns an array of its match results", function() { + let parser = peg.generate("start = 'a'*", options); + + expect(parser).to.parse("", []); + expect(parser).to.parse("a", ["a"]); + expect(parser).to.parse("aaa", ["a", "a", "a"]); + }); + }); + }); + + describe("one or more", function() { + describe("when the expression matches one or more times", function() { + it("returns an array of its match results", function() { + let parser = peg.generate("start = 'a'+", options); + + expect(parser).to.parse("a", ["a"]); + expect(parser).to.parse("aaa", ["a", "a", "a"]); + }); + }); + + describe("when the expression doesn't match", function() { + it("reports match failure", function() { + let parser = peg.generate("start = 'a'+", options); + + expect(parser).to.failToParse(""); + }); + }); + }); + + describe("text", function() { + describe("when the expression matches", function() { + it("returns the matched text", function() { + let parser = peg.generate("start = $('a' 'b' 'c')", options); + + expect(parser).to.parse("abc", "abc"); + }); + }); + + describe("when the expression doesn't match", function() { + it("reports match failure", function() { + let parser = peg.generate("start = $('a')", options); + + expect(parser).to.failToParse("b"); + }); + }); + }); + + describe("positive simple predicate", function() { + describe("when the expression matches", function() { + it("returns |undefined|", function() { + let parser = peg.generate("start = &'a' 'a'", options); + + expect(parser).to.parse("a", [undefined, "a"]); + }); + + it("resets parse position", function() { + let parser = peg.generate("start = &'a' 'a'", options); + + expect(parser).to.parse("a"); + }); + }); + + describe("when the expression doesn't match", function() { + it("reports match failure", function() { + let parser = peg.generate("start = &'a'", options); + + expect(parser).to.failToParse("b"); + }); + + it("discards any expectations recorded when matching the expression", function() { + let parser = peg.generate("start = 'a' / &'b' / 'c'", options); + + expect(parser).to.failToParse("d", { + expected: [ + { type: "literal", text: "a", ignoreCase: false }, + { type: "literal", text: "c", ignoreCase: false } + ] + }); + }); + }); + }); + + describe("negative simple predicate", function() { + describe("when the expression matches", function() { + it("reports match failure", function() { + let parser = peg.generate("start = !'a'", options); + + expect(parser).to.failToParse("a"); + }); + }); + + describe("when the expression doesn't match", function() { + it("returns |undefined|", function() { + let parser = peg.generate("start = !'a' 'b'", options); + + expect(parser).to.parse("b", [undefined, "b"]); + }); + + it("resets parse position", function() { + let parser = peg.generate("start = !'a' 'b'", options); + + expect(parser).to.parse("b"); + }); + + it("discards any expectations recorded when matching the expression", function() { + let parser = peg.generate("start = 'a' / !'b' / 'c'", options); + + expect(parser).to.failToParse("b", { + expected: [ + { type: "literal", text: "a", ignoreCase: false }, + { type: "literal", text: "c", ignoreCase: false } + ] + }); + }); + }); + }); + + describe("label", function() { + describe("when the expression matches", function() { + it("returns its match result", function() { + let parser = peg.generate("start = a:'a'", options); + + expect(parser).to.parse("a", "a"); + }); + }); + + describe("when the expression doesn't match", function() { + it("reports match failure", function() { + let parser = peg.generate("start = a:'a'", options); + + expect(parser).to.failToParse("b"); + }); + }); + }); + + describe("sequence", function() { + describe("when all expressions match", function() { + it("returns an array of their match results", function() { + let parser = peg.generate("start = 'a' 'b' 'c'", options); + + expect(parser).to.parse("abc", ["a", "b", "c"]); + }); + }); + + describe("when any expression doesn't match", function() { + it("reports match failure", function() { + let parser = peg.generate("start = 'a' 'b' 'c'", options); + + expect(parser).to.failToParse("dbc"); + expect(parser).to.failToParse("adc"); + expect(parser).to.failToParse("abd"); + }); + + it("resets parse position", function() { + let parser = peg.generate("start = 'a' 'b' / 'a'", options); + + expect(parser).to.parse("a", "a"); + }); + }); + }); + + describe("action", function() { + describe("when the expression matches", function() { + it("returns the value returned by the code", function() { + let parser = peg.generate("start = 'a' { return 42; }", options); + + expect(parser).to.parse("a", 42); + }); + + describe("label variables", function() { + describe("in the expression", function() { + it("can access variable defined by labeled expression", function() { + let parser = peg.generate("start = a:'a' { return a; }", options); + + expect(parser).to.parse("a", "a"); + }); + + it("can access variables defined by labeled sequence elements", function() { + let parser = peg.generate( + "start = a:'a' b:'b' c:'c' { return [a, b, c]; }", + options + ); + + expect(parser).to.parse("abc", ["a", "b", "c"]); + }); + + it("cannot access variables defined by subexpressions", function() { + let testcases = [ + { + grammar: "start = (a:'a') { return a; }", + input: "a" + }, + { + grammar: "start = (a:'a')? { return a; }", + input: "a" + }, + { + grammar: "start = (a:'a')* { return a; }", + input: "a" + }, + { + grammar: "start = (a:'a')+ { return a; }", + input: "a" + }, + { + grammar: "start = $(a:'a') { return a; }", + input: "a" + }, + { + grammar: "start = &(a:'a') 'a' { return a; }", + input: "a" + }, + { + grammar: "start = !(a:'a') 'b' { return a; }", + input: "b" + }, + { + grammar: "start = b:(a:'a') { return a; }", + input: "a" + }, + { + grammar: "start = ('a' b:'b' 'c') { return b; }", + input: "abc" + }, + { + grammar: "start = (a:'a' { return a; }) { return a; }", + input: "a" + }, + { + grammar: "start = ('a' / b:'b' / 'c') { return b; }", + input: "b" + } + ]; + + testcases.forEach(testcase => { + let parser = peg.generate(testcase.grammar, options); + expect(parser).to.failToParse(testcase.input); + }); + }); + }); + + describe("in outer sequence", function() { + it("can access variables defined by preceding labeled elements", function() { + let parser = peg.generate( + "start = a:'a' ('b' { return a; })", + options + ); + + expect(parser).to.parse("ab", ["a", "a"]); + }); + + it("cannot access variable defined by labeled action element", function() { + let parser = peg.generate( + "start = 'a' b:('b' { return b; }) c:'c'", + options + ); + + expect(parser).to.failToParse("abc"); + }); + + it("cannot access variables defined by following labeled elements", function() { + let parser = peg.generate( + "start = ('a' { return b; }) b:'b'", + options + ); + + expect(parser).to.failToParse("ab"); + }); + }); + }); + + describe("initializer variables & functions", function() { + it("can access variables defined in the initializer", function() { + let parser = peg.generate([ + "{ var v = 42 }", + "start = 'a' { return v; }" + ].join("\n"), options); + + expect(parser).to.parse("a", 42); + }); + + it("can access functions defined in the initializer", function() { + let parser = peg.generate([ + "{ function f() { return 42; } }", + "start = 'a' { return f(); }" + ].join("\n"), options); + + expect(parser).to.parse("a", 42); + }); + }); + + describe("available variables & functions", function() { + it("|options| contains options", function() { + let parser = peg.generate( + "start = 'a' { return options; }", + options + ); + + expect(parser).to.parse("a", { a: 42 }, { a: 42 }); + }); + + it("|text| returns text matched by the expression", function() { + let parser = peg.generate( + "start = 'a' { return text(); }", + options + ); + + expect(parser).to.parse("a", "a"); + }); + + it("|location| returns location info of the expression", function() { + let parser = peg.generate([ + "{ var result; }", + "start = line (nl+ line)* { return result; }", + "line = thing (' '+ thing)*", + "thing = digit / mark", + "digit = [0-9]", + "mark = 'x' { result = location(); }", + "nl = '\\r'? '\\n'" + ].join("\n"), options); + + expect(parser).to.parse("1\n2\n\n3\n\n\n4 5 x", { + start: { offset: 13, line: 7, column: 5 }, + end: { offset: 14, line: 7, column: 6 } + }); + + // Newline representations + expect(parser).to.parse("1\nx", { // Unix + start: { offset: 2, line: 2, column: 1 }, + end: { offset: 3, line: 2, column: 2 } + }); + expect(parser).to.parse("1\r\nx", { // Windows + start: { offset: 3, line: 2, column: 1 }, + end: { offset: 4, line: 2, column: 2 } + }); + }); + + describe("|expected|", function() { + it("terminates parsing and throws an exception", function() { + let parser = peg.generate( + "start = 'a' { expected('a'); }", + options + ); + + expect(parser).to.failToParse("a", { + message: "Expected a but \"a\" found.", + expected: [{ type: "other", description: "a" }], + found: "a", + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 1, line: 1, column: 2 } + } + }); + }); + + it("allows to set custom location info", function() { + let parser = peg.generate([ + "start = 'a' {", + " expected('a', {", + " start: { offset: 1, line: 1, column: 2 },", + " end: { offset: 2, line: 1, column: 3 }", + " });", + "}" + ].join("\n"), options); + + expect(parser).to.failToParse("a", { + message: "Expected a but \"a\" found.", + expected: [{ type: "other", description: "a" }], + found: "a", + location: { + start: { offset: 1, line: 1, column: 2 }, + end: { offset: 2, line: 1, column: 3 } + } + }); + }); + }); + + describe("|error|", function() { + it("terminates parsing and throws an exception", function() { + let parser = peg.generate( + "start = 'a' { error('a'); }", + options + ); + + expect(parser).to.failToParse("a", { + message: "a", + found: null, + expected: null, + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 1, line: 1, column: 2 } + } + }); + }); + + it("allows to set custom location info", function() { + let parser = peg.generate([ + "start = 'a' {", + " error('a', {", + " start: { offset: 1, line: 1, column: 2 },", + " end: { offset: 2, line: 1, column: 3 }", + " });", + "}" + ].join("\n"), options); + + expect(parser).to.failToParse("a", { + message: "a", + expected: null, + found: null, + location: { + start: { offset: 1, line: 1, column: 2 }, + end: { offset: 2, line: 1, column: 3 } + } + }); + }); + }); + }); + }); + + describe("when the expression doesn't match", function() { + it("reports match failure", function() { + let parser = peg.generate("start = 'a' { return 42; }", options); + + expect(parser).to.failToParse("b"); + }); + + it("doesn't execute the code", function() { + let parser = peg.generate( + "start = 'a' { throw 'Boom!'; } / 'b'", + options + ); + + expect(parser).to.parse("b"); + }); + }); + }); + + describe("choice", function() { + describe("when any expression matches", function() { + it("returns its match result", function() { + let parser = peg.generate("start = 'a' / 'b' / 'c'", options); + + expect(parser).to.parse("a", "a"); + expect(parser).to.parse("b", "b"); + expect(parser).to.parse("c", "c"); + }); + }); + + describe("when all expressions don't match", function() { + it("reports match failure", function() { + let parser = peg.generate("start = 'a' / 'b' / 'c'", options); + + expect(parser).to.failToParse("d"); + }); + }); + }); + + describe("error reporting", function() { + describe("behavior", function() { + it("reports only the rightmost error", function() { + let parser = peg.generate("start = 'a' 'b' / 'a' 'c' 'd'", options); + + expect(parser).to.failToParse("ace", { + expected: [{ type: "literal", text: "d", ignoreCase: false }] + }); + }); + }); + + describe("expectations reporting", function() { + it("reports expectations correctly with no alternative", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("ab", { + expected: [{ type: "end" }] + }); + }); + + it("reports expectations correctly with one alternative", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("b", { + expected: [{ type: "literal", text: "a", ignoreCase: false }] + }); + }); + + it("reports expectations correctly with multiple alternatives", function() { + let parser = peg.generate("start = 'a' / 'b' / 'c'", options); + + expect(parser).to.failToParse("d", { + expected: [ + { type: "literal", text: "a", ignoreCase: false }, + { type: "literal", text: "b", ignoreCase: false }, + { type: "literal", text: "c", ignoreCase: false } + ] + }); + }); + }); + + describe("found string reporting", function() { + it("reports found string correctly at the end of input", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("", { found: null }); + }); + + it("reports found string correctly in the middle of input", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("b", { found: "b" }); + }); + }); + + describe("message building", function() { + it("builds message correctly with no alternative", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("ab", { + message: "Expected end of input but \"b\" found." + }); + }); + + it("builds message correctly with one alternative", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("b", { + message: "Expected \"a\" but \"b\" found." + }); + }); + + it("builds message correctly with multiple alternatives", function() { + let parser = peg.generate("start = 'a' / 'b' / 'c'", options); + + expect(parser).to.failToParse("d", { + message: "Expected \"a\", \"b\", or \"c\" but \"d\" found." + }); + }); + + it("builds message correctly at the end of input", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("", { + message: "Expected \"a\" but end of input found." + }); + }); + + it("builds message correctly in the middle of input", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("b", { + message: "Expected \"a\" but \"b\" found." + }); + }); + + it("removes duplicates from expectations", function() { + let parser = peg.generate("start = 'a' / 'a'", options); + + expect(parser).to.failToParse("b", { + message: "Expected \"a\" but \"b\" found." + }); + }); + + it("sorts expectations", function() { + let parser = peg.generate("start = 'c' / 'b' / 'a'", options); + + expect(parser).to.failToParse("d", { + message: "Expected \"a\", \"b\", or \"c\" but \"d\" found." + }); + }); + }); + + describe("position reporting", function() { + it("reports position correctly at the end of input", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("", { + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 0, line: 1, column: 1 } + } + }); + }); + + it("reports position correctly in the middle of input", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("b", { + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 1, line: 1, column: 2 } + } + }); + }); + + it("reports position correctly with trailing input", function() { + let parser = peg.generate("start = 'a'", options); + + expect(parser).to.failToParse("aa", { + location: { + start: { offset: 1, line: 1, column: 2 }, + end: { offset: 2, line: 1, column: 3 } + } + }); + }); + + it("reports position correctly in complex cases", function() { + let parser = peg.generate([ + "start = line (nl+ line)*", + "line = digit (' '+ digit)*", + "digit = [0-9]", + "nl = '\\r'? '\\n'" + ].join("\n"), options); + + expect(parser).to.failToParse("1\n2\n\n3\n\n\n4 5 x", { + location: { + start: { offset: 13, line: 7, column: 5 }, + end: { offset: 14, line: 7, column: 6 } + } + }); + + // Newline representations + expect(parser).to.failToParse("1\nx", { // Old Mac + location: { + start: { offset: 2, line: 2, column: 1 }, + end: { offset: 3, line: 2, column: 2 } + } + }); + expect(parser).to.failToParse("1\r\nx", { // Windows + location: { + start: { offset: 3, line: 2, column: 1 }, + end: { offset: 4, line: 2, column: 2 } + } + }); + }); + }); + }); + + // Following examples are from Wikipedia, see + // http://en.wikipedia.org/w/index.php?title=Parsing_expression_grammar&oldid=335106938. + describe("complex examples", function() { + it("handles arithmetics example correctly", function() { + // Value ← [0-9]+ / '(' Expr ')' + // Product ← Value (('*' / '/') Value)* + // Sum ← Product (('+' / '-') Product)* + // Expr ← Sum + let parser = peg.generate([ + "Expr = Sum", + "Sum = head:Product tail:(('+' / '-') Product)* {", + " return tail.reduce(function(result, element) {", + " if (element[0] === '+') { return result + element[1]; }", + " if (element[0] === '-') { return result - element[1]; }", + " }, head);", + " }", + "Product = head:Value tail:(('*' / '/') Value)* {", + " return tail.reduce(function(result, element) {", + " if (element[0] === '*') { return result * element[1]; }", + " if (element[0] === '/') { return result / element[1]; }", + " }, head);", + " }", + "Value = digits:[0-9]+ { return parseInt(digits.join(''), 10); }", + " / '(' expr:Expr ')' { return expr; }" + ].join("\n"), options); + + // The "value" rule + expect(parser).to.parse("0", 0); + expect(parser).to.parse("123", 123); + expect(parser).to.parse("(42+43)", 42 + 43); + + // The "product" rule + expect(parser).to.parse("42", 42); + expect(parser).to.parse("42*43", 42 * 43); + expect(parser).to.parse("42*43*44*45", 42 * 43 * 44 * 45); + expect(parser).to.parse("42/43", 42 / 43); + expect(parser).to.parse("42/43/44/45", 42 / 43 / 44 / 45); + + // The "sum" rule + expect(parser).to.parse("42*43", 42 * 43); + expect(parser).to.parse("42*43+44*45", 42 * 43 + 44 * 45); + expect(parser).to.parse("42*43+44*45+46*47+48*49", 42 * 43 + 44 * 45 + 46 * 47 + 48 * 49); + expect(parser).to.parse("42*43-44*45", 42 * 43 - 44 * 45); + expect(parser).to.parse("42*43-44*45-46*47-48*49", 42 * 43 - 44 * 45 - 46 * 47 - 48 * 49); + + // The "expr" rule + expect(parser).to.parse("42+43", 42 + 43); + + // Complex test + expect(parser).to.parse("(1+2)*(3+4)", (1 + 2) * (3 + 4)); + }); + + it("handles non-context-free language correctly", function() { + // The following parsing expression grammar describes the classic + // non-context-free language { a^n b^n c^n : n >= 1 }: + // + // S ← &(A c) a+ B !(a/b/c) + // A ← a A? b + // B ← b B? c + let parser = peg.generate([ + "S = &(A 'c') a:'a'+ B:B !('a' / 'b' / 'c') { return a.join('') + B; }", + "A = a:'a' A:A? b:'b' { return [a, A, b].join(''); }", + "B = b:'b' B:B? c:'c' { return [b, B, c].join(''); }" + ].join("\n"), options); + + expect(parser).to.parse("abc", "abc"); + expect(parser).to.parse("aaabbbccc", "aaabbbccc"); + expect(parser).to.failToParse("aabbbccc"); + expect(parser).to.failToParse("aaaabbbccc"); + expect(parser).to.failToParse("aaabbccc"); + expect(parser).to.failToParse("aaabbbbccc"); + expect(parser).to.failToParse("aaabbbcc"); + expect(parser).to.failToParse("aaabbbcccc"); + }); + + it("handles nested comments example correctly", function() { + // Begin ← "(*" + // End ← "*)" + // C ← Begin N* End + // N ← C / (!Begin !End Z) + // Z ← any single character + let parser = peg.generate([ + "C = begin:Begin ns:N* end:End { return begin + ns.join('') + end; }", + "N = C", + " / !Begin !End z:Z { return z; }", + "Z = .", + "Begin = '(*'", + "End = '*)'" + ].join("\n"), options); + + expect(parser).to.parse("(**)", "(**)"); + expect(parser).to.parse("(*abc*)", "(*abc*)"); + expect(parser).to.parse("(*(**)*)", "(*(**)*)"); + expect(parser).to.parse( + "(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)", + "(*abc(*def*)ghi(*(*(*jkl*)*)*)mno*)" + ); + }); + }); + }); }); diff --git a/test/unit/compiler/passes/generate-bytecode.spec.js b/test/unit/compiler/passes/generate-bytecode.spec.js index e9d8f3c..5c1db08 100644 --- a/test/unit/compiler/passes/generate-bytecode.spec.js +++ b/test/unit/compiler/passes/generate-bytecode.spec.js @@ -9,648 +9,648 @@ chai.use(helpers); let expect = chai.expect; describe("compiler pass |generateBytecode|", function() { - function bytecodeDetails(bytecode) { - return { - rules: [{ bytecode: bytecode }] - }; - } - - function constsDetails(consts) { return { consts: consts }; } - - describe("for grammar", function() { - it("generates correct bytecode", function() { - expect(pass).to.changeAST([ - "a = 'a'", - "b = 'b'", - "c = 'c'" - ].join("\n"), { - rules: [ - { bytecode: [18, 0, 2, 2, 22, 0, 23, 1] }, - { bytecode: [18, 2, 2, 2, 22, 2, 23, 3] }, - { bytecode: [18, 4, 2, 2, 22, 4, 23, 5] } - ] - }); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST([ - "a = 'a'", - "b = 'b'", - "c = 'c'" - ].join("\n"), constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)", - "\"b\"", - "peg$literalExpectation(\"b\", false)", - "\"c\"", - "peg$literalExpectation(\"c\", false)" - ])); - }); - }); - - describe("for rule", function() { - it("generates correct bytecode", function() { - expect(pass).to.changeAST("start = 'a'", bytecodeDetails([ - 18, 0, 2, 2, 22, 0, 23, 1 // - ])); - }); - }); - - describe("for named", function() { - let grammar = "start 'start' = 'a'"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 28, // SILENT_FAILS_ON - 18, 1, 2, 2, 22, 1, 23, 2, // - 29, // SILENT_FAILS_OFF - 14, 2, 0, // IF_ERROR - 23, 0 // * FAIL - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "peg$otherExpectation(\"start\")", - "\"a\"", - "peg$literalExpectation(\"a\", false)" - ])); - }); - }); - - describe("for choice", function() { - it("generates correct bytecode", function() { - expect(pass).to.changeAST("start = 'a' / 'b' / 'c'", bytecodeDetails([ - 18, 0, 2, 2, 22, 0, 23, 1, // - 14, 21, 0, // IF_ERROR - 6, // * POP - 18, 2, 2, 2, 22, 2, 23, 3, // - 14, 9, 0, // IF_ERROR - 6, // * POP - 18, 4, 2, 2, 22, 4, 23, 5 // - ])); - }); - }); - - describe("for action", function() { - describe("without labels", function() { - let grammar = "start = 'a' { code }"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 5, // PUSH_CURR_POS - 18, 0, 2, 2, 22, 0, 23, 1, // - 15, 6, 0, // IF_NOT_ERROR - 24, 1, // * LOAD_SAVED_POS - 26, 2, 1, 0, // CALL - 9 // NIP - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)", - "function() { code }" - ])); - }); - }); - - describe("with one label", function() { - let grammar = "start = a:'a' { code }"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 5, // PUSH_CURR_POS - 18, 0, 2, 2, 22, 0, 23, 1, // - 15, 7, 0, // IF_NOT_ERROR - 24, 1, // * LOAD_SAVED_POS - 26, 2, 1, 1, 0, // CALL - 9 // NIP - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)", - "function(a) { code }" - ])); - }); - }); - - describe("with multiple labels", function() { - let grammar = "start = a:'a' b:'b' c:'c' { code }"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 5, // PUSH_CURR_POS - 18, 0, 2, 2, 22, 0, 23, 1, // - 15, 40, 3, // IF_NOT_ERROR - 18, 2, 2, 2, 22, 2, 23, 3, // * - 15, 25, 4, // IF_NOT_ERROR - 18, 4, 2, 2, 22, 4, 23, 5, // * - 15, 10, 4, // IF_NOT_ERROR - 24, 3, // * LOAD_SAVED_POS - 26, 6, 3, 3, 2, 1, 0, // CALL - 9, // NIP - 8, 3, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 8, 2, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 6, // * POP - 7, // POP_CURR_POS - 3 // PUSH_FAILED - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)", - "\"b\"", - "peg$literalExpectation(\"b\", false)", - "\"c\"", - "peg$literalExpectation(\"c\", false)", - "function(a, b, c) { code }" - ])); - }); - }); - }); - - describe("for sequence", function() { - let grammar = "start = 'a' 'b' 'c'"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 5, // PUSH_CURR_POS - 18, 0, 2, 2, 22, 0, 23, 1, // - 15, 33, 3, // IF_NOT_ERROR - 18, 2, 2, 2, 22, 2, 23, 3, // * - 15, 18, 4, // IF_NOT_ERROR - 18, 4, 2, 2, 22, 4, 23, 5, // * - 15, 3, 4, // IF_NOT_ERROR - 11, 3, // * WRAP - 9, // NIP - 8, 3, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 8, 2, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 6, // * POP - 7, // POP_CURR_POS - 3 // PUSH_FAILED - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)", - "\"b\"", - "peg$literalExpectation(\"b\", false)", - "\"c\"", - "peg$literalExpectation(\"c\", false)" - ])); - }); - }); - - describe("for labeled", function() { - it("generates correct bytecode", function() { - expect(pass).to.changeAST("start = a:'a'", bytecodeDetails([ - 18, 0, 2, 2, 22, 0, 23, 1 // - ])); - }); - }); - - describe("for text", function() { - it("generates correct bytecode", function() { - expect(pass).to.changeAST("start = $'a'", bytecodeDetails([ - 5, // PUSH_CURR_POS - 18, 0, 2, 2, 22, 0, 23, 1, // - 15, 2, 1, // IF_NOT_ERROR - 6, // * POP - 12, // TEXT - 9 // * NIP - ])); - }); - }); - - describe("for simple_and", function() { - let grammar = "start = &'a'"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 5, // PUSH_CURR_POS - 28, // SILENT_FAILS_ON - 18, 0, 2, 2, 22, 0, 23, 1, // - 29, // SILENT_FAILS_OFF - 15, 3, 3, // IF_NOT_ERROR - 6, // * POP - 7, // POP_CURR_POS - 1, // PUSH_UNDEFINED - 6, // * POP - 6, // POP - 3 // PUSH_FAILED - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)" - ])); - }); - }); - - describe("for simple_not", function() { - let grammar = "start = !'a'"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 5, // PUSH_CURR_POS - 28, // SILENT_FAILS_ON - 18, 0, 2, 2, 22, 0, 23, 1, // - 29, // SILENT_FAILS_OFF - 14, 3, 3, // IF_ERROR - 6, // * POP - 6, // POP - 1, // PUSH_UNDEFINED - 6, // * POP - 7, // POP_CURR_POS - 3 // PUSH_FAILED - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)" - ])); - }); - }); - - describe("for optional", function() { - let grammar = "start = 'a'?"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 18, 0, 2, 2, 22, 0, 23, 1, // - 14, 2, 0, // IF_ERROR - 6, // * POP - 2 // PUSH_NULL - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)" - ])); - }); - }); - - describe("for zero_or_more", function() { - let grammar = "start = 'a'*"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 4, // PUSH_EMPTY_ARRAY - 18, 0, 2, 2, 22, 0, 23, 1, // - 16, 9, // WHILE_NOT_ERROR - 10, // * APPEND - 18, 0, 2, 2, 22, 0, 23, 1, // - 6 // POP - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)" - ])); - }); - }); - - describe("for one_or_more", function() { - let grammar = "start = 'a'+"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 4, // PUSH_EMPTY_ARRAY - 18, 0, 2, 2, 22, 0, 23, 1, // - 15, 12, 3, // IF_NOT_ERROR - 16, 9, // * WHILE_NOT_ERROR - 10, // * APPEND - 18, 0, 2, 2, 22, 0, 23, 1, // - 6, // POP - 6, // * POP - 6, // POP - 3 // PUSH_FAILED - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)" - ])); - }); - }); - - describe("for group", function() { - it("generates correct bytecode", function() { - expect(pass).to.changeAST("start = ('a')", bytecodeDetails([ - 18, 0, 2, 2, 22, 0, 23, 1 // - ])); - }); - }); - - describe("for semantic_and", function() { - describe("without labels", function() { - let grammar = "start = &{ code }"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 25, // UPDATE_SAVED_POS - 26, 0, 0, 0, // CALL - 13, 2, 2, // IF - 6, // * POP - 1, // PUSH_UNDEFINED - 6, // * POP - 3 // PUSH_FAILED - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST( - grammar, - constsDetails(["function() { code }"]) - ); - }); - }); - - describe("with labels", function() { - let grammar = "start = a:'a' b:'b' c:'c' &{ code }"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 5, // PUSH_CURR_POS - 18, 0, 2, 2, 22, 0, 23, 1, // - 15, 55, 3, // IF_NOT_ERROR - 18, 2, 2, 2, 22, 2, 23, 3, // * - 15, 40, 4, // IF_NOT_ERROR - 18, 4, 2, 2, 22, 4, 23, 5, // * - 15, 25, 4, // IF_NOT_ERROR - 25, // * UPDATE_SAVED_POS - 26, 6, 0, 3, 2, 1, 0, // CALL - 13, 2, 2, // IF - 6, // * POP - 1, // PUSH_UNDEFINED - 6, // * POP - 3, // PUSH_FAILED - 15, 3, 4, // IF_NOT_ERROR - 11, 4, // * WRAP - 9, // NIP - 8, 4, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 8, 3, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 8, 2, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 6, // * POP - 7, // POP_CURR_POS - 3 // PUSH_FAILED - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)", - "\"b\"", - "peg$literalExpectation(\"b\", false)", - "\"c\"", - "peg$literalExpectation(\"c\", false)", - "function(a, b, c) { code }" - ])); - }); - }); - }); - - describe("for semantic_not", function() { - describe("without labels", function() { - let grammar = "start = !{ code }"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 25, // UPDATE_SAVED_POS - 26, 0, 0, 0, // CALL - 13, 2, 2, // IF - 6, // * POP - 3, // PUSH_FAILED - 6, // * POP - 1 // PUSH_UNDEFINED - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST( - grammar, - constsDetails(["function() { code }"]) - ); - }); - }); - - describe("with labels", function() { - let grammar = "start = a:'a' b:'b' c:'c' !{ code }"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 5, // PUSH_CURR_POS - 18, 0, 2, 2, 22, 0, 23, 1, // - 15, 55, 3, // IF_NOT_ERROR - 18, 2, 2, 2, 22, 2, 23, 3, // * - 15, 40, 4, // IF_NOT_ERROR - 18, 4, 2, 2, 22, 4, 23, 5, // * - 15, 25, 4, // IF_NOT_ERROR - 25, // * UPDATE_SAVED_POS - 26, 6, 0, 3, 2, 1, 0, // CALL - 13, 2, 2, // IF - 6, // * POP - 3, // PUSH_FAILED - 6, // * POP - 1, // PUSH_UNDEFINED - 15, 3, 4, // IF_NOT_ERROR - 11, 4, // * WRAP - 9, // NIP - 8, 4, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 8, 3, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 8, 2, // * POP_N - 7, // POP_CURR_POS - 3, // PUSH_FAILED - 6, // * POP - 7, // POP_CURR_POS - 3 // PUSH_FAILED - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)", - "\"b\"", - "peg$literalExpectation(\"b\", false)", - "\"c\"", - "peg$literalExpectation(\"c\", false)", - "function(a, b, c) { code }" - ])); - }); - }); - }); - - describe("for rule_ref", function() { - it("generates correct bytecode", function() { - expect(pass).to.changeAST([ - "start = other", - "other = 'other'" - ].join("\n"), { - rules: [ - { - bytecode: [27, 1] // RULE - }, - { } - ] - }); - }); - }); - - describe("for literal", function() { - describe("empty", function() { - let grammar = "start = ''"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 0, 0 // PUSH - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails(["\"\""])); - }); - }); - - describe("non-empty case-sensitive", function() { - let grammar = "start = 'a'"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 18, 0, 2, 2, // MATCH_STRING - 22, 0, // * ACCEPT_STRING - 23, 1 // * FAIL - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"a\", false)" - ])); - }); - }); - - describe("non-empty case-insensitive", function() { - let grammar = "start = 'A'i"; - - it("generates correct bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 19, 0, 2, 2, // MATCH_STRING_IC - 21, 1, // * ACCEPT_N - 23, 1 // * FAIL - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST(grammar, constsDetails([ - "\"a\"", - "peg$literalExpectation(\"A\", true)" - ])); - }); - }); - }); - - describe("for class", function() { - it("generates correct bytecode", function() { - expect(pass).to.changeAST("start = [a]", bytecodeDetails([ - 20, 0, 2, 2, // MATCH_REGEXP - 21, 1, // * ACCEPT_N - 23, 1 // * FAIL - ])); - }); - - describe("non-inverted case-sensitive", function() { - it("defines correct constants", function() { - expect(pass).to.changeAST("start = [a]", constsDetails([ - "/^[a]/", - "peg$classExpectation([\"a\"], false, false)" - ])); - }); - }); - - describe("inverted case-sensitive", function() { - it("defines correct constants", function() { - expect(pass).to.changeAST("start = [^a]", constsDetails([ - "/^[^a]/", - "peg$classExpectation([\"a\"], true, false)" - ])); - }); - }); - - describe("non-inverted case-insensitive", function() { - it("defines correct constants", function() { - expect(pass).to.changeAST("start = [a]i", constsDetails([ - "/^[a]/i", - "peg$classExpectation([\"a\"], false, true)" - ])); - }); - }); - - describe("complex", function() { - it("defines correct constants", function() { - expect(pass).to.changeAST("start = [ab-def-hij-l]", constsDetails([ - "/^[ab-def-hij-l]/", - "peg$classExpectation([\"a\", [\"b\", \"d\"], \"e\", [\"f\", \"h\"], \"i\", [\"j\", \"l\"]], false, false)" - ])); - }); - }); - }); - - describe("for any", function() { - let grammar = "start = ."; - - it("generates bytecode", function() { - expect(pass).to.changeAST(grammar, bytecodeDetails([ - 17, 2, 2, // MATCH_ANY - 21, 1, // * ACCEPT_N - 23, 0 // * FAIL - ])); - }); - - it("defines correct constants", function() { - expect(pass).to.changeAST( - grammar, - constsDetails(["peg$anyExpectation()"]) - ); - }); - }); + function bytecodeDetails(bytecode) { + return { + rules: [{ bytecode: bytecode }] + }; + } + + function constsDetails(consts) { return { consts: consts }; } + + describe("for grammar", function() { + it("generates correct bytecode", function() { + expect(pass).to.changeAST([ + "a = 'a'", + "b = 'b'", + "c = 'c'" + ].join("\n"), { + rules: [ + { bytecode: [18, 0, 2, 2, 22, 0, 23, 1] }, + { bytecode: [18, 2, 2, 2, 22, 2, 23, 3] }, + { bytecode: [18, 4, 2, 2, 22, 4, 23, 5] } + ] + }); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST([ + "a = 'a'", + "b = 'b'", + "c = 'c'" + ].join("\n"), constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)", + "\"b\"", + "peg$literalExpectation(\"b\", false)", + "\"c\"", + "peg$literalExpectation(\"c\", false)" + ])); + }); + }); + + describe("for rule", function() { + it("generates correct bytecode", function() { + expect(pass).to.changeAST("start = 'a'", bytecodeDetails([ + 18, 0, 2, 2, 22, 0, 23, 1 // + ])); + }); + }); + + describe("for named", function() { + let grammar = "start 'start' = 'a'"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 28, // SILENT_FAILS_ON + 18, 1, 2, 2, 22, 1, 23, 2, // + 29, // SILENT_FAILS_OFF + 14, 2, 0, // IF_ERROR + 23, 0 // * FAIL + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "peg$otherExpectation(\"start\")", + "\"a\"", + "peg$literalExpectation(\"a\", false)" + ])); + }); + }); + + describe("for choice", function() { + it("generates correct bytecode", function() { + expect(pass).to.changeAST("start = 'a' / 'b' / 'c'", bytecodeDetails([ + 18, 0, 2, 2, 22, 0, 23, 1, // + 14, 21, 0, // IF_ERROR + 6, // * POP + 18, 2, 2, 2, 22, 2, 23, 3, // + 14, 9, 0, // IF_ERROR + 6, // * POP + 18, 4, 2, 2, 22, 4, 23, 5 // + ])); + }); + }); + + describe("for action", function() { + describe("without labels", function() { + let grammar = "start = 'a' { code }"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 5, // PUSH_CURR_POS + 18, 0, 2, 2, 22, 0, 23, 1, // + 15, 6, 0, // IF_NOT_ERROR + 24, 1, // * LOAD_SAVED_POS + 26, 2, 1, 0, // CALL + 9 // NIP + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)", + "function() { code }" + ])); + }); + }); + + describe("with one label", function() { + let grammar = "start = a:'a' { code }"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 5, // PUSH_CURR_POS + 18, 0, 2, 2, 22, 0, 23, 1, // + 15, 7, 0, // IF_NOT_ERROR + 24, 1, // * LOAD_SAVED_POS + 26, 2, 1, 1, 0, // CALL + 9 // NIP + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)", + "function(a) { code }" + ])); + }); + }); + + describe("with multiple labels", function() { + let grammar = "start = a:'a' b:'b' c:'c' { code }"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 5, // PUSH_CURR_POS + 18, 0, 2, 2, 22, 0, 23, 1, // + 15, 40, 3, // IF_NOT_ERROR + 18, 2, 2, 2, 22, 2, 23, 3, // * + 15, 25, 4, // IF_NOT_ERROR + 18, 4, 2, 2, 22, 4, 23, 5, // * + 15, 10, 4, // IF_NOT_ERROR + 24, 3, // * LOAD_SAVED_POS + 26, 6, 3, 3, 2, 1, 0, // CALL + 9, // NIP + 8, 3, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 8, 2, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 6, // * POP + 7, // POP_CURR_POS + 3 // PUSH_FAILED + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)", + "\"b\"", + "peg$literalExpectation(\"b\", false)", + "\"c\"", + "peg$literalExpectation(\"c\", false)", + "function(a, b, c) { code }" + ])); + }); + }); + }); + + describe("for sequence", function() { + let grammar = "start = 'a' 'b' 'c'"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 5, // PUSH_CURR_POS + 18, 0, 2, 2, 22, 0, 23, 1, // + 15, 33, 3, // IF_NOT_ERROR + 18, 2, 2, 2, 22, 2, 23, 3, // * + 15, 18, 4, // IF_NOT_ERROR + 18, 4, 2, 2, 22, 4, 23, 5, // * + 15, 3, 4, // IF_NOT_ERROR + 11, 3, // * WRAP + 9, // NIP + 8, 3, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 8, 2, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 6, // * POP + 7, // POP_CURR_POS + 3 // PUSH_FAILED + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)", + "\"b\"", + "peg$literalExpectation(\"b\", false)", + "\"c\"", + "peg$literalExpectation(\"c\", false)" + ])); + }); + }); + + describe("for labeled", function() { + it("generates correct bytecode", function() { + expect(pass).to.changeAST("start = a:'a'", bytecodeDetails([ + 18, 0, 2, 2, 22, 0, 23, 1 // + ])); + }); + }); + + describe("for text", function() { + it("generates correct bytecode", function() { + expect(pass).to.changeAST("start = $'a'", bytecodeDetails([ + 5, // PUSH_CURR_POS + 18, 0, 2, 2, 22, 0, 23, 1, // + 15, 2, 1, // IF_NOT_ERROR + 6, // * POP + 12, // TEXT + 9 // * NIP + ])); + }); + }); + + describe("for simple_and", function() { + let grammar = "start = &'a'"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 5, // PUSH_CURR_POS + 28, // SILENT_FAILS_ON + 18, 0, 2, 2, 22, 0, 23, 1, // + 29, // SILENT_FAILS_OFF + 15, 3, 3, // IF_NOT_ERROR + 6, // * POP + 7, // POP_CURR_POS + 1, // PUSH_UNDEFINED + 6, // * POP + 6, // POP + 3 // PUSH_FAILED + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)" + ])); + }); + }); + + describe("for simple_not", function() { + let grammar = "start = !'a'"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 5, // PUSH_CURR_POS + 28, // SILENT_FAILS_ON + 18, 0, 2, 2, 22, 0, 23, 1, // + 29, // SILENT_FAILS_OFF + 14, 3, 3, // IF_ERROR + 6, // * POP + 6, // POP + 1, // PUSH_UNDEFINED + 6, // * POP + 7, // POP_CURR_POS + 3 // PUSH_FAILED + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)" + ])); + }); + }); + + describe("for optional", function() { + let grammar = "start = 'a'?"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 18, 0, 2, 2, 22, 0, 23, 1, // + 14, 2, 0, // IF_ERROR + 6, // * POP + 2 // PUSH_NULL + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)" + ])); + }); + }); + + describe("for zero_or_more", function() { + let grammar = "start = 'a'*"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 4, // PUSH_EMPTY_ARRAY + 18, 0, 2, 2, 22, 0, 23, 1, // + 16, 9, // WHILE_NOT_ERROR + 10, // * APPEND + 18, 0, 2, 2, 22, 0, 23, 1, // + 6 // POP + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)" + ])); + }); + }); + + describe("for one_or_more", function() { + let grammar = "start = 'a'+"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 4, // PUSH_EMPTY_ARRAY + 18, 0, 2, 2, 22, 0, 23, 1, // + 15, 12, 3, // IF_NOT_ERROR + 16, 9, // * WHILE_NOT_ERROR + 10, // * APPEND + 18, 0, 2, 2, 22, 0, 23, 1, // + 6, // POP + 6, // * POP + 6, // POP + 3 // PUSH_FAILED + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)" + ])); + }); + }); + + describe("for group", function() { + it("generates correct bytecode", function() { + expect(pass).to.changeAST("start = ('a')", bytecodeDetails([ + 18, 0, 2, 2, 22, 0, 23, 1 // + ])); + }); + }); + + describe("for semantic_and", function() { + describe("without labels", function() { + let grammar = "start = &{ code }"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 25, // UPDATE_SAVED_POS + 26, 0, 0, 0, // CALL + 13, 2, 2, // IF + 6, // * POP + 1, // PUSH_UNDEFINED + 6, // * POP + 3 // PUSH_FAILED + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST( + grammar, + constsDetails(["function() { code }"]) + ); + }); + }); + + describe("with labels", function() { + let grammar = "start = a:'a' b:'b' c:'c' &{ code }"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 5, // PUSH_CURR_POS + 18, 0, 2, 2, 22, 0, 23, 1, // + 15, 55, 3, // IF_NOT_ERROR + 18, 2, 2, 2, 22, 2, 23, 3, // * + 15, 40, 4, // IF_NOT_ERROR + 18, 4, 2, 2, 22, 4, 23, 5, // * + 15, 25, 4, // IF_NOT_ERROR + 25, // * UPDATE_SAVED_POS + 26, 6, 0, 3, 2, 1, 0, // CALL + 13, 2, 2, // IF + 6, // * POP + 1, // PUSH_UNDEFINED + 6, // * POP + 3, // PUSH_FAILED + 15, 3, 4, // IF_NOT_ERROR + 11, 4, // * WRAP + 9, // NIP + 8, 4, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 8, 3, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 8, 2, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 6, // * POP + 7, // POP_CURR_POS + 3 // PUSH_FAILED + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)", + "\"b\"", + "peg$literalExpectation(\"b\", false)", + "\"c\"", + "peg$literalExpectation(\"c\", false)", + "function(a, b, c) { code }" + ])); + }); + }); + }); + + describe("for semantic_not", function() { + describe("without labels", function() { + let grammar = "start = !{ code }"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 25, // UPDATE_SAVED_POS + 26, 0, 0, 0, // CALL + 13, 2, 2, // IF + 6, // * POP + 3, // PUSH_FAILED + 6, // * POP + 1 // PUSH_UNDEFINED + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST( + grammar, + constsDetails(["function() { code }"]) + ); + }); + }); + + describe("with labels", function() { + let grammar = "start = a:'a' b:'b' c:'c' !{ code }"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 5, // PUSH_CURR_POS + 18, 0, 2, 2, 22, 0, 23, 1, // + 15, 55, 3, // IF_NOT_ERROR + 18, 2, 2, 2, 22, 2, 23, 3, // * + 15, 40, 4, // IF_NOT_ERROR + 18, 4, 2, 2, 22, 4, 23, 5, // * + 15, 25, 4, // IF_NOT_ERROR + 25, // * UPDATE_SAVED_POS + 26, 6, 0, 3, 2, 1, 0, // CALL + 13, 2, 2, // IF + 6, // * POP + 3, // PUSH_FAILED + 6, // * POP + 1, // PUSH_UNDEFINED + 15, 3, 4, // IF_NOT_ERROR + 11, 4, // * WRAP + 9, // NIP + 8, 4, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 8, 3, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 8, 2, // * POP_N + 7, // POP_CURR_POS + 3, // PUSH_FAILED + 6, // * POP + 7, // POP_CURR_POS + 3 // PUSH_FAILED + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)", + "\"b\"", + "peg$literalExpectation(\"b\", false)", + "\"c\"", + "peg$literalExpectation(\"c\", false)", + "function(a, b, c) { code }" + ])); + }); + }); + }); + + describe("for rule_ref", function() { + it("generates correct bytecode", function() { + expect(pass).to.changeAST([ + "start = other", + "other = 'other'" + ].join("\n"), { + rules: [ + { + bytecode: [27, 1] // RULE + }, + { } + ] + }); + }); + }); + + describe("for literal", function() { + describe("empty", function() { + let grammar = "start = ''"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 0, 0 // PUSH + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails(["\"\""])); + }); + }); + + describe("non-empty case-sensitive", function() { + let grammar = "start = 'a'"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 18, 0, 2, 2, // MATCH_STRING + 22, 0, // * ACCEPT_STRING + 23, 1 // * FAIL + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"a\", false)" + ])); + }); + }); + + describe("non-empty case-insensitive", function() { + let grammar = "start = 'A'i"; + + it("generates correct bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 19, 0, 2, 2, // MATCH_STRING_IC + 21, 1, // * ACCEPT_N + 23, 1 // * FAIL + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST(grammar, constsDetails([ + "\"a\"", + "peg$literalExpectation(\"A\", true)" + ])); + }); + }); + }); + + describe("for class", function() { + it("generates correct bytecode", function() { + expect(pass).to.changeAST("start = [a]", bytecodeDetails([ + 20, 0, 2, 2, // MATCH_REGEXP + 21, 1, // * ACCEPT_N + 23, 1 // * FAIL + ])); + }); + + describe("non-inverted case-sensitive", function() { + it("defines correct constants", function() { + expect(pass).to.changeAST("start = [a]", constsDetails([ + "/^[a]/", + "peg$classExpectation([\"a\"], false, false)" + ])); + }); + }); + + describe("inverted case-sensitive", function() { + it("defines correct constants", function() { + expect(pass).to.changeAST("start = [^a]", constsDetails([ + "/^[^a]/", + "peg$classExpectation([\"a\"], true, false)" + ])); + }); + }); + + describe("non-inverted case-insensitive", function() { + it("defines correct constants", function() { + expect(pass).to.changeAST("start = [a]i", constsDetails([ + "/^[a]/i", + "peg$classExpectation([\"a\"], false, true)" + ])); + }); + }); + + describe("complex", function() { + it("defines correct constants", function() { + expect(pass).to.changeAST("start = [ab-def-hij-l]", constsDetails([ + "/^[ab-def-hij-l]/", + "peg$classExpectation([\"a\", [\"b\", \"d\"], \"e\", [\"f\", \"h\"], \"i\", [\"j\", \"l\"]], false, false)" + ])); + }); + }); + }); + + describe("for any", function() { + let grammar = "start = ."; + + it("generates bytecode", function() { + expect(pass).to.changeAST(grammar, bytecodeDetails([ + 17, 2, 2, // MATCH_ANY + 21, 1, // * ACCEPT_N + 23, 0 // * FAIL + ])); + }); + + it("defines correct constants", function() { + expect(pass).to.changeAST( + grammar, + constsDetails(["peg$anyExpectation()"]) + ); + }); + }); }); diff --git a/test/unit/compiler/passes/helpers.js b/test/unit/compiler/passes/helpers.js index 1eed9aa..18322ba 100644 --- a/test/unit/compiler/passes/helpers.js +++ b/test/unit/compiler/passes/helpers.js @@ -3,86 +3,86 @@ let parser = require("../../../../lib/parser"); module.exports = function(chai, utils) { - let Assertion = chai.Assertion; - - Assertion.addMethod("changeAST", function(grammar, props, options) { - options = options !== undefined ? options : {}; - - function matchProps(value, props) { - function isArray(value) { - return Object.prototype.toString.apply(value) === "[object Array]"; - } - - function isObject(value) { - return value !== null && typeof value === "object"; - } - - if (isArray(props)) { - if (!isArray(value)) { return false; } - - if (value.length !== props.length) { return false; } - for (let i = 0; i < props.length; i++) { - if (!matchProps(value[i], props[i])) { return false; } - } - - return true; - } else if (isObject(props)) { - if (!isObject(value)) { return false; } - - let keys = Object.keys(props); - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - - if (!(key in value)) { return false; } - - if (!matchProps(value[key], props[key])) { return false; } - } - - return true; - } else { - return value === props; - } - } - - let ast = parser.parse(grammar); - - utils.flag(this, "object")(ast, options); - - this.assert( - matchProps(ast, props), - "expected #{this} to change the AST to match #{exp}", - "expected #{this} to not change the AST to match #{exp}", - props, - ast - ); - }); - - Assertion.addMethod("reportError", function(grammar, props) { - let ast = parser.parse(grammar); - - let passed, result; - - try { - utils.flag(this, "object")(ast); - passed = true; - } catch (e) { - result = e; - passed = false; - } - - this.assert( - !passed, - "expected #{this} to report an error but it didn't", - "expected #{this} to not report an error but #{act} was reported", - null, - result - ); - - if (!passed && props !== undefined) { - Object.keys(props).forEach(key => { - new Assertion(result).to.have.property(key) - .that.is.deep.equal(props[key]); - }); - } - }); + let Assertion = chai.Assertion; + + Assertion.addMethod("changeAST", function(grammar, props, options) { + options = options !== undefined ? options : {}; + + function matchProps(value, props) { + function isArray(value) { + return Object.prototype.toString.apply(value) === "[object Array]"; + } + + function isObject(value) { + return value !== null && typeof value === "object"; + } + + if (isArray(props)) { + if (!isArray(value)) { return false; } + + if (value.length !== props.length) { return false; } + for (let i = 0; i < props.length; i++) { + if (!matchProps(value[i], props[i])) { return false; } + } + + return true; + } else if (isObject(props)) { + if (!isObject(value)) { return false; } + + let keys = Object.keys(props); + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + + if (!(key in value)) { return false; } + + if (!matchProps(value[key], props[key])) { return false; } + } + + return true; + } else { + return value === props; + } + } + + let ast = parser.parse(grammar); + + utils.flag(this, "object")(ast, options); + + this.assert( + matchProps(ast, props), + "expected #{this} to change the AST to match #{exp}", + "expected #{this} to not change the AST to match #{exp}", + props, + ast + ); + }); + + Assertion.addMethod("reportError", function(grammar, props) { + let ast = parser.parse(grammar); + + let passed, result; + + try { + utils.flag(this, "object")(ast); + passed = true; + } catch (e) { + result = e; + passed = false; + } + + this.assert( + !passed, + "expected #{this} to report an error but it didn't", + "expected #{this} to not report an error but #{act} was reported", + null, + result + ); + + if (!passed && props !== undefined) { + Object.keys(props).forEach(key => { + new Assertion(result).to.have.property(key) + .that.is.deep.equal(props[key]); + }); + } + }); }; diff --git a/test/unit/compiler/passes/remove-proxy-rules.spec.js b/test/unit/compiler/passes/remove-proxy-rules.spec.js index e7afc23..54803aa 100644 --- a/test/unit/compiler/passes/remove-proxy-rules.spec.js +++ b/test/unit/compiler/passes/remove-proxy-rules.spec.js @@ -9,51 +9,51 @@ chai.use(helpers); let expect = chai.expect; describe("compiler pass |removeProxyRules|", function() { - describe("when a proxy rule isn't listed in |allowedStartRules|", function() { - it("updates references and removes it", function() { - expect(pass).to.changeAST( - [ - "start = proxy", - "proxy = proxied", - "proxied = 'a'" - ].join("\n"), - { - rules: [ - { - name: "start", - expression: { type: "rule_ref", name: "proxied" } - }, - { name: "proxied" } - ] - }, - { allowedStartRules: ["start"] } - ); - }); - }); + describe("when a proxy rule isn't listed in |allowedStartRules|", function() { + it("updates references and removes it", function() { + expect(pass).to.changeAST( + [ + "start = proxy", + "proxy = proxied", + "proxied = 'a'" + ].join("\n"), + { + rules: [ + { + name: "start", + expression: { type: "rule_ref", name: "proxied" } + }, + { name: "proxied" } + ] + }, + { allowedStartRules: ["start"] } + ); + }); + }); - describe("when a proxy rule is listed in |allowedStartRules|", function() { - it("updates references but doesn't remove it", function() { - expect(pass).to.changeAST( - [ - "start = proxy", - "proxy = proxied", - "proxied = 'a'" - ].join("\n"), - { - rules: [ - { - name: "start", - expression: { type: "rule_ref", name: "proxied" } - }, - { - name: "proxy", - expression: { type: "rule_ref", name: "proxied" } - }, - { name: "proxied" } - ] - }, - { allowedStartRules: ["start", "proxy"] } - ); - }); - }); + describe("when a proxy rule is listed in |allowedStartRules|", function() { + it("updates references but doesn't remove it", function() { + expect(pass).to.changeAST( + [ + "start = proxy", + "proxy = proxied", + "proxied = 'a'" + ].join("\n"), + { + rules: [ + { + name: "start", + expression: { type: "rule_ref", name: "proxied" } + }, + { + name: "proxy", + expression: { type: "rule_ref", name: "proxied" } + }, + { name: "proxied" } + ] + }, + { allowedStartRules: ["start", "proxy"] } + ); + }); + }); }); diff --git a/test/unit/compiler/passes/report-duplicate-labels.spec.js b/test/unit/compiler/passes/report-duplicate-labels.spec.js index a775e89..8e0672b 100644 --- a/test/unit/compiler/passes/report-duplicate-labels.spec.js +++ b/test/unit/compiler/passes/report-duplicate-labels.spec.js @@ -9,55 +9,55 @@ chai.use(helpers); let expect = chai.expect; describe("compiler pass |reportDuplicateLabels|", function() { - describe("in a sequence", function() { - it("reports labels duplicate with labels of preceding elements", function() { - expect(pass).to.reportError("start = a:'a' a:'a'", { - message: "Label \"a\" is already defined at line 1, column 9.", - location: { - start: { offset: 14, line: 1, column: 15 }, - end: { offset: 19, line: 1, column: 20 } - } - }); - }); - - it("doesn't report labels duplicate with labels in subexpressions", function() { - expect(pass).to.not.reportError("start = ('a' / a:'a' / 'a') a:'a'"); - expect(pass).to.not.reportError("start = (a:'a' { }) a:'a'"); - expect(pass).to.not.reportError("start = ('a' a:'a' 'a') a:'a'"); - expect(pass).to.not.reportError("start = b:(a:'a') a:'a'"); - expect(pass).to.not.reportError("start = $(a:'a') a:'a'"); - expect(pass).to.not.reportError("start = &(a:'a') a:'a'"); - expect(pass).to.not.reportError("start = !(a:'a') a:'a'"); - expect(pass).to.not.reportError("start = (a:'a')? a:'a'"); - expect(pass).to.not.reportError("start = (a:'a')* a:'a'"); - expect(pass).to.not.reportError("start = (a:'a')+ a:'a'"); - expect(pass).to.not.reportError("start = (a:'a') a:'a'"); - }); - }); - - describe("in a choice", function() { - it("doesn't report labels duplicate with labels of preceding alternatives", function() { - expect(pass).to.not.reportError("start = a:'a' / a:'a'"); - }); - }); - - describe("in outer sequence", function() { - it("reports labels duplicate with labels of preceding elements", function() { - expect(pass).to.reportError("start = a:'a' (a:'a')", { - message: "Label \"a\" is already defined at line 1, column 9.", - location: { - start: { offset: 15, line: 1, column: 16 }, - end: { offset: 20, line: 1, column: 21 } - } - }); - }); - - it("doesn't report labels duplicate with the label of the current element", function() { - expect(pass).to.not.reportError("start = a:(a:'a')"); - }); - - it("doesn't report labels duplicate with labels of following elements", function() { - expect(pass).to.not.reportError("start = (a:'a') a:'a'"); - }); - }); + describe("in a sequence", function() { + it("reports labels duplicate with labels of preceding elements", function() { + expect(pass).to.reportError("start = a:'a' a:'a'", { + message: "Label \"a\" is already defined at line 1, column 9.", + location: { + start: { offset: 14, line: 1, column: 15 }, + end: { offset: 19, line: 1, column: 20 } + } + }); + }); + + it("doesn't report labels duplicate with labels in subexpressions", function() { + expect(pass).to.not.reportError("start = ('a' / a:'a' / 'a') a:'a'"); + expect(pass).to.not.reportError("start = (a:'a' { }) a:'a'"); + expect(pass).to.not.reportError("start = ('a' a:'a' 'a') a:'a'"); + expect(pass).to.not.reportError("start = b:(a:'a') a:'a'"); + expect(pass).to.not.reportError("start = $(a:'a') a:'a'"); + expect(pass).to.not.reportError("start = &(a:'a') a:'a'"); + expect(pass).to.not.reportError("start = !(a:'a') a:'a'"); + expect(pass).to.not.reportError("start = (a:'a')? a:'a'"); + expect(pass).to.not.reportError("start = (a:'a')* a:'a'"); + expect(pass).to.not.reportError("start = (a:'a')+ a:'a'"); + expect(pass).to.not.reportError("start = (a:'a') a:'a'"); + }); + }); + + describe("in a choice", function() { + it("doesn't report labels duplicate with labels of preceding alternatives", function() { + expect(pass).to.not.reportError("start = a:'a' / a:'a'"); + }); + }); + + describe("in outer sequence", function() { + it("reports labels duplicate with labels of preceding elements", function() { + expect(pass).to.reportError("start = a:'a' (a:'a')", { + message: "Label \"a\" is already defined at line 1, column 9.", + location: { + start: { offset: 15, line: 1, column: 16 }, + end: { offset: 20, line: 1, column: 21 } + } + }); + }); + + it("doesn't report labels duplicate with the label of the current element", function() { + expect(pass).to.not.reportError("start = a:(a:'a')"); + }); + + it("doesn't report labels duplicate with labels of following elements", function() { + expect(pass).to.not.reportError("start = (a:'a') a:'a'"); + }); + }); }); diff --git a/test/unit/compiler/passes/report-duplicate-rules.spec.js b/test/unit/compiler/passes/report-duplicate-rules.spec.js index 6c9283c..9cfb47d 100644 --- a/test/unit/compiler/passes/report-duplicate-rules.spec.js +++ b/test/unit/compiler/passes/report-duplicate-rules.spec.js @@ -9,16 +9,16 @@ chai.use(helpers); let expect = chai.expect; describe("compiler pass |reportDuplicateRules|", function() { - it("reports duplicate rules", function() { - expect(pass).to.reportError([ - "start = 'a'", - "start = 'b'" - ].join("\n"), { - message: "Rule \"start\" is already defined at line 1, column 1.", - location: { - start: { offset: 12, line: 2, column: 1 }, - end: { offset: 23, line: 2, column: 12 } - } - }); - }); + it("reports duplicate rules", function() { + expect(pass).to.reportError([ + "start = 'a'", + "start = 'b'" + ].join("\n"), { + message: "Rule \"start\" is already defined at line 1, column 1.", + location: { + start: { offset: 12, line: 2, column: 1 }, + end: { offset: 23, line: 2, column: 12 } + } + }); + }); }); diff --git a/test/unit/compiler/passes/report-infinite-recursion.spec.js b/test/unit/compiler/passes/report-infinite-recursion.spec.js index 28b46f3..46ee7a0 100644 --- a/test/unit/compiler/passes/report-infinite-recursion.spec.js +++ b/test/unit/compiler/passes/report-infinite-recursion.spec.js @@ -9,111 +9,111 @@ chai.use(helpers); let expect = chai.expect; describe("compiler pass |reportInfiniteRecursion|", function() { - it("reports direct left recursion", function() { - expect(pass).to.reportError("start = start", { - message: "Possible infinite loop when parsing (left recursion: start -> start).", - location: { - start: { offset: 8, line: 1, column: 9 }, - end: { offset: 13, line: 1, column: 14 } - } - }); - }); - - it("reports indirect left recursion", function() { - expect(pass).to.reportError([ - "start = stop", - "stop = start" - ].join("\n"), { - message: "Possible infinite loop when parsing (left recursion: start -> stop -> start).", - location: { - start: { offset: 20, line: 2, column: 8 }, - end: { offset: 25, line: 2, column: 13 } - } - }); - }); - - describe("in sequences", function() { - it("reports left recursion if all preceding elements match empty string", function() { - expect(pass).to.reportError("start = '' '' '' start"); - }); - - it("doesn't report left recursion if some preceding element doesn't match empty string", function() { - expect(pass).to.not.reportError("start = 'a' '' '' start"); - expect(pass).to.not.reportError("start = '' 'a' '' start"); - expect(pass).to.not.reportError("start = '' '' 'a' start"); - }); - - // Regression test for #359. - it("reports left recursion when rule reference is wrapped in an expression", function() { - expect(pass).to.reportError("start = '' start?"); - }); - - it("computes expressions that always consume input on success correctly", function() { - expect(pass).to.reportError([ - "start = a start", - "a 'a' = ''" - ].join("\n")); - expect(pass).to.not.reportError([ - "start = a start", - "a 'a' = 'a'" - ].join("\n")); - - expect(pass).to.reportError("start = ('' / 'a' / 'b') start"); - expect(pass).to.reportError("start = ('a' / '' / 'b') start"); - expect(pass).to.reportError("start = ('a' / 'b' / '') start"); - expect(pass).to.not.reportError("start = ('a' / 'b' / 'c') start"); - - expect(pass).to.reportError("start = ('' { }) start"); - expect(pass).to.not.reportError("start = ('a' { }) start"); - - expect(pass).to.reportError("start = ('' '' '') start"); - expect(pass).to.not.reportError("start = ('a' '' '') start"); - expect(pass).to.not.reportError("start = ('' 'a' '') start"); - expect(pass).to.not.reportError("start = ('' '' 'a') start"); - - expect(pass).to.reportError("start = a:'' start"); - expect(pass).to.not.reportError("start = a:'a' start"); - - expect(pass).to.reportError("start = $'' start"); - expect(pass).to.not.reportError("start = $'a' start"); - - expect(pass).to.reportError("start = &'' start"); - expect(pass).to.reportError("start = &'a' start"); - - expect(pass).to.reportError("start = !'' start"); - expect(pass).to.reportError("start = !'a' start"); - - expect(pass).to.reportError("start = ''? start"); - expect(pass).to.reportError("start = 'a'? start"); - - expect(pass).to.reportError("start = ''* start"); - expect(pass).to.reportError("start = 'a'* start"); - - expect(pass).to.reportError("start = ''+ start"); - expect(pass).to.not.reportError("start = 'a'+ start"); - - expect(pass).to.reportError("start = ('') start"); - expect(pass).to.not.reportError("start = ('a') start"); - - expect(pass).to.reportError("start = &{ } start"); - - expect(pass).to.reportError("start = !{ } start"); - - expect(pass).to.reportError([ - "start = a start", - "a = ''" - ].join("\n")); - expect(pass).to.not.reportError([ - "start = a start", - "a = 'a'" - ].join("\n")); - - expect(pass).to.reportError("start = '' start"); - expect(pass).to.not.reportError("start = 'a' start"); - - expect(pass).to.not.reportError("start = [a-d] start"); - - expect(pass).to.not.reportError("start = . start"); - }); - }); + it("reports direct left recursion", function() { + expect(pass).to.reportError("start = start", { + message: "Possible infinite loop when parsing (left recursion: start -> start).", + location: { + start: { offset: 8, line: 1, column: 9 }, + end: { offset: 13, line: 1, column: 14 } + } + }); + }); + + it("reports indirect left recursion", function() { + expect(pass).to.reportError([ + "start = stop", + "stop = start" + ].join("\n"), { + message: "Possible infinite loop when parsing (left recursion: start -> stop -> start).", + location: { + start: { offset: 20, line: 2, column: 8 }, + end: { offset: 25, line: 2, column: 13 } + } + }); + }); + + describe("in sequences", function() { + it("reports left recursion if all preceding elements match empty string", function() { + expect(pass).to.reportError("start = '' '' '' start"); + }); + + it("doesn't report left recursion if some preceding element doesn't match empty string", function() { + expect(pass).to.not.reportError("start = 'a' '' '' start"); + expect(pass).to.not.reportError("start = '' 'a' '' start"); + expect(pass).to.not.reportError("start = '' '' 'a' start"); + }); + + // Regression test for #359. + it("reports left recursion when rule reference is wrapped in an expression", function() { + expect(pass).to.reportError("start = '' start?"); + }); + + it("computes expressions that always consume input on success correctly", function() { + expect(pass).to.reportError([ + "start = a start", + "a 'a' = ''" + ].join("\n")); + expect(pass).to.not.reportError([ + "start = a start", + "a 'a' = 'a'" + ].join("\n")); + + expect(pass).to.reportError("start = ('' / 'a' / 'b') start"); + expect(pass).to.reportError("start = ('a' / '' / 'b') start"); + expect(pass).to.reportError("start = ('a' / 'b' / '') start"); + expect(pass).to.not.reportError("start = ('a' / 'b' / 'c') start"); + + expect(pass).to.reportError("start = ('' { }) start"); + expect(pass).to.not.reportError("start = ('a' { }) start"); + + expect(pass).to.reportError("start = ('' '' '') start"); + expect(pass).to.not.reportError("start = ('a' '' '') start"); + expect(pass).to.not.reportError("start = ('' 'a' '') start"); + expect(pass).to.not.reportError("start = ('' '' 'a') start"); + + expect(pass).to.reportError("start = a:'' start"); + expect(pass).to.not.reportError("start = a:'a' start"); + + expect(pass).to.reportError("start = $'' start"); + expect(pass).to.not.reportError("start = $'a' start"); + + expect(pass).to.reportError("start = &'' start"); + expect(pass).to.reportError("start = &'a' start"); + + expect(pass).to.reportError("start = !'' start"); + expect(pass).to.reportError("start = !'a' start"); + + expect(pass).to.reportError("start = ''? start"); + expect(pass).to.reportError("start = 'a'? start"); + + expect(pass).to.reportError("start = ''* start"); + expect(pass).to.reportError("start = 'a'* start"); + + expect(pass).to.reportError("start = ''+ start"); + expect(pass).to.not.reportError("start = 'a'+ start"); + + expect(pass).to.reportError("start = ('') start"); + expect(pass).to.not.reportError("start = ('a') start"); + + expect(pass).to.reportError("start = &{ } start"); + + expect(pass).to.reportError("start = !{ } start"); + + expect(pass).to.reportError([ + "start = a start", + "a = ''" + ].join("\n")); + expect(pass).to.not.reportError([ + "start = a start", + "a = 'a'" + ].join("\n")); + + expect(pass).to.reportError("start = '' start"); + expect(pass).to.not.reportError("start = 'a' start"); + + expect(pass).to.not.reportError("start = [a-d] start"); + + expect(pass).to.not.reportError("start = . start"); + }); + }); }); diff --git a/test/unit/compiler/passes/report-infinite-repetition.spec.js b/test/unit/compiler/passes/report-infinite-repetition.spec.js index ff3dea9..9b3ca5d 100644 --- a/test/unit/compiler/passes/report-infinite-repetition.spec.js +++ b/test/unit/compiler/passes/report-infinite-repetition.spec.js @@ -9,91 +9,91 @@ chai.use(helpers); let expect = chai.expect; describe("compiler pass |reportInfiniteRepetition|", function() { - it("reports infinite loops for zero_or_more", function() { - expect(pass).to.reportError("start = ('')*", { - message: "Possible infinite loop when parsing (repetition used with an expression that may not consume any input).", - location: { - start: { offset: 8, line: 1, column: 9 }, - end: { offset: 13, line: 1, column: 14 } - } - }); - }); - - it("reports infinite loops for one_or_more", function() { - expect(pass).to.reportError("start = ('')+", { - message: "Possible infinite loop when parsing (repetition used with an expression that may not consume any input).", - location: { - start: { offset: 8, line: 1, column: 9 }, - end: { offset: 13, line: 1, column: 14 } - } - }); - }); - - it("computes expressions that always consume input on success correctly", function() { - expect(pass).to.reportError([ - "start = a*", - "a 'a' = ''" - ].join("\n")); - expect(pass).to.not.reportError([ - "start = a*", - "a 'a' = 'a'" - ].join("\n")); - - expect(pass).to.reportError("start = ('' / 'a' / 'b')*"); - expect(pass).to.reportError("start = ('a' / '' / 'b')*"); - expect(pass).to.reportError("start = ('a' / 'b' / '')*"); - expect(pass).to.not.reportError("start = ('a' / 'b' / 'c')*"); - - expect(pass).to.reportError("start = ('' { })*"); - expect(pass).to.not.reportError("start = ('a' { })*"); - - expect(pass).to.reportError("start = ('' '' '')*"); - expect(pass).to.not.reportError("start = ('a' '' '')*"); - expect(pass).to.not.reportError("start = ('' 'a' '')*"); - expect(pass).to.not.reportError("start = ('' '' 'a')*"); - - expect(pass).to.reportError("start = (a:'')*"); - expect(pass).to.not.reportError("start = (a:'a')*"); - - expect(pass).to.reportError("start = ($'')*"); - expect(pass).to.not.reportError("start = ($'a')*"); - - expect(pass).to.reportError("start = (&'')*"); - expect(pass).to.reportError("start = (&'a')*"); - - expect(pass).to.reportError("start = (!'')*"); - expect(pass).to.reportError("start = (!'a')*"); - - expect(pass).to.reportError("start = (''?)*"); - expect(pass).to.reportError("start = ('a'?)*"); - - expect(pass).to.reportError("start = (''*)*"); - expect(pass).to.reportError("start = ('a'*)*"); - - expect(pass).to.reportError("start = (''+)*"); - expect(pass).to.not.reportError("start = ('a'+)*"); - - expect(pass).to.reportError("start = ('')*"); - expect(pass).to.not.reportError("start = ('a')*"); - - expect(pass).to.reportError("start = (&{ })*"); - - expect(pass).to.reportError("start = (!{ })*"); - - expect(pass).to.reportError([ - "start = a*", - "a = ''" - ].join("\n")); - expect(pass).to.not.reportError([ - "start = a*", - "a = 'a'" - ].join("\n")); - - expect(pass).to.reportError("start = ''*"); - expect(pass).to.not.reportError("start = 'a'*"); - - expect(pass).to.not.reportError("start = [a-d]*"); - - expect(pass).to.not.reportError("start = .*"); - }); + it("reports infinite loops for zero_or_more", function() { + expect(pass).to.reportError("start = ('')*", { + message: "Possible infinite loop when parsing (repetition used with an expression that may not consume any input).", + location: { + start: { offset: 8, line: 1, column: 9 }, + end: { offset: 13, line: 1, column: 14 } + } + }); + }); + + it("reports infinite loops for one_or_more", function() { + expect(pass).to.reportError("start = ('')+", { + message: "Possible infinite loop when parsing (repetition used with an expression that may not consume any input).", + location: { + start: { offset: 8, line: 1, column: 9 }, + end: { offset: 13, line: 1, column: 14 } + } + }); + }); + + it("computes expressions that always consume input on success correctly", function() { + expect(pass).to.reportError([ + "start = a*", + "a 'a' = ''" + ].join("\n")); + expect(pass).to.not.reportError([ + "start = a*", + "a 'a' = 'a'" + ].join("\n")); + + expect(pass).to.reportError("start = ('' / 'a' / 'b')*"); + expect(pass).to.reportError("start = ('a' / '' / 'b')*"); + expect(pass).to.reportError("start = ('a' / 'b' / '')*"); + expect(pass).to.not.reportError("start = ('a' / 'b' / 'c')*"); + + expect(pass).to.reportError("start = ('' { })*"); + expect(pass).to.not.reportError("start = ('a' { })*"); + + expect(pass).to.reportError("start = ('' '' '')*"); + expect(pass).to.not.reportError("start = ('a' '' '')*"); + expect(pass).to.not.reportError("start = ('' 'a' '')*"); + expect(pass).to.not.reportError("start = ('' '' 'a')*"); + + expect(pass).to.reportError("start = (a:'')*"); + expect(pass).to.not.reportError("start = (a:'a')*"); + + expect(pass).to.reportError("start = ($'')*"); + expect(pass).to.not.reportError("start = ($'a')*"); + + expect(pass).to.reportError("start = (&'')*"); + expect(pass).to.reportError("start = (&'a')*"); + + expect(pass).to.reportError("start = (!'')*"); + expect(pass).to.reportError("start = (!'a')*"); + + expect(pass).to.reportError("start = (''?)*"); + expect(pass).to.reportError("start = ('a'?)*"); + + expect(pass).to.reportError("start = (''*)*"); + expect(pass).to.reportError("start = ('a'*)*"); + + expect(pass).to.reportError("start = (''+)*"); + expect(pass).to.not.reportError("start = ('a'+)*"); + + expect(pass).to.reportError("start = ('')*"); + expect(pass).to.not.reportError("start = ('a')*"); + + expect(pass).to.reportError("start = (&{ })*"); + + expect(pass).to.reportError("start = (!{ })*"); + + expect(pass).to.reportError([ + "start = a*", + "a = ''" + ].join("\n")); + expect(pass).to.not.reportError([ + "start = a*", + "a = 'a'" + ].join("\n")); + + expect(pass).to.reportError("start = ''*"); + expect(pass).to.not.reportError("start = 'a'*"); + + expect(pass).to.not.reportError("start = [a-d]*"); + + expect(pass).to.not.reportError("start = .*"); + }); }); diff --git a/test/unit/compiler/passes/report-undefined-rules.spec.js b/test/unit/compiler/passes/report-undefined-rules.spec.js index 5fc37f5..78385bb 100644 --- a/test/unit/compiler/passes/report-undefined-rules.spec.js +++ b/test/unit/compiler/passes/report-undefined-rules.spec.js @@ -9,13 +9,13 @@ chai.use(helpers); let expect = chai.expect; describe("compiler pass |reportUndefinedRules|", function() { - it("reports undefined rules", function() { - expect(pass).to.reportError("start = undefined", { - message: "Rule \"undefined\" is not defined.", - location: { - start: { offset: 8, line: 1, column: 9 }, - end: { offset: 17, line: 1, column: 18 } - } - }); - }); + it("reports undefined rules", function() { + expect(pass).to.reportError("start = undefined", { + message: "Rule \"undefined\" is not defined.", + location: { + start: { offset: 8, line: 1, column: 9 }, + end: { offset: 17, line: 1, column: 18 } + } + }); + }); }); diff --git a/test/unit/parser.spec.js b/test/unit/parser.spec.js index 16ff566..55fb90e 100644 --- a/test/unit/parser.spec.js +++ b/test/unit/parser.spec.js @@ -6,655 +6,655 @@ let parser = require("../../lib/parser"); let expect = chai.expect; describe("PEG.js grammar parser", function() { - let literalAbcd = { type: "literal", value: "abcd", ignoreCase: false }; - let literalEfgh = { type: "literal", value: "efgh", ignoreCase: false }; - let literalIjkl = { type: "literal", value: "ijkl", ignoreCase: false }; - let literalMnop = { type: "literal", value: "mnop", ignoreCase: false }; - let semanticAnd = { type: "semantic_and", code: " code " }; - let semanticNot = { type: "semantic_not", code: " code " }; - let optional = { type: "optional", expression: literalAbcd }; - let zeroOrMore = { type: "zero_or_more", expression: literalAbcd }; - let oneOrMore = { type: "one_or_more", expression: literalAbcd }; - let textOptional = { type: "text", expression: optional }; - let simpleNotAbcd = { type: "simple_not", expression: literalAbcd }; - let simpleAndOptional = { type: "simple_and", expression: optional }; - let simpleNotOptional = { type: "simple_not", expression: optional }; - let labeledAbcd = { type: "labeled", label: "a", expression: literalAbcd }; - let labeledEfgh = { type: "labeled", label: "b", expression: literalEfgh }; - let labeledIjkl = { type: "labeled", label: "c", expression: literalIjkl }; - let labeledMnop = { type: "labeled", label: "d", expression: literalMnop }; - let labeledSimpleNot = { type: "labeled", label: "a", expression: simpleNotAbcd }; - let sequence = { - type: "sequence", - elements: [literalAbcd, literalEfgh, literalIjkl] - }; - let sequence2 = { - type: "sequence", - elements: [labeledAbcd, labeledEfgh] - }; - let sequence4 = { - type: "sequence", - elements: [labeledAbcd, labeledEfgh, labeledIjkl, labeledMnop] - }; - let groupLabeled = { type: "group", expression: labeledAbcd }; - let groupSequence = { type: "group", expression: sequence }; - let actionAbcd = { type: "action", expression: literalAbcd, code: " code " }; - let actionEfgh = { type: "action", expression: literalEfgh, code: " code " }; - let actionIjkl = { type: "action", expression: literalIjkl, code: " code " }; - let actionMnop = { type: "action", expression: literalMnop, code: " code " }; - let actionSequence = { type: "action", expression: sequence, code: " code " }; - let choice = { - type: "choice", - alternatives: [literalAbcd, literalEfgh, literalIjkl] - }; - let choice2 = { - type: "choice", - alternatives: [actionAbcd, actionEfgh] - }; - let choice4 = { - type: "choice", - alternatives: [actionAbcd, actionEfgh, actionIjkl, actionMnop] - }; - let named = { type: "named", name: "start rule", expression: literalAbcd }; - let ruleA = { type: "rule", name: "a", expression: literalAbcd }; - let ruleB = { type: "rule", name: "b", expression: literalEfgh }; - let ruleC = { type: "rule", name: "c", expression: literalIjkl }; - let ruleStart = { type: "rule", name: "start", expression: literalAbcd }; - let initializer = { type: "initializer", code: " code " }; - - function oneRuleGrammar(expression) { - return { - type: "grammar", - initializer: null, - rules: [{ type: "rule", name: "start", expression: expression }] - }; - } - - function actionGrammar(code) { - return oneRuleGrammar( - { type: "action", expression: literalAbcd, code: code } - ); - } - - function literalGrammar(value, ignoreCase) { - return oneRuleGrammar( - { type: "literal", value: value, ignoreCase: ignoreCase } - ); - } - - function classGrammar(parts, inverted, ignoreCase) { - return oneRuleGrammar({ - type: "class", - parts: parts, - inverted: inverted, - ignoreCase: ignoreCase - }); - } - - function anyGrammar() { - return oneRuleGrammar({ type: "any" }); - } - - function ruleRefGrammar(name) { - return oneRuleGrammar({ type: "rule_ref", name: name }); - } - - let trivialGrammar = literalGrammar("abcd", false); - let twoRuleGrammar = { - type: "grammar", - initializer: null, - rules: [ruleA, ruleB] - }; - - let stripLocation = (function() { - function buildVisitor(functions) { - return function(node) { - return functions[node.type].apply(null, arguments); - }; - } - - function stripLeaf(node) { - delete node.location; - } - - function stripExpression(node) { - delete node.location; - - strip(node.expression); - } - - function stripChildren(property) { - return function(node) { - delete node.location; - - node[property].forEach(strip); - }; - } - - let strip = buildVisitor({ - grammar(node) { - delete node.location; - - if (node.initializer) { - strip(node.initializer); - } - node.rules.forEach(strip); - }, - - initializer: stripLeaf, - rule: stripExpression, - named: stripExpression, - choice: stripChildren("alternatives"), - action: stripExpression, - sequence: stripChildren("elements"), - labeled: stripExpression, - text: stripExpression, - simple_and: stripExpression, - simple_not: stripExpression, - optional: stripExpression, - zero_or_more: stripExpression, - one_or_more: stripExpression, - group: stripExpression, - semantic_and: stripLeaf, - semantic_not: stripLeaf, - rule_ref: stripLeaf, - literal: stripLeaf, - class: stripLeaf, - any: stripLeaf - }); - - return strip; - })(); - - function helpers(chai, utils) { - let Assertion = chai.Assertion; - - Assertion.addMethod("parseAs", function(expected) { - let result = parser.parse(utils.flag(this, "object")); - - stripLocation(result); - - this.assert( - utils.eql(result, expected), - "expected #{this} to parse as #{exp} but got #{act}", - "expected #{this} to not parse as #{exp}", - expected, - result, - !utils.flag(this, "negate") - ); - }); - - Assertion.addMethod("failToParse", function(props) { - let passed, result; - - try { - result = parser.parse(utils.flag(this, "object")); - passed = true; - } catch (e) { - result = e; - passed = false; - } - - if (passed) { - stripLocation(result); - } - - this.assert( - !passed, - "expected #{this} to fail to parse but got #{act}", - "expected #{this} to not fail to parse but it failed with #{act}", - null, - result - ); - - if (!passed && props !== undefined) { - Object.keys(props).forEach(key => { - new Assertion(result).to.have.property(key) - .that.is.deep.equal(props[key]); - }); - } - }); - } - - // Helper activation needs to put inside a |beforeEach| block because the - // helpers conflict with the ones in - // test/behavior/generated-parser-behavior.spec.js. - beforeEach(function() { - chai.use(helpers); - }); - - // Canonical Grammar is "a = 'abcd'; b = 'efgh'; c = 'ijkl';". - it("parses Grammar", function() { - expect("\na = 'abcd';\n").to.parseAs( - { type: "grammar", initializer: null, rules: [ruleA] } - ); - expect("\na = 'abcd';\nb = 'efgh';\nc = 'ijkl';\n").to.parseAs( - { type: "grammar", initializer: null, rules: [ruleA, ruleB, ruleC] } - ); - expect("\n{ code };\na = 'abcd';\n").to.parseAs( - { type: "grammar", initializer: initializer, rules: [ruleA] } - ); - }); - - // Canonical Initializer is "{ code }". - it("parses Initializer", function() { - expect("{ code };start = 'abcd'").to.parseAs( - { type: "grammar", initializer: initializer, rules: [ruleStart] } - ); - }); - - // Canonical Rule is "a = 'abcd';". - it("parses Rule", function() { - expect("start\n=\n'abcd';").to.parseAs( - oneRuleGrammar(literalAbcd) - ); - expect("start\n'start rule'\n=\n'abcd';").to.parseAs( - oneRuleGrammar(named) - ); - }); - - // Canonical Expression is "'abcd'". - it("parses Expression", function() { - expect("start = 'abcd' / 'efgh' / 'ijkl'").to.parseAs( - oneRuleGrammar(choice) - ); - }); - - // Canonical ChoiceExpression is "'abcd' / 'efgh' / 'ijkl'". - it("parses ChoiceExpression", function() { - expect("start = 'abcd' { code }").to.parseAs( - oneRuleGrammar(actionAbcd) - ); - expect("start = 'abcd' { code }\n/\n'efgh' { code }").to.parseAs( - oneRuleGrammar(choice2) - ); - expect( - "start = 'abcd' { code }\n/\n'efgh' { code }\n/\n'ijkl' { code }\n/\n'mnop' { code }" - ).to.parseAs( - oneRuleGrammar(choice4) - ); - }); - - // Canonical ActionExpression is "'abcd' { code }". - it("parses ActionExpression", function() { - expect("start = 'abcd' 'efgh' 'ijkl'").to.parseAs( - oneRuleGrammar(sequence) - ); - expect("start = 'abcd' 'efgh' 'ijkl'\n{ code }").to.parseAs( - oneRuleGrammar(actionSequence) - ); - }); - - // Canonical SequenceExpression is "'abcd' 'efgh' 'ijkl'". - it("parses SequenceExpression", function() { - expect("start = a:'abcd'").to.parseAs( - oneRuleGrammar(labeledAbcd) - ); - expect("start = a:'abcd'\nb:'efgh'").to.parseAs( - oneRuleGrammar(sequence2) - ); - expect("start = a:'abcd'\nb:'efgh'\nc:'ijkl'\nd:'mnop'").to.parseAs( - oneRuleGrammar(sequence4) - ); - }); - - // Canonical LabeledExpression is "a:'abcd'". - it("parses LabeledExpression", function() { - expect("start = a\n:\n!'abcd'").to.parseAs(oneRuleGrammar(labeledSimpleNot)); - expect("start = !'abcd'").to.parseAs(oneRuleGrammar(simpleNotAbcd)); - }); - - // Canonical PrefixedExpression is "!'abcd'". - it("parses PrefixedExpression", function() { - expect("start = !\n'abcd'?").to.parseAs(oneRuleGrammar(simpleNotOptional)); - expect("start = 'abcd'?").to.parseAs(oneRuleGrammar(optional)); - }); - - // Canonical PrefixedOperator is "!". - it("parses PrefixedOperator", function() { - expect("start = $'abcd'?").to.parseAs(oneRuleGrammar(textOptional)); - expect("start = &'abcd'?").to.parseAs(oneRuleGrammar(simpleAndOptional)); - expect("start = !'abcd'?").to.parseAs(oneRuleGrammar(simpleNotOptional)); - }); - - // Canonical SuffixedExpression is "'abcd'?". - it("parses SuffixedExpression", function() { - expect("start = 'abcd'\n?").to.parseAs(oneRuleGrammar(optional)); - expect("start = 'abcd'").to.parseAs(oneRuleGrammar(literalAbcd)); - }); - - // Canonical SuffixedOperator is "?". - it("parses SuffixedOperator", function() { - expect("start = 'abcd'?").to.parseAs(oneRuleGrammar(optional)); - expect("start = 'abcd'*").to.parseAs(oneRuleGrammar(zeroOrMore)); - expect("start = 'abcd'+").to.parseAs(oneRuleGrammar(oneOrMore)); - }); - - // Canonical PrimaryExpression is "'abcd'". - it("parses PrimaryExpression", function() { - expect("start = 'abcd'").to.parseAs(trivialGrammar); - expect("start = [a-d]").to.parseAs(classGrammar([["a", "d"]], false, false)); - expect("start = .").to.parseAs(anyGrammar()); - expect("start = a").to.parseAs(ruleRefGrammar("a")); - expect("start = &{ code }").to.parseAs(oneRuleGrammar(semanticAnd)); - - expect("start = (\na:'abcd'\n)").to.parseAs(oneRuleGrammar(groupLabeled)); - expect("start = (\n'abcd' 'efgh' 'ijkl'\n)").to.parseAs(oneRuleGrammar(groupSequence)); - expect("start = (\n'abcd'\n)").to.parseAs(trivialGrammar); - }); - - // Canonical RuleReferenceExpression is "a". - it("parses RuleReferenceExpression", function() { - expect("start = a").to.parseAs(ruleRefGrammar("a")); - - expect("start = a\n=").to.failToParse(); - expect("start = a\n'abcd'\n=").to.failToParse(); - }); - - // Canonical SemanticPredicateExpression is "!{ code }". - it("parses SemanticPredicateExpression", function() { - expect("start = !\n{ code }").to.parseAs(oneRuleGrammar(semanticNot)); - }); - - // Canonical SemanticPredicateOperator is "!". - it("parses SemanticPredicateOperator", function() { - expect("start = &{ code }").to.parseAs(oneRuleGrammar(semanticAnd)); - expect("start = !{ code }").to.parseAs(oneRuleGrammar(semanticNot)); - }); - - // The SourceCharacter rule is not tested. - - // Canonical WhiteSpace is " ". - it("parses WhiteSpace", function() { - expect("start =\t'abcd'").to.parseAs(trivialGrammar); - expect("start =\v'abcd'").to.parseAs(trivialGrammar); - expect("start =\f'abcd'").to.parseAs(trivialGrammar); - expect("start = 'abcd'").to.parseAs(trivialGrammar); - expect("start =\u00A0'abcd'").to.parseAs(trivialGrammar); - expect("start =\uFEFF'abcd'").to.parseAs(trivialGrammar); - expect("start =\u1680'abcd'").to.parseAs(trivialGrammar); - }); - - // Canonical LineTerminator is "\n". - it("parses LineTerminator", function() { - expect("start = '\n'").to.failToParse(); - expect("start = '\r'").to.failToParse(); - expect("start = '\u2028'").to.failToParse(); - expect("start = '\u2029'").to.failToParse(); - }); - - // Canonical LineTerminatorSequence is "\r\n". - it("parses LineTerminatorSequence", function() { - expect("start =\n'abcd'").to.parseAs(trivialGrammar); - expect("start =\r\n'abcd'").to.parseAs(trivialGrammar); - expect("start =\r'abcd'").to.parseAs(trivialGrammar); - expect("start =\u2028'abcd'").to.parseAs(trivialGrammar); - expect("start =\u2029'abcd'").to.parseAs(trivialGrammar); - }); - - // Canonical Comment is "/* comment */". - it("parses Comment", function() { - expect("start =// comment\n'abcd'").to.parseAs(trivialGrammar); - expect("start =/* comment */'abcd'").to.parseAs(trivialGrammar); - }); - - // Canonical MultiLineComment is "/* comment */". - it("parses MultiLineComment", function() { - expect("start =/**/'abcd'").to.parseAs(trivialGrammar); - expect("start =/*a*/'abcd'").to.parseAs(trivialGrammar); - expect("start =/*abc*/'abcd'").to.parseAs(trivialGrammar); - - expect("start =/**/*/'abcd'").to.failToParse(); - }); - - // Canonical MultiLineCommentNoLineTerminator is "/* comment */". - it("parses MultiLineCommentNoLineTerminator", function() { - expect("a = 'abcd'/**/\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); - expect("a = 'abcd'/*a*/\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); - expect("a = 'abcd'/*abc*/\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); - - expect("a = 'abcd'/**/*/\r\nb = 'efgh'").to.failToParse(); - expect("a = 'abcd'/*\n*/\r\nb = 'efgh'").to.failToParse(); - }); - - // Canonical SingleLineComment is "// comment". - it("parses SingleLineComment", function() { - expect("start =//\n'abcd'").to.parseAs(trivialGrammar); - expect("start =//a\n'abcd'").to.parseAs(trivialGrammar); - expect("start =//abc\n'abcd'").to.parseAs(trivialGrammar); - - expect("start =//\n@\n'abcd'").to.failToParse(); - }); - - // Canonical Identifier is "a". - it("parses Identifier", function() { - expect("start = a:'abcd'").to.parseAs(oneRuleGrammar(labeledAbcd)); - }); - - // Canonical IdentifierName is "a". - it("parses IdentifierName", function() { - expect("start = a").to.parseAs(ruleRefGrammar("a")); - expect("start = ab").to.parseAs(ruleRefGrammar("ab")); - expect("start = abcd").to.parseAs(ruleRefGrammar("abcd")); - }); - - // Canonical IdentifierStart is "a". - it("parses IdentifierStart", function() { - expect("start = a").to.parseAs(ruleRefGrammar("a")); - expect("start = $").to.parseAs(ruleRefGrammar("$")); - expect("start = _").to.parseAs(ruleRefGrammar("_")); - expect("start = \\u0061").to.parseAs(ruleRefGrammar("a")); - }); - - // Canonical IdentifierPart is "a". - it("parses IdentifierPart", function() { - expect("start = aa").to.parseAs(ruleRefGrammar("aa")); - expect("start = a\u0300").to.parseAs(ruleRefGrammar("a\u0300")); - expect("start = a0").to.parseAs(ruleRefGrammar("a0")); - expect("start = a\u203F").to.parseAs(ruleRefGrammar("a\u203F")); - expect("start = a\u200C").to.parseAs(ruleRefGrammar("a\u200C")); - expect("start = a\u200D").to.parseAs(ruleRefGrammar("a\u200D")); - }); - - // Unicode rules and reserved word rules are not tested. - - // Canonical LiteralMatcher is "'abcd'". - it("parses LiteralMatcher", function() { - expect("start = 'abcd'").to.parseAs(literalGrammar("abcd", false)); - expect("start = 'abcd'i").to.parseAs(literalGrammar("abcd", true)); - }); - - // Canonical StringLiteral is "'abcd'". - it("parses StringLiteral", function() { - expect("start = \"\"").to.parseAs(literalGrammar("", false)); - expect("start = \"a\"").to.parseAs(literalGrammar("a", false)); - expect("start = \"abc\"").to.parseAs(literalGrammar("abc", false)); - - expect("start = ''").to.parseAs(literalGrammar("", false)); - expect("start = 'a'").to.parseAs(literalGrammar("a", false)); - expect("start = 'abc'").to.parseAs(literalGrammar("abc", false)); - }); - - // Canonical DoubleStringCharacter is "a". - it("parses DoubleStringCharacter", function() { - expect("start = \"a\"").to.parseAs(literalGrammar("a", false)); - expect("start = \"\\n\"").to.parseAs(literalGrammar("\n", false)); - expect("start = \"\\\n\"").to.parseAs(literalGrammar("", false)); - - expect("start = \"\"\"").to.failToParse(); - expect("start = \"\\\"").to.failToParse(); - expect("start = \"\n\"").to.failToParse(); - }); - - // Canonical SingleStringCharacter is "a". - it("parses SingleStringCharacter", function() { - expect("start = 'a'").to.parseAs(literalGrammar("a", false)); - expect("start = '\\n'").to.parseAs(literalGrammar("\n", false)); - expect("start = '\\\n'").to.parseAs(literalGrammar("", false)); - - expect("start = '''").to.failToParse(); - expect("start = '\\'").to.failToParse(); - expect("start = '\n'").to.failToParse(); - }); - - // Canonical CharacterClassMatcher is "[a-d]". - it("parses CharacterClassMatcher", function() { - expect("start = []").to.parseAs( - classGrammar([], false, false) - ); - expect("start = [a-d]").to.parseAs( - classGrammar([["a", "d"]], false, false) - ); - expect("start = [a]").to.parseAs( - classGrammar(["a"], false, false) - ); - expect("start = [a-de-hi-l]").to.parseAs( - classGrammar( - [["a", "d"], ["e", "h"], ["i", "l"]], - false, - false - ) - ); - expect("start = [^a-d]").to.parseAs( - classGrammar([["a", "d"]], true, false) - ); - expect("start = [a-d]i").to.parseAs( - classGrammar([["a", "d"]], false, true) - ); - - expect("start = [\\\n]").to.parseAs( - classGrammar([], false, false) - ); - }); - - // Canonical ClassCharacterRange is "a-d". - it("parses ClassCharacterRange", function() { - expect("start = [a-d]").to.parseAs(classGrammar([["a", "d"]], false, false)); - - expect("start = [a-a]").to.parseAs(classGrammar([["a", "a"]], false, false)); - expect("start = [b-a]").to.failToParse({ - message: "Invalid character range: b-a." - }); - }); - - // Canonical ClassCharacter is "a". - it("parses ClassCharacter", function() { - expect("start = [a]").to.parseAs(classGrammar(["a"], false, false)); - expect("start = [\\n]").to.parseAs(classGrammar(["\n"], false, false)); - expect("start = [\\\n]").to.parseAs(classGrammar([], false, false)); - - expect("start = []]").to.failToParse(); - expect("start = [\\]").to.failToParse(); - expect("start = [\n]").to.failToParse(); - }); - - // Canonical LineContinuation is "\\\n". - it("parses LineContinuation", function() { - expect("start = '\\\r\n'").to.parseAs(literalGrammar("", false)); - }); - - // Canonical EscapeSequence is "n". - it("parses EscapeSequence", function() { - expect("start = '\\n'").to.parseAs(literalGrammar("\n", false)); - expect("start = '\\0'").to.parseAs(literalGrammar("\x00", false)); - expect("start = '\\xFF'").to.parseAs(literalGrammar("\xFF", false)); - expect("start = '\\uFFFF'").to.parseAs(literalGrammar("\uFFFF", false)); - - expect("start = '\\09'").to.failToParse(); - }); - - // Canonical CharacterEscapeSequence is "n". - it("parses CharacterEscapeSequence", function() { - expect("start = '\\n'").to.parseAs(literalGrammar("\n", false)); - expect("start = '\\a'").to.parseAs(literalGrammar("a", false)); - }); - - // Canonical SingleEscapeCharacter is "n". - it("parses SingleEscapeCharacter", function() { - expect("start = '\\''").to.parseAs(literalGrammar("'", false)); - expect("start = '\\\"'").to.parseAs(literalGrammar("\"", false)); - expect("start = '\\\\'").to.parseAs(literalGrammar("\\", false)); - expect("start = '\\b'").to.parseAs(literalGrammar("\b", false)); - expect("start = '\\f'").to.parseAs(literalGrammar("\f", false)); - expect("start = '\\n'").to.parseAs(literalGrammar("\n", false)); - expect("start = '\\r'").to.parseAs(literalGrammar("\r", false)); - expect("start = '\\t'").to.parseAs(literalGrammar("\t", false)); - expect("start = '\\v'").to.parseAs(literalGrammar("\v", false)); - }); - - // Canonical NonEscapeCharacter is "a". - it("parses NonEscapeCharacter", function() { - expect("start = '\\a'").to.parseAs(literalGrammar("a", false)); - - // The negative predicate is impossible to test with PEG.js grammar - // structure. - }); - - // The EscapeCharacter rule is impossible to test with PEG.js grammar - // structure. - - // Canonical HexEscapeSequence is "xFF". - it("parses HexEscapeSequence", function() { - expect("start = '\\xFF'").to.parseAs(literalGrammar("\xFF", false)); - }); - - // Canonical UnicodeEscapeSequence is "uFFFF". - it("parses UnicodeEscapeSequence", function() { - expect("start = '\\uFFFF'").to.parseAs(literalGrammar("\uFFFF", false)); - }); - - // Digit rules are not tested. - - // Canonical AnyMatcher is ".". - it("parses AnyMatcher", function() { - expect("start = .").to.parseAs(anyGrammar()); - }); - - // Canonical CodeBlock is "{ code }". - it("parses CodeBlock", function() { - expect("start = 'abcd' { code }").to.parseAs(actionGrammar(" code ")); - }); - - // Canonical Code is " code ". - it("parses Code", function() { - expect("start = 'abcd' {a}").to.parseAs(actionGrammar("a")); - expect("start = 'abcd' {abc}").to.parseAs(actionGrammar("abc")); - expect("start = 'abcd' {{a}}").to.parseAs(actionGrammar("{a}")); - expect("start = 'abcd' {{a}{b}{c}}").to.parseAs(actionGrammar("{a}{b}{c}")); - - expect("start = 'abcd' {{}").to.failToParse(); - expect("start = 'abcd' {}}").to.failToParse(); - }); - - // Unicode character category rules and token rules are not tested. - - // Canonical __ is "\n". - it("parses __", function() { - expect("start ='abcd'").to.parseAs(trivialGrammar); - expect("start = 'abcd'").to.parseAs(trivialGrammar); - expect("start =\r\n'abcd'").to.parseAs(trivialGrammar); - expect("start =/* comment */'abcd'").to.parseAs(trivialGrammar); - expect("start = 'abcd'").to.parseAs(trivialGrammar); - }); - - // Canonical _ is " ". - it("parses _", function() { - expect("a = 'abcd'\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); - expect("a = 'abcd' \r\nb = 'efgh'").to.parseAs(twoRuleGrammar); - expect("a = 'abcd'/* comment */\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); - expect("a = 'abcd' \r\nb = 'efgh'").to.parseAs(twoRuleGrammar); - }); - - // Canonical EOS is ";". - it("parses EOS", function() { - expect("a = 'abcd'\n;b = 'efgh'").to.parseAs(twoRuleGrammar); - expect("a = 'abcd' \r\nb = 'efgh'").to.parseAs(twoRuleGrammar); - expect("a = 'abcd' // comment\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); - expect("a = 'abcd'\nb = 'efgh'").to.parseAs(twoRuleGrammar); - }); - - // Canonical EOF is the end of input. - it("parses EOF", function() { - expect("start = 'abcd'\n").to.parseAs(trivialGrammar); - }); + let literalAbcd = { type: "literal", value: "abcd", ignoreCase: false }; + let literalEfgh = { type: "literal", value: "efgh", ignoreCase: false }; + let literalIjkl = { type: "literal", value: "ijkl", ignoreCase: false }; + let literalMnop = { type: "literal", value: "mnop", ignoreCase: false }; + let semanticAnd = { type: "semantic_and", code: " code " }; + let semanticNot = { type: "semantic_not", code: " code " }; + let optional = { type: "optional", expression: literalAbcd }; + let zeroOrMore = { type: "zero_or_more", expression: literalAbcd }; + let oneOrMore = { type: "one_or_more", expression: literalAbcd }; + let textOptional = { type: "text", expression: optional }; + let simpleNotAbcd = { type: "simple_not", expression: literalAbcd }; + let simpleAndOptional = { type: "simple_and", expression: optional }; + let simpleNotOptional = { type: "simple_not", expression: optional }; + let labeledAbcd = { type: "labeled", label: "a", expression: literalAbcd }; + let labeledEfgh = { type: "labeled", label: "b", expression: literalEfgh }; + let labeledIjkl = { type: "labeled", label: "c", expression: literalIjkl }; + let labeledMnop = { type: "labeled", label: "d", expression: literalMnop }; + let labeledSimpleNot = { type: "labeled", label: "a", expression: simpleNotAbcd }; + let sequence = { + type: "sequence", + elements: [literalAbcd, literalEfgh, literalIjkl] + }; + let sequence2 = { + type: "sequence", + elements: [labeledAbcd, labeledEfgh] + }; + let sequence4 = { + type: "sequence", + elements: [labeledAbcd, labeledEfgh, labeledIjkl, labeledMnop] + }; + let groupLabeled = { type: "group", expression: labeledAbcd }; + let groupSequence = { type: "group", expression: sequence }; + let actionAbcd = { type: "action", expression: literalAbcd, code: " code " }; + let actionEfgh = { type: "action", expression: literalEfgh, code: " code " }; + let actionIjkl = { type: "action", expression: literalIjkl, code: " code " }; + let actionMnop = { type: "action", expression: literalMnop, code: " code " }; + let actionSequence = { type: "action", expression: sequence, code: " code " }; + let choice = { + type: "choice", + alternatives: [literalAbcd, literalEfgh, literalIjkl] + }; + let choice2 = { + type: "choice", + alternatives: [actionAbcd, actionEfgh] + }; + let choice4 = { + type: "choice", + alternatives: [actionAbcd, actionEfgh, actionIjkl, actionMnop] + }; + let named = { type: "named", name: "start rule", expression: literalAbcd }; + let ruleA = { type: "rule", name: "a", expression: literalAbcd }; + let ruleB = { type: "rule", name: "b", expression: literalEfgh }; + let ruleC = { type: "rule", name: "c", expression: literalIjkl }; + let ruleStart = { type: "rule", name: "start", expression: literalAbcd }; + let initializer = { type: "initializer", code: " code " }; + + function oneRuleGrammar(expression) { + return { + type: "grammar", + initializer: null, + rules: [{ type: "rule", name: "start", expression: expression }] + }; + } + + function actionGrammar(code) { + return oneRuleGrammar( + { type: "action", expression: literalAbcd, code: code } + ); + } + + function literalGrammar(value, ignoreCase) { + return oneRuleGrammar( + { type: "literal", value: value, ignoreCase: ignoreCase } + ); + } + + function classGrammar(parts, inverted, ignoreCase) { + return oneRuleGrammar({ + type: "class", + parts: parts, + inverted: inverted, + ignoreCase: ignoreCase + }); + } + + function anyGrammar() { + return oneRuleGrammar({ type: "any" }); + } + + function ruleRefGrammar(name) { + return oneRuleGrammar({ type: "rule_ref", name: name }); + } + + let trivialGrammar = literalGrammar("abcd", false); + let twoRuleGrammar = { + type: "grammar", + initializer: null, + rules: [ruleA, ruleB] + }; + + let stripLocation = (function() { + function buildVisitor(functions) { + return function(node) { + return functions[node.type].apply(null, arguments); + }; + } + + function stripLeaf(node) { + delete node.location; + } + + function stripExpression(node) { + delete node.location; + + strip(node.expression); + } + + function stripChildren(property) { + return function(node) { + delete node.location; + + node[property].forEach(strip); + }; + } + + let strip = buildVisitor({ + grammar(node) { + delete node.location; + + if (node.initializer) { + strip(node.initializer); + } + node.rules.forEach(strip); + }, + + initializer: stripLeaf, + rule: stripExpression, + named: stripExpression, + choice: stripChildren("alternatives"), + action: stripExpression, + sequence: stripChildren("elements"), + labeled: stripExpression, + text: stripExpression, + simple_and: stripExpression, + simple_not: stripExpression, + optional: stripExpression, + zero_or_more: stripExpression, + one_or_more: stripExpression, + group: stripExpression, + semantic_and: stripLeaf, + semantic_not: stripLeaf, + rule_ref: stripLeaf, + literal: stripLeaf, + class: stripLeaf, + any: stripLeaf + }); + + return strip; + })(); + + function helpers(chai, utils) { + let Assertion = chai.Assertion; + + Assertion.addMethod("parseAs", function(expected) { + let result = parser.parse(utils.flag(this, "object")); + + stripLocation(result); + + this.assert( + utils.eql(result, expected), + "expected #{this} to parse as #{exp} but got #{act}", + "expected #{this} to not parse as #{exp}", + expected, + result, + !utils.flag(this, "negate") + ); + }); + + Assertion.addMethod("failToParse", function(props) { + let passed, result; + + try { + result = parser.parse(utils.flag(this, "object")); + passed = true; + } catch (e) { + result = e; + passed = false; + } + + if (passed) { + stripLocation(result); + } + + this.assert( + !passed, + "expected #{this} to fail to parse but got #{act}", + "expected #{this} to not fail to parse but it failed with #{act}", + null, + result + ); + + if (!passed && props !== undefined) { + Object.keys(props).forEach(key => { + new Assertion(result).to.have.property(key) + .that.is.deep.equal(props[key]); + }); + } + }); + } + + // Helper activation needs to put inside a |beforeEach| block because the + // helpers conflict with the ones in + // test/behavior/generated-parser-behavior.spec.js. + beforeEach(function() { + chai.use(helpers); + }); + + // Canonical Grammar is "a = 'abcd'; b = 'efgh'; c = 'ijkl';". + it("parses Grammar", function() { + expect("\na = 'abcd';\n").to.parseAs( + { type: "grammar", initializer: null, rules: [ruleA] } + ); + expect("\na = 'abcd';\nb = 'efgh';\nc = 'ijkl';\n").to.parseAs( + { type: "grammar", initializer: null, rules: [ruleA, ruleB, ruleC] } + ); + expect("\n{ code };\na = 'abcd';\n").to.parseAs( + { type: "grammar", initializer: initializer, rules: [ruleA] } + ); + }); + + // Canonical Initializer is "{ code }". + it("parses Initializer", function() { + expect("{ code };start = 'abcd'").to.parseAs( + { type: "grammar", initializer: initializer, rules: [ruleStart] } + ); + }); + + // Canonical Rule is "a = 'abcd';". + it("parses Rule", function() { + expect("start\n=\n'abcd';").to.parseAs( + oneRuleGrammar(literalAbcd) + ); + expect("start\n'start rule'\n=\n'abcd';").to.parseAs( + oneRuleGrammar(named) + ); + }); + + // Canonical Expression is "'abcd'". + it("parses Expression", function() { + expect("start = 'abcd' / 'efgh' / 'ijkl'").to.parseAs( + oneRuleGrammar(choice) + ); + }); + + // Canonical ChoiceExpression is "'abcd' / 'efgh' / 'ijkl'". + it("parses ChoiceExpression", function() { + expect("start = 'abcd' { code }").to.parseAs( + oneRuleGrammar(actionAbcd) + ); + expect("start = 'abcd' { code }\n/\n'efgh' { code }").to.parseAs( + oneRuleGrammar(choice2) + ); + expect( + "start = 'abcd' { code }\n/\n'efgh' { code }\n/\n'ijkl' { code }\n/\n'mnop' { code }" + ).to.parseAs( + oneRuleGrammar(choice4) + ); + }); + + // Canonical ActionExpression is "'abcd' { code }". + it("parses ActionExpression", function() { + expect("start = 'abcd' 'efgh' 'ijkl'").to.parseAs( + oneRuleGrammar(sequence) + ); + expect("start = 'abcd' 'efgh' 'ijkl'\n{ code }").to.parseAs( + oneRuleGrammar(actionSequence) + ); + }); + + // Canonical SequenceExpression is "'abcd' 'efgh' 'ijkl'". + it("parses SequenceExpression", function() { + expect("start = a:'abcd'").to.parseAs( + oneRuleGrammar(labeledAbcd) + ); + expect("start = a:'abcd'\nb:'efgh'").to.parseAs( + oneRuleGrammar(sequence2) + ); + expect("start = a:'abcd'\nb:'efgh'\nc:'ijkl'\nd:'mnop'").to.parseAs( + oneRuleGrammar(sequence4) + ); + }); + + // Canonical LabeledExpression is "a:'abcd'". + it("parses LabeledExpression", function() { + expect("start = a\n:\n!'abcd'").to.parseAs(oneRuleGrammar(labeledSimpleNot)); + expect("start = !'abcd'").to.parseAs(oneRuleGrammar(simpleNotAbcd)); + }); + + // Canonical PrefixedExpression is "!'abcd'". + it("parses PrefixedExpression", function() { + expect("start = !\n'abcd'?").to.parseAs(oneRuleGrammar(simpleNotOptional)); + expect("start = 'abcd'?").to.parseAs(oneRuleGrammar(optional)); + }); + + // Canonical PrefixedOperator is "!". + it("parses PrefixedOperator", function() { + expect("start = $'abcd'?").to.parseAs(oneRuleGrammar(textOptional)); + expect("start = &'abcd'?").to.parseAs(oneRuleGrammar(simpleAndOptional)); + expect("start = !'abcd'?").to.parseAs(oneRuleGrammar(simpleNotOptional)); + }); + + // Canonical SuffixedExpression is "'abcd'?". + it("parses SuffixedExpression", function() { + expect("start = 'abcd'\n?").to.parseAs(oneRuleGrammar(optional)); + expect("start = 'abcd'").to.parseAs(oneRuleGrammar(literalAbcd)); + }); + + // Canonical SuffixedOperator is "?". + it("parses SuffixedOperator", function() { + expect("start = 'abcd'?").to.parseAs(oneRuleGrammar(optional)); + expect("start = 'abcd'*").to.parseAs(oneRuleGrammar(zeroOrMore)); + expect("start = 'abcd'+").to.parseAs(oneRuleGrammar(oneOrMore)); + }); + + // Canonical PrimaryExpression is "'abcd'". + it("parses PrimaryExpression", function() { + expect("start = 'abcd'").to.parseAs(trivialGrammar); + expect("start = [a-d]").to.parseAs(classGrammar([["a", "d"]], false, false)); + expect("start = .").to.parseAs(anyGrammar()); + expect("start = a").to.parseAs(ruleRefGrammar("a")); + expect("start = &{ code }").to.parseAs(oneRuleGrammar(semanticAnd)); + + expect("start = (\na:'abcd'\n)").to.parseAs(oneRuleGrammar(groupLabeled)); + expect("start = (\n'abcd' 'efgh' 'ijkl'\n)").to.parseAs(oneRuleGrammar(groupSequence)); + expect("start = (\n'abcd'\n)").to.parseAs(trivialGrammar); + }); + + // Canonical RuleReferenceExpression is "a". + it("parses RuleReferenceExpression", function() { + expect("start = a").to.parseAs(ruleRefGrammar("a")); + + expect("start = a\n=").to.failToParse(); + expect("start = a\n'abcd'\n=").to.failToParse(); + }); + + // Canonical SemanticPredicateExpression is "!{ code }". + it("parses SemanticPredicateExpression", function() { + expect("start = !\n{ code }").to.parseAs(oneRuleGrammar(semanticNot)); + }); + + // Canonical SemanticPredicateOperator is "!". + it("parses SemanticPredicateOperator", function() { + expect("start = &{ code }").to.parseAs(oneRuleGrammar(semanticAnd)); + expect("start = !{ code }").to.parseAs(oneRuleGrammar(semanticNot)); + }); + + // The SourceCharacter rule is not tested. + + // Canonical WhiteSpace is " ". + it("parses WhiteSpace", function() { + expect("start =\t'abcd'").to.parseAs(trivialGrammar); + expect("start =\v'abcd'").to.parseAs(trivialGrammar); + expect("start =\f'abcd'").to.parseAs(trivialGrammar); + expect("start = 'abcd'").to.parseAs(trivialGrammar); + expect("start =\u00A0'abcd'").to.parseAs(trivialGrammar); + expect("start =\uFEFF'abcd'").to.parseAs(trivialGrammar); + expect("start =\u1680'abcd'").to.parseAs(trivialGrammar); + }); + + // Canonical LineTerminator is "\n". + it("parses LineTerminator", function() { + expect("start = '\n'").to.failToParse(); + expect("start = '\r'").to.failToParse(); + expect("start = '\u2028'").to.failToParse(); + expect("start = '\u2029'").to.failToParse(); + }); + + // Canonical LineTerminatorSequence is "\r\n". + it("parses LineTerminatorSequence", function() { + expect("start =\n'abcd'").to.parseAs(trivialGrammar); + expect("start =\r\n'abcd'").to.parseAs(trivialGrammar); + expect("start =\r'abcd'").to.parseAs(trivialGrammar); + expect("start =\u2028'abcd'").to.parseAs(trivialGrammar); + expect("start =\u2029'abcd'").to.parseAs(trivialGrammar); + }); + + // Canonical Comment is "/* comment */". + it("parses Comment", function() { + expect("start =// comment\n'abcd'").to.parseAs(trivialGrammar); + expect("start =/* comment */'abcd'").to.parseAs(trivialGrammar); + }); + + // Canonical MultiLineComment is "/* comment */". + it("parses MultiLineComment", function() { + expect("start =/**/'abcd'").to.parseAs(trivialGrammar); + expect("start =/*a*/'abcd'").to.parseAs(trivialGrammar); + expect("start =/*abc*/'abcd'").to.parseAs(trivialGrammar); + + expect("start =/**/*/'abcd'").to.failToParse(); + }); + + // Canonical MultiLineCommentNoLineTerminator is "/* comment */". + it("parses MultiLineCommentNoLineTerminator", function() { + expect("a = 'abcd'/**/\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); + expect("a = 'abcd'/*a*/\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); + expect("a = 'abcd'/*abc*/\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); + + expect("a = 'abcd'/**/*/\r\nb = 'efgh'").to.failToParse(); + expect("a = 'abcd'/*\n*/\r\nb = 'efgh'").to.failToParse(); + }); + + // Canonical SingleLineComment is "// comment". + it("parses SingleLineComment", function() { + expect("start =//\n'abcd'").to.parseAs(trivialGrammar); + expect("start =//a\n'abcd'").to.parseAs(trivialGrammar); + expect("start =//abc\n'abcd'").to.parseAs(trivialGrammar); + + expect("start =//\n@\n'abcd'").to.failToParse(); + }); + + // Canonical Identifier is "a". + it("parses Identifier", function() { + expect("start = a:'abcd'").to.parseAs(oneRuleGrammar(labeledAbcd)); + }); + + // Canonical IdentifierName is "a". + it("parses IdentifierName", function() { + expect("start = a").to.parseAs(ruleRefGrammar("a")); + expect("start = ab").to.parseAs(ruleRefGrammar("ab")); + expect("start = abcd").to.parseAs(ruleRefGrammar("abcd")); + }); + + // Canonical IdentifierStart is "a". + it("parses IdentifierStart", function() { + expect("start = a").to.parseAs(ruleRefGrammar("a")); + expect("start = $").to.parseAs(ruleRefGrammar("$")); + expect("start = _").to.parseAs(ruleRefGrammar("_")); + expect("start = \\u0061").to.parseAs(ruleRefGrammar("a")); + }); + + // Canonical IdentifierPart is "a". + it("parses IdentifierPart", function() { + expect("start = aa").to.parseAs(ruleRefGrammar("aa")); + expect("start = a\u0300").to.parseAs(ruleRefGrammar("a\u0300")); + expect("start = a0").to.parseAs(ruleRefGrammar("a0")); + expect("start = a\u203F").to.parseAs(ruleRefGrammar("a\u203F")); + expect("start = a\u200C").to.parseAs(ruleRefGrammar("a\u200C")); + expect("start = a\u200D").to.parseAs(ruleRefGrammar("a\u200D")); + }); + + // Unicode rules and reserved word rules are not tested. + + // Canonical LiteralMatcher is "'abcd'". + it("parses LiteralMatcher", function() { + expect("start = 'abcd'").to.parseAs(literalGrammar("abcd", false)); + expect("start = 'abcd'i").to.parseAs(literalGrammar("abcd", true)); + }); + + // Canonical StringLiteral is "'abcd'". + it("parses StringLiteral", function() { + expect("start = \"\"").to.parseAs(literalGrammar("", false)); + expect("start = \"a\"").to.parseAs(literalGrammar("a", false)); + expect("start = \"abc\"").to.parseAs(literalGrammar("abc", false)); + + expect("start = ''").to.parseAs(literalGrammar("", false)); + expect("start = 'a'").to.parseAs(literalGrammar("a", false)); + expect("start = 'abc'").to.parseAs(literalGrammar("abc", false)); + }); + + // Canonical DoubleStringCharacter is "a". + it("parses DoubleStringCharacter", function() { + expect("start = \"a\"").to.parseAs(literalGrammar("a", false)); + expect("start = \"\\n\"").to.parseAs(literalGrammar("\n", false)); + expect("start = \"\\\n\"").to.parseAs(literalGrammar("", false)); + + expect("start = \"\"\"").to.failToParse(); + expect("start = \"\\\"").to.failToParse(); + expect("start = \"\n\"").to.failToParse(); + }); + + // Canonical SingleStringCharacter is "a". + it("parses SingleStringCharacter", function() { + expect("start = 'a'").to.parseAs(literalGrammar("a", false)); + expect("start = '\\n'").to.parseAs(literalGrammar("\n", false)); + expect("start = '\\\n'").to.parseAs(literalGrammar("", false)); + + expect("start = '''").to.failToParse(); + expect("start = '\\'").to.failToParse(); + expect("start = '\n'").to.failToParse(); + }); + + // Canonical CharacterClassMatcher is "[a-d]". + it("parses CharacterClassMatcher", function() { + expect("start = []").to.parseAs( + classGrammar([], false, false) + ); + expect("start = [a-d]").to.parseAs( + classGrammar([["a", "d"]], false, false) + ); + expect("start = [a]").to.parseAs( + classGrammar(["a"], false, false) + ); + expect("start = [a-de-hi-l]").to.parseAs( + classGrammar( + [["a", "d"], ["e", "h"], ["i", "l"]], + false, + false + ) + ); + expect("start = [^a-d]").to.parseAs( + classGrammar([["a", "d"]], true, false) + ); + expect("start = [a-d]i").to.parseAs( + classGrammar([["a", "d"]], false, true) + ); + + expect("start = [\\\n]").to.parseAs( + classGrammar([], false, false) + ); + }); + + // Canonical ClassCharacterRange is "a-d". + it("parses ClassCharacterRange", function() { + expect("start = [a-d]").to.parseAs(classGrammar([["a", "d"]], false, false)); + + expect("start = [a-a]").to.parseAs(classGrammar([["a", "a"]], false, false)); + expect("start = [b-a]").to.failToParse({ + message: "Invalid character range: b-a." + }); + }); + + // Canonical ClassCharacter is "a". + it("parses ClassCharacter", function() { + expect("start = [a]").to.parseAs(classGrammar(["a"], false, false)); + expect("start = [\\n]").to.parseAs(classGrammar(["\n"], false, false)); + expect("start = [\\\n]").to.parseAs(classGrammar([], false, false)); + + expect("start = []]").to.failToParse(); + expect("start = [\\]").to.failToParse(); + expect("start = [\n]").to.failToParse(); + }); + + // Canonical LineContinuation is "\\\n". + it("parses LineContinuation", function() { + expect("start = '\\\r\n'").to.parseAs(literalGrammar("", false)); + }); + + // Canonical EscapeSequence is "n". + it("parses EscapeSequence", function() { + expect("start = '\\n'").to.parseAs(literalGrammar("\n", false)); + expect("start = '\\0'").to.parseAs(literalGrammar("\x00", false)); + expect("start = '\\xFF'").to.parseAs(literalGrammar("\xFF", false)); + expect("start = '\\uFFFF'").to.parseAs(literalGrammar("\uFFFF", false)); + + expect("start = '\\09'").to.failToParse(); + }); + + // Canonical CharacterEscapeSequence is "n". + it("parses CharacterEscapeSequence", function() { + expect("start = '\\n'").to.parseAs(literalGrammar("\n", false)); + expect("start = '\\a'").to.parseAs(literalGrammar("a", false)); + }); + + // Canonical SingleEscapeCharacter is "n". + it("parses SingleEscapeCharacter", function() { + expect("start = '\\''").to.parseAs(literalGrammar("'", false)); + expect("start = '\\\"'").to.parseAs(literalGrammar("\"", false)); + expect("start = '\\\\'").to.parseAs(literalGrammar("\\", false)); + expect("start = '\\b'").to.parseAs(literalGrammar("\b", false)); + expect("start = '\\f'").to.parseAs(literalGrammar("\f", false)); + expect("start = '\\n'").to.parseAs(literalGrammar("\n", false)); + expect("start = '\\r'").to.parseAs(literalGrammar("\r", false)); + expect("start = '\\t'").to.parseAs(literalGrammar("\t", false)); + expect("start = '\\v'").to.parseAs(literalGrammar("\v", false)); + }); + + // Canonical NonEscapeCharacter is "a". + it("parses NonEscapeCharacter", function() { + expect("start = '\\a'").to.parseAs(literalGrammar("a", false)); + + // The negative predicate is impossible to test with PEG.js grammar + // structure. + }); + + // The EscapeCharacter rule is impossible to test with PEG.js grammar + // structure. + + // Canonical HexEscapeSequence is "xFF". + it("parses HexEscapeSequence", function() { + expect("start = '\\xFF'").to.parseAs(literalGrammar("\xFF", false)); + }); + + // Canonical UnicodeEscapeSequence is "uFFFF". + it("parses UnicodeEscapeSequence", function() { + expect("start = '\\uFFFF'").to.parseAs(literalGrammar("\uFFFF", false)); + }); + + // Digit rules are not tested. + + // Canonical AnyMatcher is ".". + it("parses AnyMatcher", function() { + expect("start = .").to.parseAs(anyGrammar()); + }); + + // Canonical CodeBlock is "{ code }". + it("parses CodeBlock", function() { + expect("start = 'abcd' { code }").to.parseAs(actionGrammar(" code ")); + }); + + // Canonical Code is " code ". + it("parses Code", function() { + expect("start = 'abcd' {a}").to.parseAs(actionGrammar("a")); + expect("start = 'abcd' {abc}").to.parseAs(actionGrammar("abc")); + expect("start = 'abcd' {{a}}").to.parseAs(actionGrammar("{a}")); + expect("start = 'abcd' {{a}{b}{c}}").to.parseAs(actionGrammar("{a}{b}{c}")); + + expect("start = 'abcd' {{}").to.failToParse(); + expect("start = 'abcd' {}}").to.failToParse(); + }); + + // Unicode character category rules and token rules are not tested. + + // Canonical __ is "\n". + it("parses __", function() { + expect("start ='abcd'").to.parseAs(trivialGrammar); + expect("start = 'abcd'").to.parseAs(trivialGrammar); + expect("start =\r\n'abcd'").to.parseAs(trivialGrammar); + expect("start =/* comment */'abcd'").to.parseAs(trivialGrammar); + expect("start = 'abcd'").to.parseAs(trivialGrammar); + }); + + // Canonical _ is " ". + it("parses _", function() { + expect("a = 'abcd'\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); + expect("a = 'abcd' \r\nb = 'efgh'").to.parseAs(twoRuleGrammar); + expect("a = 'abcd'/* comment */\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); + expect("a = 'abcd' \r\nb = 'efgh'").to.parseAs(twoRuleGrammar); + }); + + // Canonical EOS is ";". + it("parses EOS", function() { + expect("a = 'abcd'\n;b = 'efgh'").to.parseAs(twoRuleGrammar); + expect("a = 'abcd' \r\nb = 'efgh'").to.parseAs(twoRuleGrammar); + expect("a = 'abcd' // comment\r\nb = 'efgh'").to.parseAs(twoRuleGrammar); + expect("a = 'abcd'\nb = 'efgh'").to.parseAs(twoRuleGrammar); + }); + + // Canonical EOF is the end of input. + it("parses EOF", function() { + expect("start = 'abcd'\n").to.parseAs(trivialGrammar); + }); });