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 ); + + } ); + +} );