Skip to content

Commit

Permalink
Code generator rewrite
Browse files Browse the repository at this point in the history
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 :-)
  • Loading branch information
dmajda committed Jan 1, 2013
1 parent bea6b1f commit fe1ca48
Show file tree
Hide file tree
Showing 21 changed files with 4,973 additions and 4,047 deletions.
5 changes: 3 additions & 2 deletions Makefile
Expand Up @@ -8,8 +8,9 @@ PEGJS_VERSION = `cat $(VERSION_FILE)`
MODULES = utils \
grammar-error \
parser \
compiler/passes/allocate-registers \
compiler/passes/generate-code \
compiler/opcodes \
compiler/passes/generate-bytecode \
compiler/passes/generate-javascript \
compiler/passes/remove-proxy-rules \
compiler/passes/report-left-recursion \
compiler/passes/report-missing-rules \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -108,6 +108,8 @@ object to `PEG.buildParser`. The following options are supported:
* `output` — if set to `"parser"`, the method will return generated parser
object; if set to `"source"`, it will return parser source code as a string
(default: `"parser"`)
* `optimize`— selects between optimizing the generated parser for parsing
speed (`"speed"`) or code size (`"size"`) (default: `"speed"`)

Using the Parser
----------------
Expand Down
8 changes: 4 additions & 4 deletions benchmark/index.css
Expand Up @@ -24,11 +24,11 @@ table tr.total td.parse-speed .value { font-size: 175%; }
a, a:visited { color: #3d586c; }

#options {
width: 46em;
margin: 2em auto; border-radius: .5em; -moz-border-radius: .5em; padding: .5em 1em;
text-align: center;
width: 45em;
margin: 2em auto; border-radius: .5em; -moz-border-radius: .5em; padding: .5em 1.5em;
background-color: #f0f0f0;
}
#options #run-count { width: 3em; }
#options #cache { margin-left: 2em; }
#options #run { width: 5em; margin-left: 2em; }
#options label[for=optimize] { margin-left: 2em; }
#options #run { float:right; width: 5em; }
5 changes: 5 additions & 0 deletions benchmark/index.html
Expand Up @@ -13,6 +13,11 @@ <h1>PEG.js Benchmark Suite</h1>
<input type="text" id="run-count" value="10"> times
<input type="checkbox" id="cache">
<label for="cache">Use results cache</label>
<label for="optimize">Optimize:</label>
<select id="optimize">
<option value="speed">Speed</option>
<option value="size">Size</option>
</select>
<input type="button" id="run" value="Run">
</div>

