From e6d018a88d15917ac6df82f255aefa9269652f26 Mon Sep 17 00:00:00 2001 From: Futago-za Ryuu Date: Wed, 25 Oct 2017 19:19:42 +0100 Subject: [PATCH] Update code format and style This is related to my last commit. I've updated all the JavaScript files to satisfy 'eslint-config-futagozaryuu', my eslint configuration. I'm sure I've probally missed something, but I've run all NPM scripts and Gulp tasks, fixed any bugs that cropped up, and updated some stuff (mainly related to generated messages), so as far as I can, tell this conversion is over (I know I've probally jixed it just by saying this ;P). --- .eslintrc.js | 7 +- bin/options.js | 475 ++- bin/peg.js | 117 +- gulpfile.js | 143 +- lib/.eslintrc.js | 12 +- lib/compiler/asts.js | 152 +- lib/compiler/index.js | 180 +- lib/compiler/js.js | 102 +- lib/compiler/opcodes.js | 98 +- lib/compiler/passes/generate-bytecode.js | 895 +++-- lib/compiler/passes/generate-js.js | 2874 +++++++------- lib/compiler/passes/remove-proxy-rules.js | 72 +- .../passes/report-duplicate-labels.js | 131 +- lib/compiler/passes/report-duplicate-rules.js | 50 +- .../passes/report-infinite-recursion.js | 87 +- .../passes/report-infinite-repetition.js | 60 +- lib/compiler/passes/report-undefined-rules.js | 59 +- lib/compiler/visitor.js | 142 +- lib/grammar-error.js | 20 +- lib/peg.js | 102 +- package.json | 1 + test/.eslintrc.js | 6 +- test/benchmark/benchmarks.js | 74 +- test/benchmark/index.js | 279 +- test/benchmark/run | 330 +- test/benchmark/runner.js | 220 +- test/impact | 195 +- test/server/run | 60 +- test/spec/api/generated-parser-api.spec.js | 380 +- test/spec/api/pegjs-api.spec.js | 517 ++- test/spec/api/plugin-api.spec.js | 309 +- .../generated-parser-behavior.spec.js | 3516 ++++++++++------- .../compiler/passes/generate-bytecode.spec.js | 1480 ++++--- test/spec/unit/compiler/passes/helpers.js | 170 +- .../passes/remove-proxy-rules.spec.js | 124 +- .../passes/report-duplicate-labels.spec.js | 142 +- .../passes/report-duplicate-rules.spec.js | 48 +- .../passes/report-infinite-recursion.spec.js | 250 +- .../passes/report-infinite-repetition.spec.js | 202 +- .../passes/report-undefined-rules.spec.js | 60 +- test/spec/unit/parser.spec.js | 1485 +++---- 41 files changed, 8668 insertions(+), 6958 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9cab2f8..7de629b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,6 +3,11 @@ module.exports = { "extends": "futagozaryuu/node-v4", - "root": true + "root": true, + "rules": { + + "prefer-rest-params": 0, + + }, }; diff --git a/bin/options.js b/bin/options.js index 5fe7460..506c8a5 100644 --- a/bin/options.js +++ b/bin/options.js @@ -1,255 +1,304 @@ "use strict"; -let fs = require("fs"); -let path = require("path"); -let peg = require("../"); +const fs = require( "fs" ); +const path = require( "path" ); +const peg = require( "../" ); // Options let inputFile = null; let outputFile = null; -let options = { - "--": [], - "cache": false, - "dependencies": {}, - "exportVar": null, - "format": "commonjs", - "optimize": "speed", - "output": "source", - "plugins": [], - "trace": false +const options = { + "--": [], + "cache": false, + "dependencies": {}, + "exportVar": null, + "format": "commonjs", + "optimize": "speed", + "output": "source", + "plugins": [], + "trace": false }; -const EXPORT_VAR_FORMATS = ["globals", "umd"]; -const DEPENDENCY_FORMATS = ["amd", "commonjs", "es", "umd"]; -const MODULE_FORMATS = ["amd", "bare", "commonjs", "es", "globals", "umd"]; -const OPTIMIZATION_GOALS = ["size", "speed"]; +const EXPORT_VAR_FORMATS = [ "globals", "umd" ]; +const DEPENDENCY_FORMATS = [ "amd", "commonjs", "es", "umd" ]; +const MODULE_FORMATS = [ "amd", "bare", "commonjs", "es", "globals", "umd" ]; +const OPTIMIZATION_GOALS = [ "size", "speed" ]; // Helpers -function abort(message) { - console.error(message); - process.exit(1); +function abort( message ) { + + console.error( message ); + process.exit( 1 ); + } -function addExtraOptions(json) { - let extraOptions; - - try { - extraOptions = JSON.parse(json); - } catch (e) { - if (!(e instanceof SyntaxError)) { throw e; } - - abort("Error parsing JSON: " + e.message); - } - if (typeof extraOptions !== "object") { - abort("The JSON with extra options has to represent an object."); - } - - Object - .keys(extraOptions) - .forEach(key => { - options[key] = extraOptions[key]; - }); +function addExtraOptions( json ) { + + let extraOptions; + + try { + + extraOptions = JSON.parse( json ); + + } catch ( e ) { + + if ( ! ( e instanceof SyntaxError ) ) throw e; + abort( "Error parsing JSON: " + e.message ); + + } + if ( typeof extraOptions !== "object" ) { + + abort( "The JSON with extra options has to represent an object." ); + + } + + Object + .keys( extraOptions ) + .forEach( key => { + + options[ key ] = extraOptions[ key ]; + + } ); + } -function formatChoicesList(list) { - list = list.map(entry => `"${entry}"`); - let lastOption = list.pop(); +function formatChoicesList( list ) { + + list = list.map( entry => `"${ entry }"` ); + const lastOption = list.pop(); + + return list.length === 0 + ? lastOption + : list.join( ", " ) + " or " + lastOption; - return list.length === 0 - ? lastOption - : list.join(", ") + " or " + lastOption; } -function updateList(list, string) { - string - .split(",") - .forEach(entry => { - entry = entry.trim(); - if (list.indexOf(entry) === -1) { - list.push(entry); - } - }); +function updateList( list, string ) { + + string + .split( "," ) + .forEach( entry => { + + entry = entry.trim(); + if ( list.indexOf( entry ) === -1 ) { + + list.push( entry ); + + } + + } ); + } // Arguments -let args = process.argv.slice(2); +let args = process.argv.slice( 2 ); + +function nextArg( option ) { + + if ( args.length === 0 ) { -function nextArg(option) { - if (args.length === 0) { - abort(`Missing parameter of the ${option} option.`); - } + abort( `Missing parameter of the ${ option } option.` ); + + } + return args.shift(); - return args.shift(); } // Parse Arguments -while (args.length > 0) { - let json, mod; - let argument = args.shift(); - - if (argument.indexOf("-") === 0 && argument.indexOf("=") > 1) { - argument = argument.split("="); - args.unshift(argument.length > 2 ? argument.slice(1) : argument[1]); - argument = argument[0]; - } - - switch (argument) { - - case "--": - options["--"] = args; - args = []; - break; - - case "-a": - case "--allowed-start-rules": - if (!options.allowedStartRules) { - options.allowedStartRules = []; - } - updateList(options.allowedStartRules, nextArg("--allowed-start-rules")); - break; - - case "--cache": - options.cache = true; - break; - - case "--no-cache": - options.cache = false; - break; - - case "-d": - case "--dependency": - argument = nextArg("-d/--dependency"); - if (argument.indexOf(":") === -1) { - mod = [argument, argument]; - } else { - mod = argument.split(":"); - if (mod.length > 2) { - mod[1] = mod.slice(1); - } - } - options.dependencies[mod[0]] = mod[1]; - break; - - case "-e": - case "--export-var": - options.exportVar = nextArg("-e/--export-var"); - break; - - case "--extra-options": - addExtraOptions(nextArg("--extra-options")); - break; - - case "-c": - case "--config": - case "--extra-options-file": - argument = nextArg("-c/--config/--extra-options-file"); - try { - json = fs.readFileSync(argument, "utf8"); - } catch (e) { - abort(`Can't read from file "${argument}".`); - } - addExtraOptions(json); - break; - - case "-f": - case "--format": - argument = nextArg("-f/--format"); - if (MODULE_FORMATS.indexOf(argument) === -1) { - abort(`Module format must be either ${formatChoicesList(MODULE_FORMATS)}.`); - } - options.format = argument; - break; - - case "-h": - case "--help": - console.log(fs.readFileSync(path.join(__dirname, "usage.txt"), "utf8").trim()); - process.exit(); - break; - - case "-O": - case "--optimize": - argument = nextArg("-O/--optimize"); - if (OPTIMIZATION_GOALS.indexOf(argument) === -1) { - abort(`Optimization goal must be either ${formatChoicesList(OPTIMIZATION_GOALS)}.`); - } - options.optimize = argument; - break; - - case "-o": - case "--output": - outputFile = nextArg("-o/--output"); - break; - - case "-p": - case "--plugin": - argument = nextArg("-p/--plugin"); - try { - mod = require(argument); - } catch (ex1) { - if (ex1.code !== "MODULE_NOT_FOUND") { throw ex1; } - - try { - mod = require(path.resolve(argument)); - } catch (ex2) { - if (ex2.code !== "MODULE_NOT_FOUND") { throw ex2; } - - abort(`Can't load module "${argument}".`); - } - } - options.plugins.push(mod); - break; - - case "--trace": - options.trace = true; - break; - - case "--no-trace": - options.trace = false; - break; - - case "-v": - case "--version": - console.log("PEG.js v" + peg.VERSION); - process.exit(); - break; - - default: - if (inputFile !== null) { - abort(`Unknown option: "${argument}".`); - } - inputFile = argument; - } +while ( args.length > 0 ) { + + let json, mod; + let argument = args.shift(); + + if ( argument.indexOf( "-" ) === 0 && argument.indexOf( "=" ) > 1 ) { + + argument = argument.split( "=" ); + args.unshift( argument.length > 2 ? argument.slice( 1 ) : argument[ 1 ] ); + argument = argument[ 0 ]; + + } + + switch ( argument ) { + + case "--": + options[ "--" ] = args; + args = []; + break; + + case "-a": + case "--allowed-start-rules": + if ( ! options.allowedStartRules ) options.allowedStartRules = []; + updateList( options.allowedStartRules, nextArg( "--allowed-start-rules" ) ); + break; + + case "--cache": + options.cache = true; + break; + + case "--no-cache": + options.cache = false; + break; + + case "-d": + case "--dependency": + argument = nextArg( "-d/--dependency" ); + mod = argument.split( ":" ); + + if ( mod.length === 1 ) mod = [ argument, argument ]; + else if ( mod.length > 2 ) mod[ 1 ] = mod.slice( 1 ); + + options.dependencies[ mod[ 0 ] ] = mod[ 1 ]; + break; + + case "-e": + case "--export-var": + options.exportVar = nextArg( "-e/--export-var" ); + break; + + case "--extra-options": + addExtraOptions( nextArg( "--extra-options" ) ); + break; + + case "-c": + case "--config": + case "--extra-options-file": + argument = nextArg( "-c/--config/--extra-options-file" ); + try { + + json = fs.readFileSync( argument, "utf8" ); + + } catch ( e ) { + + abort( `Can't read from file "${ argument }".` ); + + } + addExtraOptions( json ); + break; + + case "-f": + case "--format": + argument = nextArg( "-f/--format" ); + if ( MODULE_FORMATS.indexOf( argument ) === -1 ) { + + abort( `Module format must be either ${ formatChoicesList( MODULE_FORMATS ) }.` ); + + } + options.format = argument; + break; + + case "-h": + case "--help": + console.log( fs.readFileSync( path.join( __dirname, "usage.txt" ), "utf8" ).trim() ); + process.exit(); + break; + + case "-O": + case "--optimize": + argument = nextArg( "-O/--optimize" ); + if ( OPTIMIZATION_GOALS.indexOf( argument ) === -1 ) { + + abort( `Optimization goal must be either ${ formatChoicesList( OPTIMIZATION_GOALS ) }.` ); + + } + options.optimize = argument; + break; + + case "-o": + case "--output": + outputFile = nextArg( "-o/--output" ); + break; + + case "-p": + case "--plugin": + argument = nextArg( "-p/--plugin" ); + try { + + mod = require( argument ); + + } catch ( ex1 ) { + + if ( ex1.code !== "MODULE_NOT_FOUND" ) throw ex1; + try { + + mod = require( path.resolve( argument ) ); + + } catch ( ex2 ) { + + if ( ex2.code !== "MODULE_NOT_FOUND" ) throw ex2; + abort( `Can't load module "${ argument }".` ); + + } + + } + options.plugins.push( mod ); + break; + + case "--trace": + options.trace = true; + break; + + case "--no-trace": + options.trace = false; + break; + + case "-v": + case "--version": + console.log( "PEG.js v" + peg.VERSION ); + process.exit(); + break; + + default: + if ( inputFile !== null ) { + + abort( `Unknown option: "${ argument }".` ); + + } + inputFile = argument; + + } + } // Validation and defaults -if (Object.keys(options.dependencies).length > 0) { - if (DEPENDENCY_FORMATS.indexOf(options.format) === -1) { - abort(`Can't use the -d/--dependency option with the "${options.format}" module format.`); - } -} +if ( Object.keys( options.dependencies ).length > 0 ) { + + if ( DEPENDENCY_FORMATS.indexOf( options.format ) === -1 ) { + + abort( `Can't use the -d/--dependency option with the "${ options.format }" module format.` ); + + } -if (options.exportVar !== null) { - if (EXPORT_VAR_FORMATS.indexOf(options.format) === -1) { - abort(`Can't use the -e/--export-var option with the "${options.format}" module format.`); - } } -if (inputFile === null) { - inputFile = "-"; +if ( options.exportVar !== null ) { + + if ( EXPORT_VAR_FORMATS.indexOf( options.format ) === -1 ) { + + abort( `Can't use the -e/--export-var option with the "${ options.format }" module format.` ); + + } + } -if (outputFile === null) { - if (inputFile === "-") { - outputFile = "-"; - } else if (inputFile) { - outputFile = inputFile.substr(0, inputFile.length - path.extname(inputFile).length) + ".js"; - } +if ( inputFile === null ) inputFile = "-"; + +if ( outputFile === null ) { + + if ( inputFile === "-" ) outputFile = "-"; + else if ( inputFile ) { + + outputFile = inputFile + .substr( 0, inputFile.length - path.extname( inputFile ).length ) + + ".js"; + + } + } // Export diff --git a/bin/peg.js b/bin/peg.js index 29ef696..976ef9a 100644 --- a/bin/peg.js +++ b/bin/peg.js @@ -2,63 +2,100 @@ "use strict"; -let fs = require("fs"); -let peg = require("../lib/peg"); -let options = require("./options"); +const fs = require( "fs" ); +const peg = require( "../lib/peg" ); +const options = require( "./options" ); // Helpers -function readStream(inputStream, callback) { - let input = ""; - inputStream.on("data", data => { input += data; }); - inputStream.on("end", () => { callback(input); }); +function readStream( inputStream, callback ) { + + let input = ""; + inputStream.on( "data", data => { + + input += data; + + } ); + inputStream.on( "end", () => { + + callback( input ); + + } ); + } -function abort(message) { - console.error(message); - process.exit(1); +function abort( message ) { + + console.error( message ); + process.exit( 1 ); + } // Main let inputStream, outputStream; -if (options.inputFile === "-") { - process.stdin.resume(); - inputStream = process.stdin; - inputStream.on("error", () => { - abort(`Can't read from file "${options.inputFile}".`); - }); +if ( options.inputFile === "-" ) { + + process.stdin.resume(); + inputStream = process.stdin; + inputStream.on( "error", () => { + + abort( `Can't read from file "${ options.inputFile }".` ); + + } ); + } else { - inputStream = fs.createReadStream(options.inputFile); + + inputStream = fs.createReadStream( options.inputFile ); + } -if (options.outputFile === "-") { - outputStream = process.stdout; +if ( options.outputFile === "-" ) { + + outputStream = process.stdout; + } else { - outputStream = fs.createWriteStream(options.outputFile); - outputStream.on("error", () => { - abort(`Can't write to file "${options.outputFile}".`); - }); + + outputStream = fs.createWriteStream( options.outputFile ); + outputStream.on( "error", () => { + + abort( `Can't write to file "${ options.outputFile }".` ); + + } ); + } -readStream(inputStream, input => { - let location, source; - - try { - source = peg.generate(input, options); - } catch (e) { - if (e.location !== undefined) { - location = e.location.start; - abort(location.line + ":" + location.column + ": " + e.message); - } else { - abort(e.message); +readStream( inputStream, input => { + + let location, source; + + try { + + source = peg.generate( input, options ); + + } catch ( e ) { + + if ( typeof e.location === "object" ) { + + location = e.location.start; + if ( typeof location === "object" ) { + + return abort( location.line + ":" + location.column + ": " + e.message ); + + } + + } + + return abort( e.message ); + } - } - outputStream.write(source); - if (outputStream !== process.stdout) { - outputStream.end(); - } -}); + outputStream.write( source ); + if ( outputStream !== process.stdout ) { + + outputStream.end(); + + } +} ); diff --git a/gulpfile.js b/gulpfile.js index 277ff9f..7dbe078 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,89 +1,96 @@ "use strict"; -let babelify = require("babelify"); -let browserify = require("browserify"); -let buffer = require("vinyl-buffer"); -let del = require("del"); -let eslint = require("gulp-eslint"); -let gulp = require("gulp"); -let header = require("gulp-header"); -let mocha = require("gulp-mocha"); -let rename = require("gulp-rename"); -let runSequence = require("run-sequence"); -let source = require("vinyl-source-stream"); -let spawn = require("child_process").spawn; -let uglify = require("gulp-uglify"); - -function execFile(args) { - return spawn("node", args.split(" "), { stdio: "inherit" }); +const version = require( "./package" ).version; +const spawn = require( "child_process" ).spawn; +const gulp = require( "gulp" ); +const task = gulp.task.bind( gulp ); +const eslint = require( "gulp-eslint" ); +const mocha = require( "gulp-mocha" ); +const dedent = require( "dedent" ); +const browserify = require( "browserify" ); +const babelify = require( "babelify" ); +const source = require( "vinyl-source-stream" ); +const rename = require( "gulp-rename" ); +const buffer = require( "vinyl-buffer" ); +const uglify = require( "gulp-uglify" ); +const header = require( "gulp-header" ); +const del = require( "del" ); +const runSequence = require( "run-sequence" ); + +function node( args ) { + + return spawn( "node", args.split( " " ), { stdio: "inherit" } ); + } // Run ESLint on all JavaScript files. -gulp.task("lint", () => - gulp.src([ - "lib/**/*.js", - "!lib/parser.js", - "test/benchmark/**/*.js", - "test/benchmark/run", - "test/impact", - "test/spec/**/*.js", - "test/server/run", - "bin/*.js", - "gulpfile.js" - ]) - .pipe(eslint()) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()) +task( "lint", () => gulp + .src( [ + "**/.*rc.js", + "lib/**/*.js", + "!lib/parser.js", + "test/benchmark/**/*.js", + "test/benchmark/run", + "test/impact", + "test/spec/**/*.js", + "test/server/run", + "bin/*.js", + "gulpfile.js" + ] ) + .pipe( eslint( { dotfiles: true } ) ) + .pipe( eslint.format() ) + .pipe( eslint.failAfterError() ) ); // Run tests. -gulp.task("test", () => - gulp.src("test/spec/**/*.spec.js", { read: false }) - .pipe(mocha()) +task( "test", () => gulp + .src( "test/spec/**/*.spec.js", { read: false } ) + .pipe( mocha() ) ); // Run benchmarks. -gulp.task("benchmark", () => execFile("test/benchmark/run")); +task( "benchmark", () => node( "test/benchmark/run" ) ); // Create the browser build. -gulp.task("browser:build", () => { - const HEADER = [ - "//", - "// PEG.js v" + require("./package").version, - "// https://pegjs.org/", - "//", - "// Copyright (c) 2010-2016 David Majda", - "// Copyright (c) 2017+ Futago-za Ryuu", - "//", - "// Licensed under the MIT License.", - "//", - "" - ] - .map(line => `${line}\n`) - .join(""); - - return browserify("lib/peg.js", { standalone: "peg" }) - .transform(babelify, { presets: "es2015", compact: false }) - .bundle() - .pipe(source("peg.js")) - .pipe(header(HEADER)) - .pipe(gulp.dest("browser")) - .pipe(rename({ suffix: ".min" })) - .pipe(buffer()) - .pipe(uglify()) - .pipe(header(HEADER)) - .pipe(gulp.dest("browser")); -}); +task( "browser:build", () => { + + const HEADER = dedent` + + /** + * PEG.js v${ version } + * https://pegjs.org/ + * + * Copyright (c) 2010-2016 David Majda + * Copyright (c) 2017+ Futago-za Ryuu + * + * Released under the MIT License. + */\n\n + + `; + + return browserify( "lib/peg.js", { standalone: "peg" } ) + .transform( babelify, { presets: "es2015", compact: false } ) + .bundle() + .pipe( source( "peg.js" ) ) + .pipe( header( HEADER ) ) + .pipe( gulp.dest( "browser" ) ) + .pipe( rename( { suffix: ".min" } ) ) + .pipe( buffer() ) + .pipe( uglify() ) + .pipe( header( HEADER ) ) + .pipe( gulp.dest( "browser" ) ); + +} ); // Delete the browser build. -gulp.task("browser:clean", () => del("browser")); +task( "browser:clean", () => del( "browser" ) ); // Generate the grammar parser. -gulp.task("parser", () => - execFile("bin/peg src/parser.pegjs -o lib/parser.js") +task( "parser", () => + node( "bin/peg src/parser.pegjs -o lib/parser.js" ) ); // Default task. -gulp.task("default", cb => - runSequence("lint", "test", cb) +task( "default", cb => + runSequence( "benchmark", "test", cb ) ); diff --git a/lib/.eslintrc.js b/lib/.eslintrc.js index 5663514..c454440 100644 --- a/lib/.eslintrc.js +++ b/lib/.eslintrc.js @@ -4,8 +4,16 @@ module.exports = { "extends": "futagozaryuu/es2015", "env": { - "commonjs": true + + "commonjs": true, + + }, + "root": true, + "rules": { + + "prefer-rest-params": 0, + "strict": 0, + }, - "root": true }; diff --git a/lib/compiler/asts.js b/lib/compiler/asts.js index e7f383a..50c4ff7 100644 --- a/lib/compiler/asts.js +++ b/lib/compiler/asts.js @@ -1,76 +1,102 @@ "use strict"; -let visitor = require("./visitor"); +const 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]; - } - } +const asts = { + findRule( ast, name ) { - return undefined; - }, + for ( let i = 0; i < ast.rules.length; i++ ) { - indexOfRule(ast, name) { - for (let i = 0; i < ast.rules.length; i++) { - if (ast.rules[i].name === name) { - return i; - } - } + if ( ast.rules[ i ].name === name ) return ast.rules[ i ]; - return -1; - }, + } - alwaysConsumesOnSuccess(ast, node) { - function consumesTrue() { return true; } - function consumesFalse() { return false; } + return void 0; - function consumesExpression(node) { - return consumes(node.expression); - } + }, + + 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 ) { + + let consumes; + + 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); - } + 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..6d2fec6 100644 --- a/lib/compiler/index.js +++ b/lib/compiler/index.js @@ -1,91 +1,109 @@ "use strict"; -let generateBytecode = require("./passes/generate-bytecode"); -let generateJS = require("./passes/generate-js"); -let removeProxyRules = require("./passes/remove-proxy-rules"); -let reportDuplicateLabels = require("./passes/report-duplicate-labels"); -let reportDuplicateRules = require("./passes/report-duplicate-rules"); -let reportInfiniteRecursion = require("./passes/report-infinite-recursion"); -let reportInfiniteRepetition = require("./passes/report-infinite-repetition"); -let reportUndefinedRules = require("./passes/report-undefined-rules"); -let visitor = require("./visitor"); - -function processOptions(options, defaults) { - let processedOptions = {}; - - 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]; - } - }); +const generateBytecode = require( "./passes/generate-bytecode" ); +const generateJS = require( "./passes/generate-js" ); +const removeProxyRules = require( "./passes/remove-proxy-rules" ); +const reportDuplicateLabels = require( "./passes/report-duplicate-labels" ); +const reportDuplicateRules = require( "./passes/report-duplicate-rules" ); +const reportInfiniteRecursion = require( "./passes/report-infinite-recursion" ); +const reportInfiniteRepetition = require( "./passes/report-infinite-repetition" ); +const reportUndefinedRules = require( "./passes/report-undefined-rules" ); +const visitor = require( "./visitor" ); + +function processOptions( options, defaults ) { + + const processedOptions = {}; + + 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 ]; + + } + + } ); + + return processedOptions; - return processedOptions; } -let compiler = { - // 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 +const compiler = { + // 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 + } }, - 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 : {}; - - 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); }); - }); - - switch (options.output) { - case "parser": - return eval(ast.code); - - case "source": - return ast.code; - - default: - throw new Error("Invalid output format: " + options.output + "."); + + // 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 = typeof options !== "undefined" ? options : {}; + + 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( pass => { + + pass( ast, options ); + + } ); + + } ); + + switch ( options.output ) { + + case "parser": + return eval( ast.code ); + + case "source": + return ast.code; + + 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..f09251a 100644 --- a/lib/compiler/js.js +++ b/lib/compiler/js.js @@ -1,54 +1,62 @@ "use strict"; -function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } +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)); - }, - - 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)); - } +const 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 ) ); + + }, + + 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..e91ed65 100644 --- a/lib/compiler/opcodes.js +++ b/lib/compiler/opcodes.js @@ -1,54 +1,56 @@ "use strict"; // Bytecode instruction opcodes. -let opcodes = { - // 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 - - // 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 - - // 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 - - // 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 - - // Rules - - RULE: 27, // RULE r - - // Failure Reporting - - SILENT_FAILS_ON: 28, // SILENT_FAILS_ON - SILENT_FAILS_OFF: 29 // SILENT_FAILS_OFF +const opcodes = { + + // 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 + + // 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 + + // 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 + + // 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 + + // Rules + + RULE: 27, // RULE r + + // Failure Reporting + + 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 3f36ce9..c364b75 100644 --- a/lib/compiler/passes/generate-bytecode.js +++ b/lib/compiler/passes/generate-bytecode.js @@ -1,9 +1,9 @@ "use strict"; -let asts = require("../asts"); -let js = require("../js"); -let op = require("../opcodes"); -let visitor = require("../visitor"); +const asts = require( "../asts" ); +const js = require( "../js" ); +const op = require( "../opcodes" ); +const visitor = require( "../visitor" ); // Generates bytecode. // @@ -187,431 +187,494 @@ let visitor = require("../visitor"); // [29] SILENT_FAILS_OFF // // 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) { +function generateBytecode( ast ) { + + const consts = []; + let generate; + + function addConst( value ) { + + const 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 ) { + + const 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 ) { + + const 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( - generate(alternatives[0], { - sp: context.sp, - env: cloneEnv(context.env), - action: null - }), - alternatives.length > 1 - ? buildCondition( - [op.IF_ERROR], + [ 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], - buildAlternativesCode(alternatives.slice(1), context) + [ 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 ] + ) + ) ); - } - - 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 - }), + + } + + function buildSemanticPredicate( code, negative, context ) { + + const functionIndex = addFunctionConst( Object.keys( context.env ), code ); + + return buildSequence( + [ op.UPDATE_SAVED_POS ], + buildCall( functionIndex, 0, context.env, context.sp ), 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] - ) + [ op.IF ], + buildSequence( [ op.POP ], negative ? [ op.PUSH_FAILED ] : [ op.PUSH_UNDEFINED ] ), + buildSequence( [ op.POP ], negative ? [ op.PUSH_UNDEFINED ] : [ op.PUSH_FAILED ] ) ) - ); - } else { - if (context.action) { - let functionIndex = addFunctionConst( - Object.keys(context.env), - context.action.code + ); + + } + + function buildAppendLoop( expressionCode ) { + + return buildLoop( + [ op.WHILE_NOT_ERROR ], + buildSequence( [ op.APPEND ], expressionCode ) + ); + + } + + 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 ) { + + const 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 < 2 + ? [] + : buildCondition( + [ op.IF_ERROR ], + buildSequence( + [ op.POP ], + buildAlternativesCode( alternatives.slice( 1 ), context ) + ), + [] + ) + ); + + } + + return buildAlternativesCode( node.alternatives, context ); + + }, + + action( node, context ) { + + const env = cloneEnv( context.env ); + const emitCall = node.expression.type !== "sequence" || node.expression.elements.length === 0; + const expressionCode = generate( node.expression, { + sp: context.sp + ( emitCall ? 1 : 0 ), + env: env, + action: node + } ); + const functionIndex = addFunctionConst( Object.keys( env ), node.code ); + + return emitCall === false + ? expressionCode + : 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 ] + ); + + }, + + sequence( node, context ) { + + function buildElementsCode( elements, context ) { + + if ( elements.length > 0 ) { + + const 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 ) { + + const functionIndex = addFunctionConst( + Object.keys( context.env ), + context.action.code + ); + + return buildSequence( + [ op.LOAD_SAVED_POS, node.elements.length ], + buildCall( + functionIndex, + node.elements.length + 1, + context.env, + context.sp + ) + ); + + } + return buildSequence( [ op.WRAP, node.elements.length ], [ op.NIP ] ); + + } + return buildSequence( - [op.LOAD_SAVED_POS, node.elements.length], - buildCall( - functionIndex, - node.elements.length + 1, - context.env, - context.sp - ) + [ op.PUSH_CURR_POS ], + buildElementsCode( node.elements, { + sp: context.sp + 1, + env: context.env, + action: context.action + } ) ); - } else { - return buildSequence([op.WRAP, node.elements.length], [op.NIP]); - } + + }, + + labeled( node, context ) { + + const 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 ) { + + const 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 ) { + + const 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 ) { + + const stringIndex = addConst( `"${ js.stringEscape( + node.ignoreCase ? node.value.toLowerCase() : node.value + ) }"` ); + const 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 ] + ); + + } + + const stringIndex = addConst( "\"\"" ); + return [ op.PUSH, stringIndex ]; + + }, + + class( node ) { + + const regexp = "/^[" + + ( node.inverted ? "^" : "" ) + + node.parts + .map( part => + ( Array.isArray( part ) + ? js.regexpClassEscape( part[ 0 ] ) + + "-" + + js.regexpClassEscape( part[ 1 ] ) + : js.regexpClassEscape( part ) ) + ) + .join( "" ) + + "]/" + + ( node.ignoreCase ? "i" : "" ); + + const parts = "[" + + node.parts + .map( part => + ( Array.isArray( part ) + ? `["${ js.stringEscape( part[ 0 ] ) }", "${ js.stringEscape( part[ 1 ] ) }"]` + : "\"" + js.stringEscape( part ) + "\"" ) + ) + .join( ", " ) + + "]"; + + const regexpIndex = addConst( regexp ); + const expectedIndex = addConst( + "peg$classExpectation(" + + parts + ", " + + node.inverted + ", " + + node.ignoreCase + + ")" + ); + + return buildCondition( + [ op.MATCH_REGEXP, regexpIndex ], + [ op.ACCEPT_N, 1 ], + [ op.FAIL, expectedIndex ] + ); + + }, + + any() { + + const expectedIndex = addConst( "peg$anyExpectation()" ); + + return buildCondition( + [ op.MATCH_ANY ], + [ op.ACCEPT_N, 1 ], + [ op.FAIL, expectedIndex ] + ); + } - } - - 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 - ) - + "\"" - ); - 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 = "/^[" - + (node.inverted ? "^" : "") - + node.parts.map(part => - Array.isArray(part) - ? js.regexpClassEscape(part[0]) - + "-" - + js.regexpClassEscape(part[1]) - : js.regexpClassEscape(part) - ).join("") - + "]/" + (node.ignoreCase ? "i" : ""); - let parts = "[" - + node.parts.map(part => - Array.isArray(part) - ? "[\"" + js.stringEscape(part[0]) + "\", \"" + js.stringEscape(part[1]) + "\"]" - : "\"" + js.stringEscape(part) + "\"" - ).join(", ") - + "]"; - 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 ); - generate(ast); } module.exports = generateBytecode; diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index 8adeaa0..8e9c7ce 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -1,1423 +1,1567 @@ +/* eslint no-mixed-operators: 0, prefer-const: 0 */ + "use strict"; -let asts = require("../asts"); -let js = require("../js"); -let op = require("../opcodes"); +const asts = require( "../asts" ); +const js = require( "../js" ); +const op = require( "../opcodes" ); // Generates parser JavaScript code. -function generateJS(ast, options) { - /* These only indent non-empty lines to avoid trailing whitespace. */ - const lineMatchRE = /^([^`\r\n]+?(?:`[^`]*?`[^\r\n]*?)?)$/gm; - function indent2(code) { return code.replace(lineMatchRE, " $1"); } - function indent10(code) { return code.replace(lineMatchRE, " $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("")) - + "\")" - ).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")); +function generateJS( ast, options ) { + + /* These only indent non-empty lines to avoid trailing whitespace. */ + const lineMatchRE = /^([^`\r\n]+?(?:`[^`]*?`[^\r\n]*?)?)$/gm; + function indent2( code ) { + + return code.replace( lineMatchRE, " $1" ); + } + function indent10( code ) { + + return code.replace( lineMatchRE, " $1" ); - 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 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( "" ) + ) + }")` + ) + .join( ",\n" ) + ), + "];" + ].join( "\n" ); - function generateRuleFooter(ruleNameCode, resultCode) { - let parts = []; + } - if (options.cache) { - parts.push([ - "", - "peg$resultsCache[key] = { nextPos: peg$currPos, result: " + resultCode + " };" - ].join("\n")); - } + return ast.consts.map( ( c, i ) => "var peg$c" + i + " = " + c + ";" ).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 generateRuleHeader( ruleNameCode, ruleIndexCode ) { + + const 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 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 generateRuleFooter( ruleNameCode, resultCode ) { + + const 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 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"); + function generateInterpreter() { + + const parts = []; + + function generateCondition( cond, argsLength ) { + + const baseLength = argsLength + 3; + const thenLengthCode = "bc[ip + " + ( baseLength - 2 ) + "]"; + const 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 ) { + + const baseLength = 2; + const 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() { + + const baseLength = 4; + const 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" ); + } - 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")); + function generateRuleFunction( rule ) { + + const parts = []; + const stackVars = []; + + 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 + + const stack = { + sp: -1, + maxSp: -1, + + push( exprCode ) { + + const code = s( ++this.sp ) + " = " + exprCode + ";"; + if ( this.sp > this.maxSp ) this.maxSp = this.sp; + return code; + + }, + + pop( n ) { + + if ( typeof n === "undefined" ) return s( this.sp-- ); + + const 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; + const end = bc.length; + const parts = []; + let value; + + function compileCondition( cond, argCount ) { + + const baseLength = argCount + 3; + const thenLength = bc[ ip + baseLength - 2 ]; + const elseLength = bc[ ip + baseLength - 1 ]; + const 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 ) { + + const baseLength = 2; + const bodyLength = bc[ ip + baseLength - 1 ]; + const 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() { + + const baseLength = 4; + const paramsLength = bc[ ip + baseLength - 1 ]; + + const value = c( bc[ ip + 1 ] ) + + "(" + + bc + .slice( ip + baseLength, ip + baseLength + paramsLength ) + .map( 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, " + + eval( ast.consts[ bc[ ip + 1 ] ] ).length + + ") === " + + c( bc[ ip + 1 ] ) + : "input.charCodeAt(peg$currPos) === " + + eval( ast.consts[ bc[ ip + 1 ] ] ).charCodeAt( 0 ) + , 1 + ); + break; + + 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" ); + + } + + const 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" ); + } - 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--); + function generateToplevel() { + + const 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" ) { + + const startRuleIndices = "{ " + + options.allowedStartRules + .map( r => r + ": " + asts.indexOfRule( ast, r ) ) + .join( ", " ) + + " }"; + const startRuleIndex = asts.indexOfRule( ast, options.allowedStartRules[ 0 ] ); + + parts.push( [ + " var peg$startRuleIndices = " + startRuleIndices + ";", + " var peg$startRuleIndex = " + startRuleIndex + ";" + ].join( "\n" ) ); + } else { - let values = Array(n); - for (let i = 0; i < n; i++) { - values[i] = s(this.sp - n + 1 + i); - } + const startRuleFunctions = "{ " + + options.allowedStartRules + .map( r => r + ": peg$parse" + r ) + .join( ", " ) + + " }"; + const startRuleFunction = "peg$parse" + options.allowedStartRules[ 0 ]; - this.sp -= n; + parts.push( [ + " var peg$startRuleFunctions = " + startRuleFunctions + ";", + " var peg$startRuleFunction = " + startRuleFunction + ";" + ].join( "\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( "" ); + + 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" ) ); + } - 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."); + + if ( options.trace ) { + + if ( options.optimize === "size" ) { + + const ruleNames = "[" + + ast.rules + .map( 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("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(", ") - + ")"; - 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) === " - + eval(ast.consts[bc[ip + 1]]).charCodeAt(0), - 1 - ); - break; - - 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] + "."); + 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" ) ); + } - } - return parts.join("\n"); - } + parts.push( [ + "", + " function text() {", + " return input.substring(peg$savedPos, peg$currPos);", + " }", + "", + " function offset() {", + " return peg$savedPos;", + " }", + "", + " function range() {", + " return [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( "" ); - code = compile(rule.bytecode); + } else { - parts.push("function peg$parse" + rule.name + "() {"); + ast.rules.forEach( rule => { - if (options.trace) { - parts.push(" var startPos = peg$currPos;"); - } + parts.push( indent2( generateRuleFunction( rule ) ) ); + parts.push( "" ); - 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(", ") - + " }"; - 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(", ") - + " }"; - let startRuleFunction = "peg$parse" + options.allowedStartRules[0]; - - parts.push([ - " var peg$startRuleFunctions = " + startRuleFunctions + ";", - " var peg$startRuleFunction = " + startRuleFunction + ";" - ].join("\n")); - } + if ( ast.initializer ) { - 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")); - } + parts.push( indent2( ast.initializer.code ) ); + parts.push( "" ); - if (options.trace) { - if (options.optimize === "size") { - let ruleNames = "[" - + ast.rules.map( - 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")); - } + if ( options.optimize === "size" ) { - parts.push([ - "", - " function text() {", - " return input.substring(peg$savedPos, peg$currPos);", - " }", - "", - " function offset() {", - " return peg$savedPos;", - " }", - "", - " function range() {", - " return [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(""); - }); - } + parts.push( " peg$result = peg$parseRule(peg$startRuleIndex);" ); - if (ast.initializer) { - parts.push(indent2(ast.initializer.code)); - parts.push(""); - } + } else { - if (options.optimize === "size") { - parts.push(" peg$result = peg$parseRule(peg$startRuleIndex);"); - } else { - parts.push(" peg$result = peg$startRuleFunction();"); - } + 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"); - } + 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 generateParserExports() { - return options.trace - ? [ - "{", - " peg$SyntaxError as SyntaxError,", - " peg$DefaultTracer as DefaultTracer,", - " peg$parse as parse", - "}" - ].join("\n") - : [ - "{", - " peg$SyntaxError as SyntaxError,", - " peg$parse as 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(""); + function generateWrapper( toplevelCode ) { + + function generateGeneratedByComment() { + + return [ + "// Generated by PEG.js 0.10.0.", + "//", + "// https://pegjs.org/" + ].join( "\n" ); + } - parts.push([ - toplevelCode, - "", - "module.exports = " + generateParserObject() + ";", - "" - ].join("\n")); - - return parts.join("\n"); - }, - - es() { - let parts = []; - let dependencyVars = Object.keys(options.dependencies); - - parts.push( - generateGeneratedByComment(), - "" - ); - - if (dependencyVars.length > 0) { - dependencyVars.forEach(variable => { - parts.push("import " + variable - + " from \"" - + js.stringEscape(options.dependencies[variable]) - + "\";" - ); - }); - parts.push(""); + function generateParserObject() { + + return options.trace + ? [ + "{", + " SyntaxError: peg$SyntaxError,", + " DefaultTracer: peg$DefaultTracer,", + " parse: peg$parse", + "}" + ].join( "\n" ) + : [ + "{", + " SyntaxError: peg$SyntaxError,", + " parse: peg$parse", + "}" + ].join( "\n" ); + } - parts.push( - toplevelCode, - "", - "export " + generateParserExports() + ";", - "", - "export default " + generateParserObject() + ";", - "" - ); - - 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(", ") - + "]"; - 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(", ") - + "]"; - 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")); + function generateParserExports() { + + return options.trace + ? [ + "{", + " peg$SyntaxError as SyntaxError,", + " peg$DefaultTracer as DefaultTracer,", + " peg$parse as parse", + "}" + ].join( "\n" ) + : [ + "{", + " peg$SyntaxError as SyntaxError,", + " peg$parse as parse", + "}" + ].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()); + const generators = { + bare() { + + return [ + generateGeneratedByComment(), + "(function() {", + " \"use strict\";", + "", + indent2( toplevelCode ), + "", + indent2( "return " + generateParserObject() + ";" ), + "})()" + ].join( "\n" ); + + }, + + commonjs() { + + const parts = []; + const 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" ); + + }, + + es() { + + const parts = []; + const dependencyVars = Object.keys( options.dependencies ); + + parts.push( + generateGeneratedByComment(), + "" + ); + + if ( dependencyVars.length > 0 ) { + + dependencyVars.forEach( variable => { + + parts.push( "import " + variable + + " from \"" + + js.stringEscape( options.dependencies[ variable ] ) + + "\";" + ); + + } ); + parts.push( "" ); + + } + + parts.push( + toplevelCode, + "", + "export " + generateParserExports() + ";", + "", + "export default " + generateParserObject() + ";", + "" + ); + + return parts.join( "\n" ); + + }, + + amd() { + + const dependencyVars = Object.keys( options.dependencies ); + const dependencyIds = dependencyVars.map( v => options.dependencies[ v ] ); + const dependencies = "[" + + dependencyIds + .map( id => `"${ js.stringEscape( id ) }"` ) + .join( ", " ) + + "]"; + const 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() { + + const parts = []; + const dependencyVars = Object.keys( options.dependencies ); + const dependencyIds = dependencyVars.map( v => options.dependencies[ v ] ); + const dependencies = "[" + + dependencyIds + .map( id => `"${ js.stringEscape( id ) }"` ) + .join( ", " ) + + "]"; + const requires = dependencyIds + .map( id => `require("${ js.stringEscape( id ) }")` ) + .join( ", " ); + const 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..a452e0e 100644 --- a/lib/compiler/passes/remove-proxy-rules.js +++ b/lib/compiler/passes/remove-proxy-rules.js @@ -1,39 +1,59 @@ "use strict"; -let visitor = require("../visitor"); +const 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; - } - } - }); +function removeProxyRules( ast, options ) { - replace(ast); - } + function isProxyRule( node ) { - let indices = []; + return node.type === "rule" && node.expression.type === "rule_ref"; - 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(); + function replaceRuleRefs( ast, from, to ) { + + const replace = visitor.build( { + rule_ref( node ) { + + if ( node.name === from ) { + + node.name = to; + + } + + } + } ); + + replace( ast ); + + } + + const 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 ); + + } ); - 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..ab13144 100644 --- a/lib/compiler/passes/report-duplicate-labels.js +++ b/lib/compiler/passes/report-duplicate-labels.js @@ -1,62 +1,83 @@ "use strict"; -let GrammarError = require("../../grammar-error"); -let visitor = require("../visitor"); +const GrammarError = require( "../../grammar-error" ); +const visitor = require( "../visitor" ); // Checks that each label is defined only once within each scope. -function reportDuplicateLabels(ast) { - function cloneEnv(env) { - let clone = {}; - - Object.keys(env).forEach(name => { - clone[name] = env[name]; - }); - - return clone; - } - - function checkExpressionWithClonedEnv(node, env) { - check(node.expression, cloneEnv(env)); - } - - let check = visitor.build({ - rule(node) { - check(node.expression, { }); - }, - - choice(node, env) { - node.alternatives.forEach(alternative => { - check(alternative, cloneEnv(env)); - }); - }, - - action: checkExpressionWithClonedEnv, - - 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 - ); - } - - check(node.expression, env); - - 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 - }); - - check(ast); +function reportDuplicateLabels( ast ) { + + let check; + + function cloneEnv( env ) { + + const clone = {}; + + Object.keys( env ).forEach( name => { + + clone[ name ] = env[ name ]; + + } ); + + return clone; + + } + + function checkExpressionWithClonedEnv( node, env ) { + + check( node.expression, cloneEnv( env ) ); + + } + + check = visitor.build( { + rule( node ) { + + check( node.expression, {} ); + + }, + + choice( node, env ) { + + node.alternatives.forEach( alternative => { + + check( alternative, cloneEnv( env ) ); + + } ); + + }, + + action: checkExpressionWithClonedEnv, + + labeled( node, env ) { + + const label = node.label; + + if ( Object.prototype.hasOwnProperty.call( env, label ) ) { + + const start = env[ label ].start; + + throw new GrammarError( + `Label "${ label }" is already defined at line ${ start.line }, column ${ start.column }.`, + node.location + ); + + } + + check( node.expression, env ); + env[ label ] = node.location; + + }, + + text: checkExpressionWithClonedEnv, + simple_and: checkExpressionWithClonedEnv, + simple_not: checkExpressionWithClonedEnv, + optional: checkExpressionWithClonedEnv, + zero_or_more: checkExpressionWithClonedEnv, + one_or_more: checkExpressionWithClonedEnv, + group: checkExpressionWithClonedEnv + } ); + + 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..55826a3 100644 --- a/lib/compiler/passes/report-duplicate-rules.js +++ b/lib/compiler/passes/report-duplicate-rules.js @@ -1,28 +1,36 @@ "use strict"; -let GrammarError = require("../../grammar-error"); -let visitor = require("../visitor"); +const GrammarError = require( "../../grammar-error" ); +const visitor = require( "../visitor" ); // Checks that each rule is defined only once. -function reportDuplicateRules(ast) { - 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 " - + "at line " + rules[node.name].start.line + ", " - + "column " + rules[node.name].start.column + ".", - node.location - ); - } - - rules[node.name] = node.location; - } - }); - - check(ast); +function reportDuplicateRules( ast ) { + + const rules = {}; + + const check = visitor.build( { + rule( node ) { + + const name = node.name; + + if ( Object.prototype.hasOwnProperty.call( rules, name ) ) { + + const start = rules[ name ].start; + + throw new GrammarError( + `Rule "${ name }" is already defined at line ${ start.line }, column ${ start.column }.`, + node.location + ); + + } + + rules[ node.name ] = node.location; + + } + } ); + + 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..c7a6840 100644 --- a/lib/compiler/passes/report-infinite-recursion.js +++ b/lib/compiler/passes/report-infinite-recursion.js @@ -1,8 +1,8 @@ "use strict"; -let GrammarError = require("../../grammar-error"); -let asts = require("../asts"); -let visitor = require("../visitor"); +const GrammarError = require( "../../grammar-error" ); +const asts = require( "../asts" ); +const visitor = require( "../visitor" ); // Reports left recursion in the grammar, which prevents infinite recursion in // the generated parser. @@ -14,41 +14,52 @@ 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 check = visitor.build({ - rule(node) { - visitedRules.push(node.name); - check(node.expression); - visitedRules.pop(node.name); - }, - - sequence(node) { - node.elements.every(element => { - check(element); - - return !asts.alwaysConsumesOnSuccess(ast, element); - }); - }, - - rule_ref(node) { - if (visitedRules.indexOf(node.name) !== -1) { - visitedRules.push(node.name); - - throw new GrammarError( - "Possible infinite loop when parsing (left recursion: " - + visitedRules.join(" -> ") - + ").", - node.location - ); - } - - check(asts.findRule(ast, node.name)); - } - }); - - check(ast); +function reportInfiniteRecursion( ast ) { + + const visitedRules = []; + + const check = visitor.build( { + rule( node ) { + + visitedRules.push( node.name ); + check( node.expression ); + visitedRules.pop( node.name ); + + }, + + sequence( node ) { + + node.elements.every( element => { + + check( element ); + + return ! asts.alwaysConsumesOnSuccess( ast, element ); + + } ); + + }, + + rule_ref( node ) { + + if ( visitedRules.indexOf( node.name ) !== -1 ) { + + visitedRules.push( node.name ); + const rulePath = visitedRules.join( " -> " ); + + throw new GrammarError( + `Possible infinite loop when parsing (left recursion: ${ rulePath }).`, + node.location + ); + + } + + check( asts.findRule( ast, node.name ) ); + + } + } ); + + 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..4bf756f 100644 --- a/lib/compiler/passes/report-infinite-repetition.js +++ b/lib/compiler/passes/report-infinite-repetition.js @@ -1,33 +1,43 @@ "use strict"; -let GrammarError = require("../../grammar-error"); -let asts = require("../asts"); -let visitor = require("../visitor"); +const GrammarError = require( "../../grammar-error" ); +const asts = require( "../asts" ); +const 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 - ); - } - }, - - 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); +function reportInfiniteRepetition( ast ) { + + const 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 + ); + + } + + } + } ); + + check( ast ); + } module.exports = reportInfiniteRepetition; diff --git a/lib/compiler/passes/report-undefined-rules.js b/lib/compiler/passes/report-undefined-rules.js index 4108432..798568e 100644 --- a/lib/compiler/passes/report-undefined-rules.js +++ b/lib/compiler/passes/report-undefined-rules.js @@ -1,32 +1,43 @@ "use strict"; -let GrammarError = require("../../grammar-error"); -let asts = require("../asts"); -let visitor = require("../visitor"); +const GrammarError = require( "../../grammar-error" ); +const asts = require( "../asts" ); +const visitor = require( "../visitor" ); // Checks that all referenced rules exist. -function reportUndefinedRules(ast, options) { - let check = visitor.build({ - rule_ref(node) { - if (!asts.findRule(ast, node.name)) { - throw new GrammarError( - "Rule \"" + node.name + "\" is not defined.", - node.location - ); - } +function reportUndefinedRules( ast, options ) { + + const 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 ); + + if ( options.allowedStartRules ) { + + options.allowedStartRules.forEach( rule => { + + if ( ! asts.findRule( ast, rule ) ) { + + throw new GrammarError( `Start rule "${ rule }" is not defined.` ); + + } + + } ); + } - }); - - check(ast); - - if (options.allowedStartRules) { - options.allowedStartRules.forEach(rule => { - if (!asts.findRule(ast, rule)) { - throw new GrammarError( - "Start rule \"" + rule + "\" is not defined."); - } - }); - } + } module.exports = reportUndefinedRules; diff --git a/lib/compiler/visitor.js b/lib/compiler/visitor.js index 8f5d08a..d4b97c7 100644 --- a/lib/compiler/visitor.js +++ b/lib/compiler/visitor.js @@ -1,75 +1,97 @@ "use strict"; // Simple AST node visitor builder. -let visitor = { - build(functions) { - function visit(node) { - return functions[node.type].apply(null, arguments); - } +const visitor = { + build( functions ) { - function visitNop() { - // Do nothing. - } + function visit( node ) { - function visitExpression(node) { - let extraArgs = Array.prototype.slice.call(arguments, 1); + return functions[ node.type ].apply( null, arguments ); - visit.apply(null, [node.expression].concat(extraArgs)); - } + } - function visitChildren(property) { - return function(node) { - let extraArgs = Array.prototype.slice.call(arguments, 1); + function visitNop() { + // Do nothing. + } - node[property].forEach(child => { - visit.apply(null, [child].concat(extraArgs)); - }); - }; - } + function visitExpression( node ) { + + const extraArgs = Array.prototype.slice.call( arguments, 1 ); - const DEFAULT_FUNCTIONS = { - grammar(node) { - let extraArgs = Array.prototype.slice.call(arguments, 1); + visit( ...[ node.expression ].concat( extraArgs ) ); - if (node.initializer) { - visit.apply(null, [node.initializer].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 - }; - - Object.keys(DEFAULT_FUNCTIONS).forEach(type => { - if (!Object.prototype.hasOwnProperty.call(functions, type)) { - functions[type] = DEFAULT_FUNCTIONS[type]; - } - }); - - return visit; - } + function visitChildren( property ) { + + return function visitProperty( node ) { + + const extraArgs = Array.prototype.slice.call( arguments, 1 ); + + node[ property ].forEach( child => { + + visit( ...[ child ].concat( extraArgs ) ); + + } ); + + }; + + } + + const DEFAULT_FUNCTIONS = { + grammar( node ) { + + const extraArgs = Array.prototype.slice.call( arguments, 1 ); + + if ( node.initializer ) { + + visit( ...[ node.initializer ].concat( extraArgs ) ); + + } + + node.rules.forEach( rule => { + + visit( ...[ 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 + }; + + Object.keys( DEFAULT_FUNCTIONS ).forEach( type => { + + if ( ! Object.prototype.hasOwnProperty.call( functions, type ) ) { + + functions[ type ] = DEFAULT_FUNCTIONS[ type ]; + + } + + } ); + + return visit; + + } }; module.exports = visitor; diff --git a/lib/grammar-error.js b/lib/grammar-error.js index ce3fb98..68fbaff 100644 --- a/lib/grammar-error.js +++ b/lib/grammar-error.js @@ -2,15 +2,21 @@ // Thrown when the grammar contains an error. class GrammarError { - constructor(message, location) { - this.name = "GrammarError"; - this.message = message; - this.location = location; - if (typeof Error.captureStackTrace === "function") { - Error.captureStackTrace(this, GrammarError); + constructor( message, location ) { + + this.name = "GrammarError"; + this.message = message; + this.location = location; + + if ( typeof Error.captureStackTrace === "function" ) { + + Error.captureStackTrace( this, GrammarError ); + + } + } - } + } module.exports = GrammarError; diff --git a/lib/peg.js b/lib/peg.js index a4639fe..66d1191 100644 --- a/lib/peg.js +++ b/lib/peg.js @@ -1,54 +1,64 @@ "use strict"; -let GrammarError = require("./grammar-error"); -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; - } +const GrammarError = require( "./grammar-error" ); +const compiler = require( "./compiler" ); +const parser = require( "./parser" ); + +const 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 = typeof options !== "undefined" ? options : {}; + + function convertPasses( passes ) { + + const converted = {}; + + Object.keys( passes ).forEach( stage => { - let plugins = "plugins" in options ? options.plugins : []; - let config = { - parser: peg.parser, - passes: convertPasses(peg.compiler.passes) - }; + converted[ stage ] = Object.keys( passes[ stage ] ) + .map( name => passes[ stage ][ name ] ); - plugins.forEach(p => { p.use(config, options); }); + } ); - return peg.compiler.compile( - config.parser.parse(grammar), - config.passes, - options - ); - } + return converted; + + } + + const plugins = "plugins" in options ? options.plugins : []; + const 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/package.json b/package.json index ce9d644..10f3258 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "devDependencies": { "babel-preset-es2015": "6.24.1", "babel-core": "6.26.0", + "dedent": "0.7.0", "babelify": "8.0.0", "browserify": "14.5.0", "chai": "4.1.2", diff --git a/test/.eslintrc.js b/test/.eslintrc.js index d2cf41d..f3e9e90 100644 --- a/test/.eslintrc.js +++ b/test/.eslintrc.js @@ -5,8 +5,10 @@ module.exports = { "extends": "futagozaryuu/test", "rules": { - "node/shebang": 0 + "node/shebang": 0, + "func-names": 0, + "no-mixed-operators": 0, - } + }, }; diff --git a/test/benchmark/benchmarks.js b/test/benchmark/benchmarks.js index 227888b..3dcb899 100644 --- a/test/benchmark/benchmarks.js +++ b/test/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)" } - ] - } +const 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)" } + ] + } ]; module.exports = benchmarks; diff --git a/test/benchmark/index.js b/test/benchmark/index.js index 57fd4aa..75bdc11 100644 --- a/test/benchmark/index.js +++ b/test/benchmark/index.js @@ -2,137 +2,152 @@ /* eslint-env browser, jquery */ -let Runner = require("./runner.js"); -let benchmarks = require("./benchmarks.js"); - -$("#run").click(() => { - // Results Table Manipulation - - let resultsTable = $("#results-table"); - - function appendResult(klass, title, url, inputSize, parseTime) { - const KB = 1024; - const MS_IN_S = 1000; - - resultsTable.append( - "" - + "" - + (url !== null ? "" : "") - + title - + (url !== null ? "" : "") - + "" - + "" - + "" - + (inputSize / KB).toFixed(2) - + "" - + " kB" - + "" - + "" - + "" - + parseTime.toFixed(2) - + "" - + " ms" - + "" - + "" - + "" - + ((inputSize / KB) / (parseTime / MS_IN_S)).toFixed(2) - + "" - + " 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: "benchmark/" + file, - dataType: "text", - async: false - }).responseText; - }, - - testStart() { - // Nothing to do. - }, - - testFinish(benchmark, test, inputSize, parseTime) { - appendResult( - "individual", - test.title, - "benchmark/" + benchmark.id + "/" + test.file, - inputSize, - parseTime - ); - }, - - benchmarkStart(benchmark) { - resultsTable.append( - "" - + "" - + 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"); +const Runner = require( "./runner.js" ); +const benchmarks = require( "./benchmarks.js" ); + +$( "#run" ).click( () => { + + // Results Table Manipulation + + const resultsTable = $( "#results-table" ); + + function appendResult( klass, title, url, inputSize, parseTime ) { + + const KB = 1024; + const MS_IN_S = 1000; + + resultsTable.append( + "" + + "" + + ( url !== null ? "" : "" ) + + title + + ( url !== null ? "" : "" ) + + "" + + "" + + "" + + ( inputSize / KB ).toFixed( 2 ) + + "" + + " kB" + + "" + + "" + + "" + + parseTime.toFixed( 2 ) + + "" + + " ms" + + "" + + "" + + "" + + ( ( inputSize / KB ) / ( parseTime / MS_IN_S ) ).toFixed( 2 ) + + "" + + " 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. + + const runCount = parseInt( $( "#run-count" ).val(), 10 ); + const options = { + cache: $( "#cache" ).is( ":checked" ), + optimize: $( "#optimize" ).val() + }; + + if ( isNaN( runCount ) || runCount <= 0 ) { + + alert( "Number of runs must be a positive integer." ); + + return; + } - }); -}); -$(document).ready(() => { - $("#run").focus(); -}); + Runner.run( benchmarks, runCount, options, { + readFile( file ) { + + return $.ajax( { + type: "GET", + url: "benchmark/" + file, + dataType: "text", + async: false + } ).responseText; + + }, + + testStart() { + // Nothing to do. + }, + + testFinish( benchmark, test, inputSize, parseTime ) { + + appendResult( + "individual", + test.title, + "benchmark/" + benchmark.id + "/" + test.file, + inputSize, + parseTime + ); + + }, + + benchmarkStart( benchmark ) { + + resultsTable.append( + "" + + "" + + 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() ); diff --git a/test/benchmark/run b/test/benchmark/run index c4c16a3..278c8ab 100644 --- a/test/benchmark/run +++ b/test/benchmark/run @@ -2,195 +2,251 @@ "use strict"; -let Runner = require("./runner.js"); -let benchmarks = require("./benchmarks.js"); -let fs = require("fs"); +const Runner = require( "./runner.js" ); +const benchmarks = require( "./benchmarks.js" ); +const fs = require( "fs" ); +const path = require( "path" ); // Results Table Manipulation -function dup(text, count) { - let result = ""; +function dup( text, count ) { - for (let i = 1; i <= count; i++) { - result += text; - } + let result = ""; + + for ( let i = 1; i <= count; i++ ) result += text; + + return result; - return result; } -function padLeft(text, length) { - return dup(" ", length - text.length) + text; +function padLeft( text, length ) { + + return dup( " ", length - text.length ) + text; + } -function padRight(text, length) { - return text + dup(" ", length - text.length); +function padRight( text, length ) { + + return text + dup( " ", length - text.length ); + } -function center(text, length) { - let padLength = (length - text.length) / 2; +function center( text, length ) { + + const padLength = ( length - text.length ) / 2; + + return dup( " ", Math.floor( padLength ) ) + + text + + dup( " ", Math.ceil( padLength ) ); - return dup(" ", Math.floor(padLength)) - + text - + dup(" ", Math.ceil(padLength)); } function writeTableHeader() { - console.log("┌─────────────────────────────────────┬───────────┬────────────┬──────────────┐"); - console.log("│ Test │ Inp. size │ Avg. time │ Avg. speed │"); + + console.log( "┌─────────────────────────────────────┬───────────┬────────────┬──────────────┐" ); + console.log( "│ Test │ Inp. size │ Avg. time │ Avg. speed │" ); + } -function writeHeading(heading) { - console.log("├─────────────────────────────────────┴───────────┴────────────┴──────────────┤"); - console.log("│ " + center(heading, 75) + " │"); - console.log("├─────────────────────────────────────┬───────────┬────────────┬──────────────┤"); +function writeHeading( heading ) { + + console.log( "├─────────────────────────────────────┴───────────┴────────────┴──────────────┤" ); + console.log( "│ " + center( heading, 75 ) + " │" ); + console.log( "├─────────────────────────────────────┬───────────┬────────────┬──────────────┤" ); + } -function writeResult(title, inputSize, parseTime) { - const KB = 1024; - const MS_IN_S = 1000; +function writeResult( title, inputSize, parseTime ) { + + const KB = 1024; + const MS_IN_S = 1000; + + console.log( + "│ " + + padRight( title, 35 ) + + " │ " + + padLeft( ( inputSize / KB ).toFixed( 2 ), 6 ) + + " kB │ " + + padLeft( parseTime.toFixed( 2 ), 7 ) + + " ms │ " + + padLeft( ( ( inputSize / KB ) / ( parseTime / MS_IN_S ) ).toFixed( 2 ), 7 ) + + " kB/s │" + ); - console.log("│ " - + padRight(title, 35) - + " │ " - + padLeft((inputSize / KB).toFixed(2), 6) - + " kB │ " - + padLeft(parseTime.toFixed(2), 7) - + " ms │ " - + padLeft(((inputSize / KB) / (parseTime / MS_IN_S)).toFixed(2), 7) - + " kB/s │" - ); } function writeSeparator() { - console.log("├─────────────────────────────────────┼───────────┼────────────┼──────────────┤"); + + console.log( "├─────────────────────────────────────┼───────────┼────────────┼──────────────┤" ); + } function writeTableFooter() { - console.log("└─────────────────────────────────────┴───────────┴────────────┴──────────────┘"); + + console.log( "└─────────────────────────────────────┴───────────┴────────────┴──────────────┘" ); + } // Helpers function printHelp() { - console.log("Usage: run [options]"); - console.log(""); - console.log("Runs PEG.js benchmark suite."); - console.log(""); - console.log("Options:"); - console.log(" -n, --run-count number of runs (default: 10)"); - console.log(" --cache make tested parsers cache results"); - console.log(" -o, --optimize select optimization for speed or size (default:"); - console.log(" speed)"); + + console.log( "Usage: run [options]" ); + console.log( "" ); + console.log( "Runs PEG.js benchmark suite." ); + console.log( "" ); + console.log( "Options:" ); + console.log( " -n, --run-count number of runs (default: 10)" ); + console.log( " --cache make tested parsers cache results" ); + console.log( " -o, --optimize select optimization for speed or size (default:" ); + console.log( " speed)" ); + } function exitSuccess() { - process.exit(0); + + process.exit( 0 ); + } function exitFailure() { - process.exit(1); + + process.exit( 1 ); + } -function abort(message) { - console.error(message); - exitFailure(); +function abort( message ) { + + console.error( message ); + exitFailure(); + } // Arguments -let args = process.argv.slice(2); // Trim "node" and the script path. +const args = process.argv.slice( 2 ); // Trim "node" and the script path. + +function isOption( arg ) { + + return ( /^-/ ).test( arg ); -function isOption(arg) { - return (/^-/).test(arg); } function nextArg() { - args.shift(); + + args.shift(); + } // Main let runCount = 10; -let options = { - cache: false, - optimize: "speed" +const options = { + cache: false, + optimize: "speed" }; -while (args.length > 0 && isOption(args[0])) { - switch (args[0]) { - case "-n": - case "--run-count": - nextArg(); - if (args.length === 0) { - abort("Missing parameter of the -n/--run-count option."); - } - runCount = parseInt(args[0], 10); - if (isNaN(runCount) || runCount <= 0) { - abort("Number of runs must be a positive integer."); - } - break; - - case "--cache": - options.cache = true; - break; - - case "-o": - case "--optimize": - nextArg(); - if (args.length === 0) { - abort("Missing parameter of the -o/--optimize option."); - } - if (args[0] !== "speed" && args[0] !== "size") { - abort("Optimization goal must be either \"speed\" or \"size\"."); - } - options.optimize = args[0]; - break; - - case "-h": - case "--help": - printHelp(); - exitSuccess(); - break; - - default: - abort("Unknown option: " + args[0] + "."); - } - nextArg(); -} - -if (args.length > 0) { - abort("No arguments are allowed."); -} - -Runner.run(benchmarks, runCount, options, { - readFile(file) { - return fs.readFileSync(__dirname + "/" + file, "utf8"); - }, - - testStart() { +while ( args.length > 0 && isOption( args[ 0 ] ) ) { + + switch ( args[ 0 ] ) { + + case "-n": + case "--run-count": + nextArg(); + if ( args.length === 0 ) { + + abort( "Missing parameter of the -n/--run-count option." ); + + } + runCount = parseInt( args[ 0 ], 10 ); + if ( isNaN( runCount ) || runCount <= 0 ) { + + abort( "Number of runs must be a positive integer." ); + + } + break; + + case "--cache": + options.cache = true; + break; + + case "-o": + case "--optimize": + nextArg(); + if ( args.length === 0 ) { + + abort( "Missing parameter of the -o/--optimize option." ); + + } + if ( args[ 0 ] !== "speed" && args[ 0 ] !== "size" ) { + + abort( "Optimization goal must be either \"speed\" or \"size\"." ); + + } + options.optimize = args[ 0 ]; + break; + + case "-h": + case "--help": + printHelp(); + exitSuccess(); + break; + + default: + abort( "Unknown option: " + args[ 0 ] + "." ); + + } + nextArg(); + +} + +if ( args.length > 0 ) { + + abort( "No arguments are allowed." ); + +} + +Runner.run( benchmarks, runCount, options, { + readFile( file ) { + + return fs.readFileSync( path.join( __dirname, file ), "utf8" ); + + }, + + testStart() { // Nothing to do. - }, - - testFinish(benchmark, test, inputSize, parseTime) { - writeResult(test.title, inputSize, parseTime); - }, - - benchmarkStart(benchmark) { - writeHeading(benchmark.title); - }, - - benchmarkFinish(benchmark, inputSize, parseTime) { - writeSeparator(); - writeResult(benchmark.title + " total", inputSize, parseTime); - }, - - start() { - writeTableHeader(); - }, - - finish(inputSize, parseTime) { - writeSeparator(); - writeResult("Total", inputSize, parseTime); - writeTableFooter(); - } -}); + }, + + testFinish( benchmark, test, inputSize, parseTime ) { + + writeResult( test.title, inputSize, parseTime ); + + }, + + benchmarkStart( benchmark ) { + + writeHeading( benchmark.title ); + + }, + + benchmarkFinish( benchmark, inputSize, parseTime ) { + + writeSeparator(); + writeResult( benchmark.title + " total", inputSize, parseTime ); + + }, + + start() { + + writeTableHeader(); + + }, + + finish( inputSize, parseTime ) { + + writeSeparator(); + writeResult( "Total", inputSize, parseTime ); + writeTableFooter(); + + } +} ); diff --git a/test/benchmark/runner.js b/test/benchmark/runner.js index 7b49506..7a6f68c 100644 --- a/test/benchmark/runner.js +++ b/test/benchmark/runner.js @@ -1,118 +1,150 @@ "use strict"; -/* global setTimeout */ +const peg = require( "../../lib/peg" ); -let peg = require("../../lib/peg"); +const Runner = { + run( benchmarks, runCount, options, callbacks ) { -let Runner = { - run(benchmarks, runCount, options, callbacks) { - // Queue + // Queue - let Q = { - functions: [], + const Q = { + functions: [], - add(f) { - this.functions.push(f); - }, + add( f ) { - run() { - if (this.functions.length > 0) { - this.functions.shift()(); + 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. + + const state = {}; + + function initialize() { + + callbacks.start(); + + state.totalInputSize = 0; + state.totalParseTime = 0; - // 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 benchmarkInitializer( benchmark ) { + + return function () { + + callbacks.benchmarkStart( benchmark ); - function testRunner(benchmark, test) { - return function() { - callbacks.testStart(benchmark, test); + state.parser = peg.generate( + callbacks.readFile( "../../examples/" + benchmark.id + ".pegjs" ), + options + ); + state.benchmarkInputSize = 0; + state.benchmarkParseTime = 0; - 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); + function testRunner( benchmark, test ) { - state.benchmarkInputSize += input.length; - state.benchmarkParseTime += averageParseTime; - }; - } + return function () { - function benchmarkFinalizer(benchmark) { - return function() { - callbacks.benchmarkFinish( - benchmark, - state.benchmarkInputSize, - state.benchmarkParseTime - ); - - state.totalInputSize += state.benchmarkInputSize; - state.totalParseTime += state.benchmarkParseTime; - }; - } + callbacks.testStart( benchmark, test ); - function finalize() { - callbacks.finish(state.totalInputSize, state.totalParseTime); - } + const input = callbacks.readFile( benchmark.id + "/" + test.file ); + + let parseTime = 0; + for ( let i = 0; i < runCount; i++ ) { + + const t = ( new Date() ).getTime(); + state.parser.parse( input ); + parseTime += ( new Date() ).getTime() - t; - // Main + } + const averageParseTime = parseTime / runCount; - 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); + callbacks.testFinish( benchmark, test, input.length, averageParseTime ); - Q.run(); - } + 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/test/impact b/test/impact index ebbaa3b..8b59113 100644 --- a/test/impact +++ b/test/impact @@ -1,150 +1,179 @@ #!/usr/bin/env node -/* eslint camelcase:0, max-len:0, one-var:0 */ - // // Measures impact of a Git commit (or multiple commits) on generated parsers // speed and size. Makes sense to use only on PEG.js git repository checkout. // +/* eslint prefer-const: 0 */ + "use strict"; -let child_process = require("child_process"); -let fs = require("fs"); -let os = require("os"); -let path = require("path"); -let glob = require("glob"); +const child_process = require( "child_process" ); +const fs = require( "fs" ); +const os = require( "os" ); +const path = require( "path" ); +const dedent = require( "dedent" ); +const glob = require( "glob" ); // Current Working Directory -let cwd = path.join(__dirname, ".."); - -if (process.cwd() !== cwd) { - process.chdir(cwd); -} +const cwd = path.join( __dirname, ".." ); +if ( process.cwd() !== cwd ) process.chdir( cwd ); // Execution Files let PEGJS_BIN = "bin/peg.js"; let BENCHMARK_BIN = "test/benchmark/run"; -if (!fs.existsSync(PEGJS_BIN)) { - PEGJS_BIN = "bin/pegjs"; +if ( ! fs.existsSync( PEGJS_BIN ) ) { + + PEGJS_BIN = "bin/pegjs"; + } -if (!fs.existsSync(BENCHMARK_BIN)) { - BENCHMARK_BIN = "benchmark/run"; +if ( ! fs.existsSync( BENCHMARK_BIN ) ) { + + BENCHMARK_BIN = "benchmark/run"; + } // Utils -let print = console.log; +function echo( message ) { + + process.stdout.write( message ); -function echo(message) { - process.stdout.write(message); } -function exec(command) { - return child_process.execSync(command, { encoding: "utf8" }); +function exec( command ) { + + return child_process.execSync( command, { encoding: "utf8" } ); + } -function prepare(commit) { - exec(`git checkout --quiet "${commit}"`); +function prepare( commit ) { + + exec( `git checkout --quiet "${ commit }"` ); + } function runBenchmark() { - return parseFloat( - exec("node " + BENCHMARK_BIN) - // Split by table seprator, reverse and return the total bytes per second - .split("│") - .reverse()[1] - // Trim the whitespaces and remove ` kB/s` from the end - .trim() - .slice(0, -5) - ); + + return parseFloat( + exec( "node " + BENCHMARK_BIN ) + + // Split by table seprator, reverse and return the total bytes per second + .split( "│" ) + .reverse()[ 1 ] + + // Trim the whitespaces and remove ` kB/s` from the end + .trim() + .slice( 0, -5 ) + ); + } function measureSpeed() { - return (runBenchmark() + runBenchmark() + runBenchmark() + runBenchmark() + runBenchmark() / 5).toFixed(2); + + return ( runBenchmark() + runBenchmark() + runBenchmark() + runBenchmark() + runBenchmark() / 5 ).toFixed( 2 ); + } function measureSize() { - let size = 0; - glob.sync("examples/*.pegjs") - .forEach(example => { - exec(`node ${PEGJS_BIN} ${example}`); - example = example.slice(0, -5) + "js"; - size += fs.statSync(example).size; - fs.unlinkSync(example); - }); + let size = 0; + + glob.sync( "examples/*.pegjs" ) + .forEach( example => { + + exec( `node ${ PEGJS_BIN } ${ example }` ); + example = example.slice( 0, -5 ) + "js"; + size += fs.statSync( example ).size; + fs.unlinkSync( example ); + + } ); + + return size; - return size; } -function difference($1, $2) { - return (($2 / $1 - 1) * 100).toFixed(4); +function difference( $1, $2 ) { + + return ( ( $2 / $1 - 1 ) * 100 ).toFixed( 4 ); + } // Prepare -let argv = process.argv.slice(2); +const argv = process.argv.slice( 2 ); let commit_before, commit_after; -if (argv.length === 1) { - commit_before = argv[0] + "~1"; - commit_after = argv[0]; -} else if (argv.length === 2) { - commit_before = argv[0]; - commit_after = argv[1]; +if ( argv.length === 1 ) { + + commit_before = argv[ 0 ] + "~1"; + commit_after = argv[ 0 ]; + +} else if ( argv.length === 2 ) { + + commit_before = argv[ 0 ]; + commit_after = argv[ 1 ]; + } else { - print("Usage:"); - print(""); - print(" test/impact "); - print(" test/impact "); - print(""); - print("Measures impact of a Git commit (or multiple commits) on generated parsers'"); - print("speed and size. Makes sense to use only on PEG.js Git repository checkout."); - print(""); - process.exit(1); + + console.log( dedent` + + Usage: + + test/impact + test/impact + + Measures impact of a Git commit (or multiple commits) on generated parser's + speed and size. Makes sense to use only on PEG.js Git repository checkout. + + ` ); + process.exit( 1 ); + } // Measure -let branch = exec("git rev-parse --abbrev-ref HEAD"); +const branch = exec( "git rev-parse --abbrev-ref HEAD" ); let speed1, size1, speed2, size2; -echo(`Measuring commit ${commit_before}...`); -prepare(commit_before); +echo( `Measuring commit ${ commit_before }...` ); +prepare( commit_before ); speed1 = measureSpeed(); size1 = measureSize(); -echo(" OK" + os.EOL); +echo( " OK" + os.EOL ); -echo(`Measuring commit ${commit_after}...`); -prepare(commit_after); +echo( `Measuring commit ${ commit_after }...` ); +prepare( commit_after ); speed2 = measureSpeed(); size2 = measureSize(); -echo(" OK" + os.EOL); +echo( " OK" + os.EOL ); // Finish -prepare(branch); +prepare( branch ); + +console.log( dedent` + + test/impact ${ commit_before } ${ commit_after } -print(` -test/impact ${commit_before} ${commit_after} + Speed impact + ------------ + Before: ${ speed1 } kB/s + After: ${ speed2 } kB/s + Difference: ${ difference( parseFloat( speed1 ), parseFloat( speed2 ) ) }% - Speed impact - ------------ - Before: ${speed1} kB/s - After: ${speed2} kB/s - Difference: ${difference(parseFloat(speed1), parseFloat(speed2))}% + Size impact + ----------- + Before: ${ size1 } b + After: ${ size2 } b + Difference: ${ difference( size1, size2 ) }% - Size impact - ----------- - Before: ${size1} b - After: ${size2} b - Difference: ${difference(size1, size2)}% + - Measured by /test/impact with Node.js ${ process.version } + - Your system: ${ os.type() } ${ os.release() } ${ os.arch() }. -- Measured by /test/impact with Node.js ${process.version} -- Your system: ${os.type()} ${os.release()} ${os.arch()}. -`); +` ); diff --git a/test/server/run b/test/server/run index e2bac59..cc3e0e9 100644 --- a/test/server/run +++ b/test/server/run @@ -2,31 +2,35 @@ "use strict"; -let babelify = require("babelify"); -let browserify = require("browserify"); -let express = require("express"); -let glob = require("glob"); -let logger = require("morgan"); - -let app = express(); - -app.use(logger("dev")); -app.use(express.static(__dirname)); -app.use("/benchmark", express.static(`${__dirname}/../benchmark`)); -app.use("/examples", express.static(`${__dirname}/../../examples`)); - -app.get("/:dir/bundle.js", (req, res) => { - browserify(glob.sync( - `${__dirname}/../${req.params.dir}/**/*.js` - )) - .transform(babelify, { - presets: "es2015", - compact: false - }) - .bundle() - .pipe(res); -}); - -app.listen(8000, () => { - console.log("Test server running at: http://localhost:8000/"); -}); +const babelify = require( "babelify" ); +const browserify = require( "browserify" ); +const express = require( "express" ); +const glob = require( "glob" ); +const logger = require( "morgan" ); + +const app = express(); + +app.use( logger( "dev" ) ); +app.use( express.static( __dirname ) ); +app.use( "/benchmark", express.static( `${ __dirname }/../benchmark` ) ); +app.use( "/examples", express.static( `${ __dirname }/../../examples` ) ); + +app.get( "/:dir/bundle.js", ( req, res ) => { + + browserify( glob.sync( + `${ __dirname }/../${ req.params.dir }/**/*.js` + ) ) + .transform( babelify, { + presets: "es2015", + compact: false + } ) + .bundle() + .pipe( res ); + +} ); + +app.listen( 8000, () => { + + console.log( "Test server running at: http://localhost:8000/" ); + +} ); diff --git a/test/spec/api/generated-parser-api.spec.js b/test/spec/api/generated-parser-api.spec.js index 2e7e84e..63b428b 100644 --- a/test/spec/api/generated-parser-api.spec.js +++ b/test/spec/api/generated-parser-api.spec.js @@ -1,168 +1,216 @@ "use strict"; -/* global console */ - -let chai = require("chai"); -let peg = require("../../../lib/peg"); -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 }); - }); - }); -}); +const chai = require( "chai" ); +const peg = require( "../../../lib/peg" ); +const sinon = require( "sinon" ); + +const expect = chai.expect; + +describe( "generated parser API", function () { + + describe( "parse", function () { + + it( "parses input", function () { + + const parser = peg.generate( "start = 'a'" ); + expect( parser.parse( "a" ) ).to.equal( "a" ); + + } ); + + it( "throws an exception on syntax error", function () { + + const parser = peg.generate( "start = 'a'" ); + expect( () => { + + parser.parse( "b" ); + + } ).to.throw(); + + } ); + + describe( "start rule", function () { + + const parser = peg.generate( ` + + a = 'x' { return 'a'; } + b = 'x' { return 'b'; } + c = 'x' { return 'c'; } + + `, { 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 () { + + const parser = peg.generate( ` + + start = a / b + a = 'a' + b = 'b' + + `, { trace: true } ); + + describe( "default tracer", function () { + + it( "traces using console.log (if console is defined)", function () { + + const 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 ) => { + + const 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 () { + + const 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 } + } + } + ]; + + const tracer = { trace: sinon.spy() }; + parser.parse( "b", { tracer: tracer } ); + + expect( tracer.trace.callCount ).to.equal( events.length ); + events.forEach( ( event, index ) => { + + const call = tracer.trace.getCall( index ); + expect( call.calledWithExactly( event ) ).to.equal( true ); + + } ); + + } ); + + } ); + + } ); + + } ); + + it( "accepts custom options", function () { + + const parser = peg.generate( "start = 'a'" ); + parser.parse( "a", { foo: 42 } ); + + } ); + + } ); + +} ); diff --git a/test/spec/api/pegjs-api.spec.js b/test/spec/api/pegjs-api.spec.js index a0ad3c1..4e8e422 100644 --- a/test/spec/api/pegjs-api.spec.js +++ b/test/spec/api/pegjs-api.spec.js @@ -1,206 +1,319 @@ "use strict"; -let chai = require("chai"); -let peg = require("../../../lib/peg"); -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"); - - it("throws an error on missing rule", function() { - expect(() => peg.generate(grammar, { - allowedStartRules: ["missing"] - })).to.throw(); - }); - - // 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. +const chai = require( "chai" ); +const peg = require( "../../../lib/peg" ); +const sinon = require( "sinon" ); - // The |plugins| option is tested in plugin API tests. +const expect = chai.expect; - it("accepts custom options", function() { - peg.generate("start = 'a'", { foo: 42 }); - }); - }); -}); +describe( "PEG.js API", function () { + + describe( "generate", function () { + + it( "generates a parser", function () { + + const 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 () { + + const grammar = ` + + a = 'x' + b = 'x' + c = 'x' + + `; + + it( "throws an error on missing rule", function () { + + expect( () => { + + peg.generate( grammar, { allowedStartRules: [ "missing" ] } ); + + } ).to.throw(); + + } ); + + // 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const grammar = ` + + { var n = 0; } + start = (a 'b') / (a 'c') { return n; } + a = 'a' { n++; } + + `; + + describe( "when |cache| is not set", function () { + + it( "generated parser doesn't cache intermediate parse results", function () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( grammar, { cache: true } ); + expect( parser.parse( "ac" ) ).to.equal( 1 ); + + } ); + + } ); + + } ); + + describe( "tracing", function () { + + const grammar = "start = 'a'"; + + describe( "when |trace| is not set", function () { + + it( "generated parser doesn't trace", function () { + + const parser = peg.generate( grammar ); + const 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 () { + + const parser = peg.generate( grammar, { trace: false } ); + const 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 () { + + const parser = peg.generate( grammar, { trace: true } ); + const 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 () { + + const grammar = "start = 'a'"; + + describe( "when |output| is not set", function () { + + it( "returns generated parser object", function () { + + const 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 () { + + const 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 () { + + const 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 |plugins| option is tested in plugin API tests. + + it( "accepts custom options", function () { + + peg.generate( "start = 'a'", { foo: 42 } ); + + } ); + + } ); + +} ); diff --git a/test/spec/api/plugin-api.spec.js b/test/spec/api/plugin-api.spec.js index 93342df..31eae10 100644 --- a/test/spec/api/plugin-api.spec.js +++ b/test/spec/api/plugin-api.spec.js @@ -1,128 +1,185 @@ "use strict"; -let chai = require("chai"); -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"); - }); - }); -}); +const chai = require( "chai" ); +const peg = require( "../../../lib/peg" ); + +const expect = chai.expect; + +describe( "plugin API", function () { + + describe( "use", function () { + + const grammar = "start = 'a'"; + + it( "is called for each plugin", function () { + + const pluginsUsed = [ false, false, false ]; + const 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 () { + + const 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 () { + + const generateOptions = { + plugins: [ { + use( config, options ) { + + expect( options ).to.equal( generateOptions ); + + } + } ], + foo: 42 + }; + + peg.generate( grammar, generateOptions ); + + } ); + + it( "can replace parser", function () { + + const plugin = { + use( config ) { + + config.parser = peg.generate( ` + + start = .* { + return { + type: 'grammar', + rules: [{ + type: 'rule', + name: 'start', + expression: { + type: 'literal', + value: text(), + ignoreCase: false + } + }] + }; + } + + ` ); + + } + }; + + const parser = peg.generate( "a", { plugins: [ plugin ] } ); + expect( parser.parse( "a" ) ).to.equal( "a" ); + + } ); + + it( "can change compiler passes", function () { + + const plugin = { + use( config ) { + + function pass( ast ) { + + ast.code = "({ parse: function() { return 42; } })"; + + } + + config.passes.generate = [ pass ]; + + } + }; + + const parser = peg.generate( grammar, { plugins: [ plugin ] } ); + expect( parser.parse( "a" ) ).to.equal( 42 ); + + } ); + + it( "can change options", function () { + + const grammar = ` + + a = 'x' + b = 'x' + c = 'x' + + `; + const plugin = { + use( config, options ) { + + options.allowedStartRules = [ "b", "c" ]; + + } + }; + + const 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/spec/behavior/generated-parser-behavior.spec.js b/test/spec/behavior/generated-parser-behavior.spec.js index aeea59b..472ee1f 100644 --- a/test/spec/behavior/generated-parser-behavior.spec.js +++ b/test/spec/behavior/generated-parser-behavior.spec.js @@ -1,1574 +1,2028 @@ "use strict"; -/* global console */ +const chai = require( "chai" ); +const peg = require( "../../../lib/peg" ); +const sinon = require( "sinon" ); -let chai = require("chai"); -let peg = require("../../../lib/peg"); -let sinon = require("sinon"); +const expect = chai.expect; -let expect = chai.expect; +describe( "generated parser behavior", function () { -describe("generated parser behavior", function() { - function varyOptimizationOptions(block) { - function clone(object) { - let result = {}; + function varyOptimizationOptions( block ) { - Object.keys(object).forEach(key => { - result[key] = object[key]; - }); + function clone( object ) { - return result; - } + const 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"); - } + Object.keys( object ).forEach( key => { - 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 - ); + result[ key ] = object[ key ]; - 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 - ); + return result; - 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 - ); + const 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 } + ]; - 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 - ); + optionsVariants.forEach( variant => { + + describe( + "with options " + chai.util.inspect( variant ), + function () { - expect(parser).to.parse("ab"); - }); + block( clone( variant ) ); - 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 } - }); - }); - - it("|offset| returns current start offset", function() { - let parser = peg.generate([ - "start = [0-9]+ val:mark { return val; }", - "mark = 'xx' { return offset(); }" - ].join("\n"), options); - - expect(parser).to.parse("0123456xx", 7); - }); - - it("|range| returns current range", function() { - let parser = peg.generate([ - "start = [0-9]+ val:mark { return val; }", - "mark = 'xx' { return range(); }" - ].join("\n"), options); - - expect(parser).to.parse("0123456xx", [7, 9]); - }); - }); - }); - - 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 - ); + function withConsoleStub( block ) { - expect(parser).to.parse("a"); - }); + if ( typeof console === "object" ) sinon.stub( console, "log" ); - it("cannot access variable defined by labeled predicate element", function() { - let parser = peg.generate( - "start = 'a' b:!{ return b !== undefined; } 'c'", - options - ); + try { - expect(parser).to.failToParse("ac"); - }); + return block(); - it("cannot access variables defined by following labeled elements", function() { - let parser = peg.generate( - "start = !{ return a !== 'a'; } a:'a'", - options - ); + } finally { - 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 - ); + if ( typeof console === "object" ) console.log.restore(); - 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"); - }); + function helpers( chai, utils ) { - it("cannot access variables defined by following labeled elements", function() { - let parser = peg.generate( - "start = ('a' !{ return b !== 'b'; }) b:'b'", - options - ); + const Assertion = chai.Assertion; - 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 - ); + Assertion.addMethod( "parse", function ( input, expected, options ) { - expect(parser).to.parse("a", { a: 42 }, { a: 42 }); - }); + options = typeof options !== "undefined" ? options : {}; - it("|text| returns text matched by the expression", function() { - let parser = peg.generate( - "start = 'a' { return text(); }", - options + const result = withConsoleStub( () => + utils.flag( this, "object" ).parse( input, 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 } - } - }); - }); + if ( typeof expected !== "undefined" ) { - it("reports position correctly in the middle of input", function() { - let parser = peg.generate("start = 'a'", options); + 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" ) + ); - 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); + } ); + + Assertion.addMethod( "failToParse", function ( input, props, options ) { + + options = typeof options !== "undefined" ? options : {}; + + let passed, result; + + try { + + result = withConsoleStub( () => + utils.flag( this, "object" ).parse( input, options ) + ); + passed = true; + + } catch ( e ) { + + result = e; + passed = false; - 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 } + 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 && typeof props !== "undefined" ) { + + Object.keys( props ).forEach( key => { + + new Assertion( result ).to.have.property( key ) + .that.is.deep.equal( props[ key ] ); + + } ); + } - }); - expect(parser).to.failToParse("1\r\nx", { // Windows - location: { - start: { offset: 3, line: 2, column: 1 }, - end: { offset: 4, line: 2, column: 2 } + + } ); + + } + + // 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 ); + + } ); + } - }); - }); - }); - }); - - // 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*)" - ); - }); - }); - }); -}); + + describe( "when the expression matches", function () { + + it( "returns its match result", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = ''", options ); + + expect( parser ).to.parse( "" ); + + } ); + + it( "matches one-character literals", function () { + + const parser = peg.generate( "start = 'a'", options ); + + expect( parser ).to.parse( "a" ); + expect( parser ).to.failToParse( "b" ); + + } ); + + it( "matches multi-character literals", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = 'a'", options ); + + expect( parser ).to.parse( "a", "a" ); + + } ); + + it( "consumes the matched text", function () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = []", options ); + + expect( parser ).to.failToParse( "a" ); + + } ); + + it( "matches classes with a character list", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = [a]", options ); + + expect( parser ).to.parse( "a", "a" ); + + } ); + + it( "consumes the matched character", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = .", options ); + + expect( parser ).to.parse( "a", "a" ); + + } ); + + it( "consumes the matched character", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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. + const parser = peg.generate( "start = &{ return true; } ''", options ); + + expect( parser ).to.parse( "", [ void 0, "" ] ); + + } ); + + } ); + + describe( "when the code returns a falsey value", function () { + + it( "reports match failure", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( + "start = &{ return a === 'a'; } a:'a'", + options + ); + + expect( parser ).to.failToParse( "a" ); + + } ); + + it( "cannot access variables defined by subexpressions", function () { + + const 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 => { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 } + } ); + + } ); + + it( "|offset| returns current start offset", function () { + + const parser = peg.generate( [ + "start = [0-9]+ val:mark { return val; }", + "mark = 'xx' { return offset(); }" + ].join( "\n" ), options ); + + expect( parser ).to.parse( "0123456xx", 7 ); + + } ); + + it( "|range| returns current range", function () { + + const parser = peg.generate( [ + "start = [0-9]+ val:mark { return val; }", + "mark = 'xx' { return range(); }" + ].join( "\n" ), options ); + + expect( parser ).to.parse( "0123456xx", [ 7, 9 ] ); + + } ); + + } ); + + } ); + + 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. + const parser = peg.generate( "start = !{ return false; } ''", options ); + + expect( parser ).to.parse( "", [ void 0, "" ] ); + + } ); + + } ); + + describe( "when the code returns a truthy value", function () { + + it( "reports match failure", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( + "start = !{ return a !== 'a'; } a:'a'", + options + ); + + expect( parser ).to.failToParse( "a" ); + + } ); + + it( "cannot access variables defined by subexpressions", function () { + + const 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 => { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = 'a'?", options ); + + expect( parser ).to.parse( "a", "a" ); + + } ); + + } ); + + describe( "when the expression doesn't match", function () { + + it( "returns |null|", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = &'a' 'a'", options ); + + expect( parser ).to.parse( "a", [ void 0, "a" ] ); + + } ); + + it( "resets parse position", function () { + + const 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 () { + + const parser = peg.generate( "start = &'a'", options ); + + expect( parser ).to.failToParse( "b" ); + + } ); + + it( "discards any expectations recorded when matching the expression", function () { + + const 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 () { + + const parser = peg.generate( "start = !'a'", options ); + + expect( parser ).to.failToParse( "a" ); + + } ); + + } ); + + describe( "when the expression doesn't match", function () { + + it( "returns |undefined|", function () { + + const parser = peg.generate( "start = !'a' 'b'", options ); + + expect( parser ).to.parse( "b", [ void 0, "b" ] ); + + } ); + + it( "resets parse position", function () { + + const parser = peg.generate( "start = !'a' 'b'", options ); + + expect( parser ).to.parse( "b" ); + + } ); + + it( "discards any expectations recorded when matching the expression", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 => { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( + "start = 'a' { return text(); }", + options + ); + + expect( parser ).to.parse( "a", "a" ); + + } ); + + it( "|location| returns location info of the expression", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = 'a' { return 42; }", options ); + + expect( parser ).to.failToParse( "b" ); + + } ); + + it( "doesn't execute the code", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = 'a'", options ); + + expect( parser ).to.failToParse( "ab", { + expected: [ { type: "end" } ] + } ); + + } ); + + it( "reports expectations correctly with one alternative", function () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = 'a'", options ); + + expect( parser ).to.failToParse( "", { found: null } ); + + } ); + + it( "reports found string correctly in the middle of input", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const parser = peg.generate( "start = 'a'", options ); + + expect( parser ).to.failToParse( "b", { + message: "Expected \"a\" but \"b\" found." + } ); + + } ); + + it( "removes duplicates from expectations", function () { + + const parser = peg.generate( "start = 'a' / 'a'", options ); + + expect( parser ).to.failToParse( "b", { + message: "Expected \"a\" but \"b\" found." + } ); + + } ); + + it( "sorts expectations", function () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 + const 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 + const 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 + const 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/spec/unit/compiler/passes/generate-bytecode.spec.js b/test/spec/unit/compiler/passes/generate-bytecode.spec.js index ef55023..71f33a0 100644 --- a/test/spec/unit/compiler/passes/generate-bytecode.spec.js +++ b/test/spec/unit/compiler/passes/generate-bytecode.spec.js @@ -1,655 +1,829 @@ "use strict"; -let chai = require("chai"); -let helpers = require("./helpers"); -let pass = require("../../../../../lib/compiler/passes/generate-bytecode"); - -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, 39, 3, // IF_NOT_ERROR - 18, 2, 2, 2, 22, 2, 23, 3, // * - 15, 24, 4, // IF_NOT_ERROR - 18, 4, 2, 2, 22, 4, 23, 5, // * - 15, 9, 4, // IF_NOT_ERROR - 24, 3, // * LOAD_SAVED_POS - 26, 6, 4, 3, 2, 1, 0, // CALL <6> - 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()"]) - ); - }); - }); -}); +const chai = require( "chai" ); +const helpers = require( "./helpers" ); +const pass = require( "../../../../../lib/compiler/passes/generate-bytecode" ); + +chai.use( helpers ); + +const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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, 39, 3, // IF_NOT_ERROR + 18, 2, 2, 2, 22, 2, 23, 3, // * + 15, 24, 4, // IF_NOT_ERROR + 18, 4, 2, 2, 22, 4, 23, 5, // * + 15, 9, 4, // IF_NOT_ERROR + 24, 3, // * LOAD_SAVED_POS + 26, 6, 4, 3, 2, 1, 0, // CALL <6> + 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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 () { + + const 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/spec/unit/compiler/passes/helpers.js b/test/spec/unit/compiler/passes/helpers.js index 0d16573..86f37e1 100644 --- a/test/spec/unit/compiler/passes/helpers.js +++ b/test/spec/unit/compiler/passes/helpers.js @@ -1,90 +1,112 @@ "use strict"; -let parser = require("../../../../../lib/parser"); +const parser = require( "../../../../../lib/parser" ); -module.exports = function(chai, utils) { - let Assertion = chai.Assertion; +module.exports = function ( chai, utils ) { - Assertion.addMethod("changeAST", function(grammar, props, options) { - options = options !== undefined ? options : {}; + const Assertion = chai.Assertion; - function matchProps(value, props) { - function isArray(value) { - return Object.prototype.toString.apply(value) === "[object Array]"; - } + Assertion.addMethod( "changeAST", function ( grammar, props, options ) { - function isObject(value) { - return value !== null && typeof value === "object"; - } + options = typeof options !== "undefined" ? options : {}; - if (isArray(props)) { - if (!isArray(value)) { return false; } + function matchProps( value, props ) { + + function isObject( value ) { + + return value !== null && typeof value === "object"; + + } + + if ( Array.isArray( props ) ) { + + if ( ! Array.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; + + const keys = Object.keys( props ); + for ( let i = 0; i < keys.length; i++ ) { + + const key = keys[ i ]; + + if ( ! ( key in value ) ) return false; + if ( ! matchProps( value[ key ], props[ key ] ) ) return false; + + } + + return true; + + } + + return value === props; - 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; } + const ast = parser.parse( grammar ); - let keys = Object.keys(props); - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; + utils.flag( this, "object" )( ast, options ); - if (!(key in value)) { return false; } + 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, options ) { + + options = typeof options !== "undefined" ? options : {}; + + const ast = parser.parse( grammar ); + + let passed, result; + + try { + + utils.flag( this, "object" )( ast, options ); + passed = true; + + } catch ( e ) { + + result = e; + passed = 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, options) { - options = options !== undefined ? options : {}; - - let ast = parser.parse(grammar); - - let passed, result; - - try { - utils.flag(this, "object")(ast, options); - 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]); - }); - } - }); + 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 && typeof props !== "undefined" ) { + + Object.keys( props ).forEach( key => { + + new Assertion( result ) + .to.have.property( key ) + .that.is.deep.equal( props[ key ] ); + + } ); + + } + + } ); + }; diff --git a/test/spec/unit/compiler/passes/remove-proxy-rules.spec.js b/test/spec/unit/compiler/passes/remove-proxy-rules.spec.js index 46d6108..3579a7f 100644 --- a/test/spec/unit/compiler/passes/remove-proxy-rules.spec.js +++ b/test/spec/unit/compiler/passes/remove-proxy-rules.spec.js @@ -1,59 +1,69 @@ "use strict"; -let chai = require("chai"); -let helpers = require("./helpers"); -let pass = require("../../../../../lib/compiler/passes/remove-proxy-rules"); - -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 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"] } - ); - }); - }); -}); +const chai = require( "chai" ); +const helpers = require( "./helpers" ); +const pass = require( "../../../../../lib/compiler/passes/remove-proxy-rules" ); + +chai.use( helpers ); + +const 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 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/spec/unit/compiler/passes/report-duplicate-labels.spec.js b/test/spec/unit/compiler/passes/report-duplicate-labels.spec.js index a69f914..a96d9bd 100644 --- a/test/spec/unit/compiler/passes/report-duplicate-labels.spec.js +++ b/test/spec/unit/compiler/passes/report-duplicate-labels.spec.js @@ -1,63 +1,83 @@ "use strict"; -let chai = require("chai"); -let helpers = require("./helpers"); -let pass = require("../../../../../lib/compiler/passes/report-duplicate-labels"); - -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'"); - }); - }); -}); +const chai = require( "chai" ); +const helpers = require( "./helpers" ); +const pass = require( "../../../../../lib/compiler/passes/report-duplicate-labels" ); + +chai.use( helpers ); + +const 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'" ); + + } ); + + } ); + +} ); diff --git a/test/spec/unit/compiler/passes/report-duplicate-rules.spec.js b/test/spec/unit/compiler/passes/report-duplicate-rules.spec.js index cf619ac..d73a7cf 100644 --- a/test/spec/unit/compiler/passes/report-duplicate-rules.spec.js +++ b/test/spec/unit/compiler/passes/report-duplicate-rules.spec.js @@ -1,24 +1,28 @@ "use strict"; -let chai = require("chai"); -let helpers = require("./helpers"); -let pass = require("../../../../../lib/compiler/passes/report-duplicate-rules"); - -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 } - } - }); - }); -}); +const chai = require( "chai" ); +const helpers = require( "./helpers" ); +const pass = require( "../../../../../lib/compiler/passes/report-duplicate-rules" ); + +chai.use( helpers ); + +const 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 } + } + } ); + + } ); + +} ); diff --git a/test/spec/unit/compiler/passes/report-infinite-recursion.spec.js b/test/spec/unit/compiler/passes/report-infinite-recursion.spec.js index 3d668c7..f11b939 100644 --- a/test/spec/unit/compiler/passes/report-infinite-recursion.spec.js +++ b/test/spec/unit/compiler/passes/report-infinite-recursion.spec.js @@ -1,119 +1,135 @@ "use strict"; -let chai = require("chai"); -let helpers = require("./helpers"); -let pass = require("../../../../../lib/compiler/passes/report-infinite-recursion"); - -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"); - }); - }); -}); +const chai = require( "chai" ); +const helpers = require( "./helpers" ); +const pass = require( "../../../../../lib/compiler/passes/report-infinite-recursion" ); + +chai.use( helpers ); + +const 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" ); + + } ); + + } ); + +} ); diff --git a/test/spec/unit/compiler/passes/report-infinite-repetition.spec.js b/test/spec/unit/compiler/passes/report-infinite-repetition.spec.js index ede874a..c7119f8 100644 --- a/test/spec/unit/compiler/passes/report-infinite-repetition.spec.js +++ b/test/spec/unit/compiler/passes/report-infinite-repetition.spec.js @@ -1,99 +1,107 @@ "use strict"; -let chai = require("chai"); -let helpers = require("./helpers"); -let pass = require("../../../../../lib/compiler/passes/report-infinite-repetition"); - -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 = .*"); - }); -}); +const chai = require( "chai" ); +const helpers = require( "./helpers" ); +const pass = require( "../../../../../lib/compiler/passes/report-infinite-repetition" ); + +chai.use( helpers ); + +const 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 = .*" ); + + } ); + +} ); diff --git a/test/spec/unit/compiler/passes/report-undefined-rules.spec.js b/test/spec/unit/compiler/passes/report-undefined-rules.spec.js index 082c7b7..3a0b18e 100644 --- a/test/spec/unit/compiler/passes/report-undefined-rules.spec.js +++ b/test/spec/unit/compiler/passes/report-undefined-rules.spec.js @@ -1,29 +1,35 @@ "use strict"; -let chai = require("chai"); -let helpers = require("./helpers"); -let pass = require("../../../../../lib/compiler/passes/report-undefined-rules"); - -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("checks allowedStartRules", function() { - expect(pass).to.reportError("start = 'a'", { - message: "Start rule \"missing\" is not defined." - }, { - allowedStartRules: ["missing"] - }); - }); -}); +const chai = require( "chai" ); +const helpers = require( "./helpers" ); +const pass = require( "../../../../../lib/compiler/passes/report-undefined-rules" ); + +chai.use( helpers ); + +const 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( "checks allowedStartRules", function () { + + expect( pass ).to.reportError( "start = 'a'", { + message: "Start rule \"missing\" is not defined." + }, { + allowedStartRules: [ "missing" ] + } ); + + } ); + +} ); diff --git a/test/spec/unit/parser.spec.js b/test/spec/unit/parser.spec.js index f4a749c..6a5c72f 100644 --- a/test/spec/unit/parser.spec.js +++ b/test/spec/unit/parser.spec.js @@ -1,689 +1,842 @@ "use strict"; -let chai = require("chai"); -let parser = require("../../../lib/parser"); +const chai = require( "chai" ); +const parser = require( "../../../lib/parser" ); -let expect = chai.expect; +const expect = chai.expect; // better diagnostics for deep eq failure chai.config.truncateThreshold = 0; -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 }] +describe( "PEG.js grammar parser", function () { + + const literalAbcd = { type: "literal", value: "abcd", ignoreCase: false }; + const literalEfgh = { type: "literal", value: "efgh", ignoreCase: false }; + const literalIjkl = { type: "literal", value: "ijkl", ignoreCase: false }; + const literalMnop = { type: "literal", value: "mnop", ignoreCase: false }; + const semanticAnd = { type: "semantic_and", code: " code " }; + const semanticNot = { type: "semantic_not", code: " code " }; + const optional = { type: "optional", expression: literalAbcd }; + const zeroOrMore = { type: "zero_or_more", expression: literalAbcd }; + const oneOrMore = { type: "one_or_more", expression: literalAbcd }; + const textOptional = { type: "text", expression: optional }; + const simpleNotAbcd = { type: "simple_not", expression: literalAbcd }; + const simpleAndOptional = { type: "simple_and", expression: optional }; + const simpleNotOptional = { type: "simple_not", expression: optional }; + const labeledAbcd = { type: "labeled", label: "a", expression: literalAbcd }; + const labeledEfgh = { type: "labeled", label: "b", expression: literalEfgh }; + const labeledIjkl = { type: "labeled", label: "c", expression: literalIjkl }; + const labeledMnop = { type: "labeled", label: "d", expression: literalMnop }; + const labeledSimpleNot = { type: "labeled", label: "a", expression: simpleNotAbcd }; + const sequence = { + type: "sequence", + elements: [ literalAbcd, literalEfgh, literalIjkl ] }; - } - - 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); - }; + const sequence2 = { + type: "sequence", + elements: [ labeledAbcd, labeledEfgh ] + }; + const sequence4 = { + type: "sequence", + elements: [ labeledAbcd, labeledEfgh, labeledIjkl, labeledMnop ] + }; + const groupLabeled = { type: "group", expression: labeledAbcd }; + const groupSequence = { type: "group", expression: sequence }; + const actionAbcd = { type: "action", expression: literalAbcd, code: " code " }; + const actionEfgh = { type: "action", expression: literalEfgh, code: " code " }; + const actionIjkl = { type: "action", expression: literalIjkl, code: " code " }; + const actionMnop = { type: "action", expression: literalMnop, code: " code " }; + const actionSequence = { type: "action", expression: sequence, code: " code " }; + const choice = { + type: "choice", + alternatives: [ literalAbcd, literalEfgh, literalIjkl ] + }; + const choice2 = { + type: "choice", + alternatives: [ actionAbcd, actionEfgh ] + }; + const choice4 = { + type: "choice", + alternatives: [ actionAbcd, actionEfgh, actionIjkl, actionMnop ] + }; + const named = { type: "named", name: "start rule", expression: literalAbcd }; + const ruleA = { type: "rule", name: "a", expression: literalAbcd }; + const ruleB = { type: "rule", name: "b", expression: literalEfgh }; + const ruleC = { type: "rule", name: "c", expression: literalIjkl }; + const ruleStart = { type: "rule", name: "start", expression: literalAbcd }; + const initializer = { type: "initializer", code: " code " }; + + function oneRuleGrammar( expression ) { + + return { + type: "grammar", + initializer: null, + rules: [ { type: "rule", name: "start", expression: expression } ] + }; + } - function stripLeaf(node) { - delete node.location; + function actionGrammar( code ) { + + return oneRuleGrammar( + { type: "action", expression: literalAbcd, code: code } + ); + } - function stripExpression(node) { - delete node.location; + function literalGrammar( value, ignoreCase ) { + + return oneRuleGrammar( + { type: "literal", value: value, ignoreCase: ignoreCase } + ); - strip(node.expression); } - function stripChildren(property) { - return function(node) { - delete node.location; + function classGrammar( parts, inverted, ignoreCase ) { + + return oneRuleGrammar( { + type: "class", + parts: parts, + inverted: inverted, + ignoreCase: ignoreCase + } ); - node[property].forEach(strip); - }; } - let strip = buildVisitor({ - grammar(node) { - delete node.location; + function anyGrammar() { - 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); - }); - - // Grammars without any rules are not accepted. - it("parses Rule+", function() { - expect("start = a").to.parseAs(ruleRefGrammar("a")); - let grammar = ruleRefGrammar("a"); - grammar.initializer = { - "type": "initializer", - "code": "" + return oneRuleGrammar( { type: "any" } ); + + } + + function ruleRefGrammar( name ) { + + return oneRuleGrammar( { type: "rule_ref", name: name } ); + + } + + const trivialGrammar = literalGrammar( "abcd", false ); + const twoRuleGrammar = { + type: "grammar", + initializer: null, + rules: [ ruleA, ruleB ] }; - expect("{}\nstart = a").to.parseAs(grammar); - - expect("").to.failToParse(); - expect("{}").to.failToParse(); - }); - - // 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)); + + const stripLocation = ( function () { + + let strip; + + 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 ); + + }; + + } + + 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 ) { + + const Assertion = chai.Assertion; + + Assertion.addMethod( "parseAs", function ( expected ) { + + const 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 && typeof 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 ); + + } ); + + // Grammars without any rules are not accepted. + it( "parses Rule+", function () { + + expect( "start = a" ).to.parseAs( ruleRefGrammar( "a" ) ); + const grammar = ruleRefGrammar( "a" ); + grammar.initializer = { + "type": "initializer", + "code": "" + }; + expect( "{}\nstart = a" ).to.parseAs( grammar ); + + expect( "" ).to.failToParse(); + expect( "{}" ).to.failToParse(); + + } ); + + // 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); - }); - - it("reports unmatched brace", function() { - const text = "rule = \n 'x' { y \n z"; - const errorLocation = { - start: { offset: 13, line: 2, column: 6 }, - end: { offset: 14, line: 2, column: 7 } - }; - expect(() => parser.parse(text)) - .to.throw("Unbalanced brace.") - .with.property("location") - .that.deep.equals(errorLocation); - }); -}); + + } ); + + // 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 ); + + } ); + + it( "reports unmatched brace", function () { + + const text = "rule = \n 'x' { y \n z"; + const errorLocation = { + start: { offset: 13, line: 2, column: 6 }, + end: { offset: 14, line: 2, column: 7 } + }; + expect( () => parser.parse( text ) ) + .to.throw( "Unbalanced brace." ) + .with.property( "location" ) + .that.deep.equals( errorLocation ); + + } ); + +} );