You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

333 lines
8.7 KiB
Plaintext

#!/usr/bin/env node
14 years ago
"use strict";
let fs = require("fs");
let path = require("path");
let peg = require("../lib/peg");
13 years ago
/* Helpers */
function printVersion() {
console.log("PEG.js " + peg.VERSION);
}
function printHelp() {
console.log("Usage: pegjs [options] [--] [<input_file>]");
console.log("");
console.log("Options:");
console.log(" --allowed-start-rules <rules> comma-separated list of rules the generated");
console.log(" parser will be allowed to start parsing");
console.log(" from (default: the first rule in the");
console.log(" grammar)");
console.log(" --cache make generated parser cache results");
console.log(" -d, --dependency <dependency> use specified dependency (can be specified");
console.log(" multiple times)");
console.log(" -e, --export-var <variable> name of a global variable into which the");
console.log(" parser object is assigned to when no module");
console.log(" loader is detected");
console.log(" --extra-options <options> additional options (in JSON format) to pass");
console.log(" to peg.generate");
console.log(" --extra-options-file <file> file with additional options (in JSON");
console.log(" format) to pass to peg.generate");
console.log(" --format <format> format of the generated parser: amd,");
console.log(" commonjs, globals, umd (default: commonjs)");
console.log(" -h, --help print help and exit");
console.log(" -O, --optimize <goal> select optimization for speed or size");
console.log(" (default: speed)");
console.log(" -o, --output <file> output file");
console.log(" --plugin <plugin> use a specified plugin (can be specified");
console.log(" multiple times)");
console.log(" --trace enable tracing in generated parser");
console.log(" -v, --version print version information and exit");
}
function exitSuccess() {
process.exit(0);
}
function exitFailure() {
process.exit(1);
}
function abort(message) {
console.error(message);
exitFailure();
}
function addExtraOptions(options, 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.");
}
for (let key in extraOptions) {
if (extraOptions.hasOwnProperty(key)) {
options[key] = extraOptions[key];
}
}
}
/*
* Extracted into a function just to silence JSHint complaining about creating
* functions in a loop.
*/
function trim(s) {
return s.trim();
}
13 years ago
/* Arguments */
let args = process.argv.slice(2); // Trim "node" and the script path.
function isOption(arg) {
return (/^-.+/).test(arg);
}
function nextArg() {
args.shift();
}
13 years ago
/* Files */
function readStream(inputStream, callback) {
let input = "";
inputStream.on("data", (data) => { input += data; });
inputStream.on("end", () => { callback(input); });
}
13 years ago
/* Main */
let inputFile = null;
let outputFile = null;
let options = {
cache: false,
dependencies: {},
exportVar: null,
format: "commonjs",
optimize: "speed",
output: "source",
plugins: [],
trace: false
};
while (args.length > 0 && isOption(args[0])) {
let json, id, mod;
switch (args[0]) {
case "--allowed-start-rules":
nextArg();
if (args.length === 0) {
abort("Missing parameter of the -e/--allowed-start-rules option.");
}
options.allowedStartRules = args[0]
.split(",")
.map(trim);
break;
case "--cache":
options.cache = true;
break;
case "-d":
case "--dependency":
nextArg();
if (args.length === 0) {
abort("Missing parameter of the -d/--dependency option.");
}
if (args[0].indexOf(":") !== -1) {
let parts = args[0].split(":");
options.dependencies[parts[0]] = parts[1];
} else {
options.dependencies[args[0]] = args[0];
}
break;
case "-e":
case "--export-var":
nextArg();
if (args.length === 0) {
abort("Missing parameter of the -e/--export-var option.");
}
options.exportVar = args[0];
break;
case "--extra-options":
nextArg();
if (args.length === 0) {
abort("Missing parameter of the --extra-options option.");
}
addExtraOptions(options, args[0]);
break;
case "--extra-options-file":
nextArg();
if (args.length === 0) {
abort("Missing parameter of the --extra-options-file option.");
}
try {
json = fs.readFileSync(args[0]);
} catch(e) {
abort("Can't read from file \"" + args[0] + "\".");
}
addExtraOptions(options, json);
break;
case "--format":
nextArg();
if (args.length === 0) {
abort("Missing parameter of the --format option.");
}
if (args[0] !== "amd" && args[0] !== "commonjs" && args[0] !== "globals" && args[0] !== "umd") {
abort("Module format must be one of \"amd\", \"commonjs\", \"globals\", and \"umd\".");
}
options.format = args[0];
break;
case "-h":
case "--help":
printHelp();
exitSuccess();
Implement basic support for tracing Parsers can now be generated with support for tracing using the --trace CLI option or a boolean |trace| option to |PEG.buildParser|. This makes them trace their progress, which can be useful for debugging. Parsers generated with tracing support are called "tracing parsers". When a tracing parser executes, by default it traces the rules it enters and exits by writing messages to the console. For example, a parser built from this grammar: start = a / b a = "a" b = "b" will write this to the console when parsing input "b": 1:1 rule.enter start 1:1 rule.enter a 1:1 rule.fail a 1:1 rule.enter b 1:2 rule.match b 1:2 rule.match start You can customize tracing by passing a custom *tracer* to parser's |parse| method using the |tracer| option: parser.parse(input, { trace: tracer }); This will replace the built-in default tracer (which writes to the console) by the tracer you supplied. The tracer must be an object with a |trace| method. This method is called each time a tracing event happens. It takes one argument which is an object describing the tracing event. Currently, three events are supported: * rule.enter -- triggered when a rule is entered * rule.match -- triggered when a rule matches successfully * rule.fail -- triggered when a rule fails to match These events are triggered in nested pairs -- for each rule.enter event there is a matching rule.match or rule.fail event. The event object passed as an argument to |trace| contains these properties: * type -- event type * rule -- name of the rule the event is related to * offset -- parse position at the time of the event * line -- line at the time of the event * column -- column at the time of the event * result -- rule's match result (only for rule.match event) The whole tracing API is somewhat experimental (which is why it isn't documented properly yet) and I expect it will evolve over time as experience is gained. The default tracer is also somewhat bare-bones. I hope that PEG.js user community will develop more sophisticated tracers over time and I'll be able to integrate their best ideas into the default tracer.
9 years ago
break;
case "-O":
Code generator rewrite This is a complete rewrite of the PEG.js code generator. Its goals are: 1. Allow optimizing the generated parser code for code size as well as for parsing speed. 2. Prepare ground for future optimizations and big features (like incremental parsing). 2. Replace the old template-based code-generation system with something more lightweight and flexible. 4. General code cleanup (structure, style, variable names, ...). New Architecture ---------------- The new code generator consists of two steps: * Bytecode generator -- produces bytecode for an abstract virtual machine * JavaScript generator -- produces JavaScript code based on the bytecode The abstract virtual machine is stack-based. Originally I wanted to make it register-based, but it turned out that all the code related to it would be more complex and the bytecode itself would be longer (because of explicit register specifications in instructions). The only downsides of the stack-based approach seem to be few small inefficiencies (see e.g. the |NIP| instruction), which seem to be insignificant. The new generator allows optimizing for parsing speed or code size (you can choose using the |optimize| option of the |PEG.buildParser| method or the --optimize/-o option on the command-line). When optimizing for size, the JavaScript generator emits the bytecode together with its constant table and a generic bytecode interpreter. Because the interpreter is small and the bytecode and constant table grow only slowly with size of the grammar, the resulting parser is also small. When optimizing for speed, the JavaScript generator just compiles the bytecode into JavaScript. The generated code is relatively efficient, so the resulting parser is fast. Internal Identifiers -------------------- As a small bonus, all internal identifiers visible to user code in the initializer, actions and predicates are prefixed by |peg$|. This lowers the chance that identifiers in user code will conflict with the ones from PEG.js. It also makes using any internals in user code ugly, which is a good thing. This solves GH-92. Performance ----------- The new code generator improved parsing speed and parser code size significantly. The generated parsers are now: * 39% faster when optimizing for speed * 69% smaller when optimizing for size (without minification) * 31% smaller when optimizing for size (with minification) (Parsing speed was measured using the |benchmark/run| script. Code size was measured by generating parsers for examples in the |examples| directory and adding up the file sizes. Minification was done by |uglify --ascii| in version 1.3.4.) Final Note ---------- This is just a beginning! The new code generator lays a foundation upon which many optimizations and improvements can (and will) be made. Stay tuned :-)
11 years ago
case "--optimize":
nextArg();
if (args.length === 0) {
abort("Missing parameter of the -O/--optimize option.");
Code generator rewrite This is a complete rewrite of the PEG.js code generator. Its goals are: 1. Allow optimizing the generated parser code for code size as well as for parsing speed. 2. Prepare ground for future optimizations and big features (like incremental parsing). 2. Replace the old template-based code-generation system with something more lightweight and flexible. 4. General code cleanup (structure, style, variable names, ...). New Architecture ---------------- The new code generator consists of two steps: * Bytecode generator -- produces bytecode for an abstract virtual machine * JavaScript generator -- produces JavaScript code based on the bytecode The abstract virtual machine is stack-based. Originally I wanted to make it register-based, but it turned out that all the code related to it would be more complex and the bytecode itself would be longer (because of explicit register specifications in instructions). The only downsides of the stack-based approach seem to be few small inefficiencies (see e.g. the |NIP| instruction), which seem to be insignificant. The new generator allows optimizing for parsing speed or code size (you can choose using the |optimize| option of the |PEG.buildParser| method or the --optimize/-o option on the command-line). When optimizing for size, the JavaScript generator emits the bytecode together with its constant table and a generic bytecode interpreter. Because the interpreter is small and the bytecode and constant table grow only slowly with size of the grammar, the resulting parser is also small. When optimizing for speed, the JavaScript generator just compiles the bytecode into JavaScript. The generated code is relatively efficient, so the resulting parser is fast. Internal Identifiers -------------------- As a small bonus, all internal identifiers visible to user code in the initializer, actions and predicates are prefixed by |peg$|. This lowers the chance that identifiers in user code will conflict with the ones from PEG.js. It also makes using any internals in user code ugly, which is a good thing. This solves GH-92. Performance ----------- The new code generator improved parsing speed and parser code size significantly. The generated parsers are now: * 39% faster when optimizing for speed * 69% smaller when optimizing for size (without minification) * 31% smaller when optimizing for size (with minification) (Parsing speed was measured using the |benchmark/run| script. Code size was measured by generating parsers for examples in the |examples| directory and adding up the file sizes. Minification was done by |uglify --ascii| in version 1.3.4.) Final Note ---------- This is just a beginning! The new code generator lays a foundation upon which many optimizations and improvements can (and will) be made. Stay tuned :-)
11 years ago
}
if (args[0] !== "speed" && args[0] !== "size") {
abort("Optimization goal must be either \"speed\" or \"size\".");
}
options.optimize = args[0];
break;
case "-o":
case "--output":
nextArg();
if (args.length === 0) {
abort("Missing parameter of the -o/--output option.");
}
outputFile = args[0];
break;
case "--plugin":
nextArg();
if (args.length === 0) {
abort("Missing parameter of the --plugin option.");
}
id = /^(\.\/|\.\.\/)/.test(args[0]) ? path.resolve(args[0]) : args[0];
mod;
try {
mod = require(id);
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") { throw e; }
abort("Can't load module \"" + id + "\".");
}
options.plugins.push(mod);
break;
case "--trace":
options.trace = true;
break;
case "-v":
case "--version":
printVersion();
exitSuccess();
break;
case "--":
nextArg();
break;
default:
abort("Unknown option: " + args[0] + ".");
}
nextArg();
}
if (Object.keys(options.dependencies).length > 0) {
if (options.format !== "amd" && options.format !== "commonjs" && options.format !== "umd") {
abort("Can't use the -d/--dependency option with the \"" + options.format + "\" module format.");
}
}
if (options.exportVar !== null) {
if (options.format !== "globals" && options.format !== "umd") {
abort("Can't use the -e/--export-var option with the \"" + options.format + "\" module format.");
}
}
let inputStream;
let outputStream;
Fix ESLint errors in bin/pegjs Fix the following errors: 12:3 error Unexpected console statement no-console 16:3 error Unexpected console statement no-console 17:3 error Unexpected console statement no-console 18:3 error Unexpected console statement no-console 19:3 error Unexpected console statement no-console 20:3 error Unexpected console statement no-console 21:3 error Unexpected console statement no-console 22:3 error Unexpected console statement no-console 23:3 error Unexpected console statement no-console 24:3 error Unexpected console statement no-console 25:3 error Unexpected console statement no-console 26:3 error Unexpected console statement no-console 27:3 error Unexpected console statement no-console 28:3 error Unexpected console statement no-console 29:3 error Unexpected console statement no-console 30:3 error Unexpected console statement no-console 31:3 error Unexpected console statement no-console 32:3 error Unexpected console statement no-console 33:3 error Unexpected console statement no-console 34:3 error Unexpected console statement no-console 35:3 error Unexpected console statement no-console 36:3 error Unexpected console statement no-console 37:3 error Unexpected console statement no-console 38:3 error Unexpected console statement no-console 39:3 error Unexpected console statement no-console 40:3 error Unexpected console statement no-console 41:3 error Unexpected console statement no-console 42:3 error Unexpected console statement no-console 43:3 error Unexpected console statement no-console 44:3 error Unexpected console statement no-console 56:3 error Unexpected console statement no-console 232:9 error "inputStream" is already defined no-redeclare 240:9 error "outputStream" is already defined no-redeclare
8 years ago
switch (args.length) {
case 0:
inputFile = "-";
break;
case 1:
inputFile = args[0];
break;
default:
abort("Too many arguments.");
}
if (outputFile === null) {
if (inputFile === "-") {
outputFile = "-";
} else {
outputFile = inputFile.substr(0, inputFile.length - path.extname(inputFile).length) + ".js";
}
}
if (inputFile === "-") {
process.stdin.resume();
inputStream = process.stdin;
inputStream.on("error", () => {
abort("Can't read from file \"" + inputFile + "\".");
});
} else {
inputStream = fs.createReadStream(inputFile);
}
if (outputFile === "-") {
outputStream = process.stdout;
} else {
outputStream = fs.createWriteStream(outputFile);
outputStream.on("error", () => {
abort("Can't write to file \"" + outputFile + "\".");
});
}
readStream(inputStream, (input) => {
let source;
try {
source = peg.generate(input, options);
} catch (e) {
if (e.location !== undefined) {
abort(e.location.start.line + ":" + e.location.start.column + ": " + e.message);
} else {
abort(e.message);
}
}
outputStream.write(source);
if (outputStream !== process.stdout) {
outputStream.end();
}
});