Update code format and style

This is related to my last commit. I've updated all the JavaScript files to satisfy 'eslint-config-futagozaryuu', my eslint configuration.

I'm sure I've probally missed something, but I've run all NPM scripts and Gulp tasks, fixed any bugs that cropped up, and updated some stuff (mainly related to generated messages), so as far as I can, tell this conversion is over (I know I've probally jixed it just by saying this ;P).
master
Futago-za Ryuu 7 years ago
parent 3c6523ff83
commit e6d018a88d

@ -3,6 +3,11 @@
module.exports = {
"extends": "futagozaryuu/node-v4",
"root": true
"root": true,
"rules": {
"prefer-rest-params": 0,
},
};

@ -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

@ -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();
}
} );

@ -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 )
);

@ -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
};

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

File diff suppressed because it is too large Load Diff

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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",

@ -5,8 +5,10 @@ module.exports = {
"extends": "futagozaryuu/test",
"rules": {
"node/shebang": 0
"node/shebang": 0,
"func-names": 0,
"no-mixed-operators": 0,
}
},
};

@ -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;

@ -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(
"<tr class='" + klass + "'>"
+ "<td class='title'>"
+ (url !== null ? "<a href='" + url + "'>" : "")
+ title
+ (url !== null ? "</a>" : "")
+ "</td>"
+ "<td class='input-size'>"
+ "<span class='value'>"
+ (inputSize / KB).toFixed(2)
+ "</span>"
+ "&nbsp;<span class='unit'>kB</span>"
+ "</td>"
+ "<td class='parse-time'>"
+ "<span class='value'>"
+ parseTime.toFixed(2)
+ "</span>"
+ "&nbsp;<span class='unit'>ms</span>"
+ "</td>"
+ "<td class='parse-speed'>"
+ "<span class='value'>"
+ ((inputSize / KB) / (parseTime / MS_IN_S)).toFixed(2)
+ "</span>"
+ "&nbsp;<span class='unit'>kB/s</span>"
+ "</td>"
+ "</tr>"
);
}
// 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(
"<tr class='heading'><th colspan='4'>"
+ "<a href='../../examples/" + benchmark.id + ".pegjs'>"
+ benchmark.title
+ "</a>"
+ "</th></tr>"
);
},
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(
"<tr class='" + klass + "'>"
+ "<td class='title'>"
+ ( url !== null ? "<a href='" + url + "'>" : "" )
+ title
+ ( url !== null ? "</a>" : "" )
+ "</td>"
+ "<td class='input-size'>"
+ "<span class='value'>"
+ ( inputSize / KB ).toFixed( 2 )
+ "</span>"
+ "&nbsp;<span class='unit'>kB</span>"
+ "</td>"
+ "<td class='parse-time'>"
+ "<span class='value'>"
+ parseTime.toFixed( 2 )
+ "</span>"
+ "&nbsp;<span class='unit'>ms</span>"
+ "</td>"
+ "<td class='parse-speed'>"
+ "<span class='value'>"
+ ( ( inputSize / KB ) / ( parseTime / MS_IN_S ) ).toFixed( 2 )
+ "</span>"
+ "&nbsp;<span class='unit'>kB/s</span>"
+ "</td>"
+ "</tr>"
);
}
// 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(
"<tr class='heading'><th colspan='4'>"
+ "<a href='../../examples/" + benchmark.id + ".pegjs'>"
+ benchmark.title
+ "</a>"
+ "</th></tr>"
);
},
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() );

@ -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 <n> number of runs (default: 10)");
console.log(" --cache make tested parsers cache results");
console.log(" -o, --optimize <goal> 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 <n> number of runs (default: 10)" );
console.log( " --cache make tested parsers cache results" );
console.log( " -o, --optimize <goal> 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();
}
} );

@ -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;

@ -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 <commit>");
print(" test/impact <commit_before> <commit_after>");
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 <commit>
test/impact <commit_before> <commit_after>
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()}.
`);
` );

@ -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/" );
} );

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

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

@ -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" );
} );
} );
} );

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -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 ] );
} );
}
} );
};

@ -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" ] }
);
} );
} );
} );

@ -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'" );
} );
} );
} );

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

@ -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" );
} );
} );
} );

@ -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 = .*" );
} );
} );

@ -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" ]
} );
} );
} );

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save