Expand Down
3 changes: 2 additions & 1 deletion benchmark/index.js
Expand Up @@ -63,7 +63,8 @@ $("#run").click(function() {
}

var options = {
cache: $("#cache").is(":checked"),
cache: $("#cache").is(":checked"),
optimize: $("#optimize").val()
};

Runner.run(benchmarks, runCount, options, {
Expand Down
19 changes: 18 additions & 1 deletion benchmark/run
Expand Up @@ -81,6 +81,8 @@ function printHelp() {
util.puts("Options:");
util.puts(" -n, --run-count <n> number of runs (default: 10)");
util.puts(" --cache make tested parsers cache results");
util.puts(" -o, --optimize <goal> select optimization for speed or size (default:");
util.puts(" speed)");
}

function exitSuccess() {
Expand Down Expand Up @@ -111,7 +113,10 @@ function nextArg() {
/* Main */

var runCount = 10;
var options = { };
var options = {
cache: false,
optimize: "speed"
};

while (args.length > 0 && isOption(args[0])) {
switch (args[0]) {
Expand All @@ -131,6 +136,18 @@ while (args.length > 0 && isOption(args[0])) {
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();
Expand Down
19 changes: 17 additions & 2 deletions bin/pegjs
Expand Up @@ -29,6 +29,8 @@ function printHelp() {
util.puts(" parser will be allowed to start parsing");
util.puts(" from (default: the first rule in the");
util.puts(" grammar)");
util.puts(" -o, --optimize <goal> select optimization for speed or size (default:");
util.puts(" speed)");
util.puts(" -v, --version print version information and exit");
util.puts(" -h, --help print help and exit");
}
Expand Down Expand Up @@ -71,8 +73,9 @@ function readStream(inputStream, callback) {
/* This makes the generated parser a CommonJS module by default. */
var exportVar = "module.exports";
var options = {
cache: false,
output: "source"
cache: false,
output: "source",
optimize: "speed"
};

while (args.length > 0 && isOption(args[0])) {
Expand Down Expand Up @@ -100,6 +103,18 @@ while (args.length > 0 && isOption(args[0])) {
.map(function(s) { return s.trim() });
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 "-v":
case "--version":
printVersion();
Expand Down
4 changes: 2 additions & 2 deletions lib/compiler.js
Expand Up @@ -11,8 +11,8 @@ module.exports = {
"reportMissingRules",
"reportLeftRecursion",
"removeProxyRules",
"allocateRegisters",
"generateCode"
"generateBytecode",
"generateJavascript"
],

/*
Expand Down
46 changes: 46 additions & 0 deletions lib/compiler/opcodes.js
@@ -0,0 +1,46 @@
/* Bytecode instruction opcodes. */
module.exports = {
/* Stack Manipulation */
PUSH: 0, // PUSH c
PUSH_CURR_POS: 1, // PUSH_CURR_POS
POP: 2, // POP
POP_CURR_POS: 3, // POP_CURR_POS
POP_N: 4, // POP_N n
NIP: 5, // NIP
NIP_CURR_POS: 6, // NIP_CURR_POS
APPEND: 7, // APPEND
WRAP: 8, // WRAP n
TEXT: 9, // TEXT

/* Conditions and Loops */

IF: 10, // IF t, f
IF_ERROR: 11, // IF_ERROR t, f
IF_NOT_ERROR: 12, // IF_NOT_ERROR t, f
WHILE_NOT_ERROR: 13, // WHILE_NOT_ERROR b

/* Matching */

MATCH_ANY: 14, // MATCH_ANY a, f, ...
MATCH_STRING: 15, // MATCH_STRING s, a, f, ...
MATCH_STRING_IC: 16, // MATCH_STRING_IC s, a, f, ...
MATCH_REGEXP: 17, // MATCH_REGEXP r, a, f, ...
ACCEPT_N: 18, // ACCEPT_N n
ACCEPT_STRING: 19, // ACCEPT_STRING s
FAIL: 20, // FAIL e

/* Calls */

REPORT_SAVED_POS: 21, // REPORT_SAVED_POS p
REPORT_CURR_POS: 22, // REPORT_CURR_POS
CALL: 23, // CALL f, n, pc, p1, p2, ..., pN

/* Rules */

RULE: 24, // RULE r

/* Failure Reporting */

SILENT_FAILS_ON: 25, // SILENT_FAILS_ON
SILENT_FAILS_OFF: 26 // SILENT_FAILS_FF
};
4 changes: 2 additions & 2 deletions lib/compiler/passes.js
Expand Up @@ -9,6 +9,6 @@ module.exports = {
reportMissingRules: require("./passes/report-missing-rules"),
reportLeftRecursion: require("./passes/report-left-recursion"),
removeProxyRules: require("./passes/remove-proxy-rules"),
allocateRegisters: require("./passes/allocate-registers"),
generateCode: require("./passes/generate-code")
generateBytecode: require("./passes/generate-bytecode"),
generateJavascript: require("./passes/generate-javascript")
};

3 comments on commit fe1ca48

@windyrobin
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Job !!!
and have you decided the time to make new npm release ?

@dmajda
Copy link
Contributor Author

@dmajda dmajda commented on fe1ca48 Jan 3, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@windyrobin I'll prepare a new release (0.8.0) when all features I'd like to be in get implemented. See the Trello board for details.

@shamansir
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, seems you kept it under the hood for a long time, great job!

Please sign in to comment.