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.
pegjs/lib/compiler/passes/generate-javascript.js

944 lines
29 KiB
JavaScript

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 :-)
12 years ago
var utils = require("../../utils"),
op = require("../opcodes");
/* Generates parser JavaScript code. */
module.exports = function(ast, options) {
/* These only indent non-empty lines to avoid trailing whitespace. */
function indent2(code) { return code.replace(/^(.+)$/gm, ' $1'); }
function indent4(code) { return code.replace(/^(.+)$/gm, ' $1'); }
function indent8(code) { return code.replace(/^(.+)$/gm, ' $1'); }
function indent10(code) { return code.replace(/^(.+)$/gm, ' $1'); }
function generateTables() {
if (options.optimize === "size") {
return [
'peg$consts = [',
indent2(ast.consts.join(',\n')),
'],',
'',
'peg$bytecode = [',
indent2(utils.map(
ast.rules,
function(rule) {
return 'peg$decode('
+ utils.quote(utils.map(
rule.bytecode,
function(b) { return String.fromCharCode(b + 32); }
).join(''))
+ ')';
}
).join(',\n')),
'],'
].join('\n');
} else {
return utils.map(
ast.consts,
function(c, i) { return 'peg$c' + i + ' = ' + c + ','; }
).join('\n');
}
}
function generateCacheHeader(ruleIndexCode) {
return [
'var key = peg$currPos * ' + ast.rules.length + ' + ' + ruleIndexCode + ',',
' cached = peg$cache[key];',
'',
'if (cached) {',
' peg$currPos = cached.nextPos;',
' return cached.result;',
'}',
''
].join('\n');
}
function generateCacheFooter(resultCode) {
return [
'',
'peg$cache[key] = { nextPos: peg$currPos, result: ' + resultCode + ' };'
].join('\n');
}
function generateInterpreter() {
var parts = [];
function generateCondition(cond, argsLength) {
var baseLength = argsLength + 3,
thenLengthCode = 'bc[ip + ' + (baseLength - 2) + ']',
elseLengthCode = 'bc[ip + ' + (baseLength - 1) + ']';
return [
'ends.push(end);',
'ips.push(ip + ' + baseLength + ' + ' + thenLengthCode + ' + ' + elseLengthCode + ');',
'',
'if (' + cond + ') {',
' end = ip + ' + baseLength + ' + ' + thenLengthCode + ';',
' ip += ' + baseLength + ';',
'} else {',
' end = ip + ' + baseLength + ' + ' + thenLengthCode + ' + ' + elseLengthCode + ';',
' ip += ' + baseLength + ' + ' + thenLengthCode + ';',
'}',
'',
'break;'
].join('\n');
}
function generateLoop(cond) {
var baseLength = 2,
bodyLengthCode = 'bc[ip + ' + (baseLength - 1) + ']';
return [
'if (' + cond + ') {',
' ends.push(end);',
' ips.push(ip);',
'',
' end = ip + ' + baseLength + ' + ' + bodyLengthCode + ';',
' ip += ' + baseLength + ';',
'} else {',
' ip += ' + baseLength + ' + ' + bodyLengthCode + ';',
'}',
'',
'break;'
].join('\n');
}
function generateCall() {
var baseLength = 4,
paramsLengthCode = 'bc[ip + ' + (baseLength - 1) + ']';
return [
'params = bc.slice(ip + ' + baseLength + ', ip + ' + baseLength + ' + ' + paramsLengthCode + ');',
'for (i = 0; i < ' + paramsLengthCode + '; i++) {',
' params[i] = stack[stack.length - 1 - params[i]];',
'}',
'',
'stack.splice(',
' stack.length - bc[ip + 2],',
' bc[ip + 2],',
' peg$consts[bc[ip + 1]].apply(null, params)',
');',
'',
'ip += ' + baseLength + ' + ' + paramsLengthCode + ';',
'break;'
].join('\n');
}
parts.push([
'function peg$decode(s) {',
' var bc = new Array(s.length), i;',
'',
' for (i = 0; i < s.length; i++) {',
' bc[i] = s.charCodeAt(i) - 32;',
' }',
'',
' return bc;',
'}',
'',
'function peg$parseRule(index) {',
' var bc = peg$bytecode[index],',
' ip = 0,',
' ips = [],',
' end = bc.length,',
' ends = [],',
' stack = [],',
' params, i;',
''
].join('\n'));
if (options.cache) {
parts.push(indent2(generateCacheHeader('index')));
}
parts.push([
' function protect(object) {',
' return Object.prototype.toString.apply(object) === "[object Array]" ? [] : object;',
' }',
'',
/*
* The point of the outer loop and the |ips| & |ends| stacks is to avoid
* recursive calls for interpreting parts of bytecode. In other words, we
* implement the |interpret| operation of the abstract machine without
* function calls. Such calls would likely slow the parser down and more
* importantly cause stack overflows for complex grammars.
*/
' while (true) {',
' while (ip < end) {',
' switch (bc[ip]) {',
' case ' + op.PUSH + ':', // PUSH c
/*
* Hack: One of the constants can be an empty array. It needs to be cloned
* because it can be modified later on the stack by |APPEND|.
*/
' stack.push(protect(peg$consts[bc[ip + 1]]));',
' ip += 2;',
' break;',
'',
' case ' + op.PUSH_CURR_POS + ':', // PUSH_CURR_POS
' stack.push(peg$currPos);',
' ip++;',
' break;',
'',
' case ' + op.POP + ':', // POP
' stack.pop();',
' ip++;',
' break;',
'',
' case ' + op.POP_CURR_POS + ':', // POP_CURR_POS
' peg$currPos = stack.pop();',
' ip++;',
' break;',
'',
' case ' + op.POP_N + ':', // POP_N n
' stack.length -= bc[ip + 1];',
' ip += 2;',
' break;',
'',
' case ' + op.NIP + ':', // NIP
' stack.splice(-2, 1);',
' ip++;',
' break;',
'',
' case ' + op.NIP_CURR_POS + ':', // NIP_CURR_POS
' peg$currPos = stack.splice(-2, 1)[0];',
' ip++;',
' break;',
'',
' case ' + op.APPEND + ':', // APPEND
' stack[stack.length - 2].push(stack.pop());',
' ip++;',
' break;',
'',
' case ' + op.WRAP + ':', // WRAP n
' stack.push(stack.splice(stack.length - bc[ip + 1]));',
' ip += 2;',
' break;',
'',
' case ' + op.TEXT + ':', // TEXT
' stack.pop();',
' stack.push(input.substring(stack[stack.length - 1], peg$currPos));',
' ip++;',
' break;',
'',
' case ' + op.IF + ':', // IF t, f
indent10(generateCondition('stack[stack.length - 1]', 0)),
'',
' case ' + op.IF_ERROR + ':', // IF_ERROR t, f
indent10(generateCondition(
'stack[stack.length - 1] === null',
0
)),
'',
' case ' + op.IF_NOT_ERROR + ':', // IF_NOT_ERROR t, f
indent10(
generateCondition('stack[stack.length - 1] !== null',
0
)),
'',
' case ' + op.WHILE_NOT_ERROR + ':', // WHILE_NOT_ERROR b
indent10(generateLoop('stack[stack.length - 1] !== null')),
'',
' case ' + op.MATCH_ANY + ':', // MATCH_ANY a, f, ...
indent10(generateCondition('input.length > peg$currPos', 0)),
'',
' case ' + op.MATCH_STRING + ':', // MATCH_STRING s, a, f, ...
indent10(generateCondition(
'input.substr(peg$currPos, peg$consts[bc[ip + 1]].length) === peg$consts[bc[ip + 1]]',
1
)),
'',
' case ' + op.MATCH_STRING_IC + ':', // MATCH_STRING_IC s, a, f, ...
indent10(generateCondition(
'input.substr(peg$currPos, peg$consts[bc[ip + 1]].length).toLowerCase() === peg$consts[bc[ip + 1]]',
1
)),
'',
' case ' + op.MATCH_REGEXP + ':', // MATCH_REGEXP r, a, f, ...
indent10(generateCondition(
'peg$consts[bc[ip + 1]].test(input.charAt(peg$currPos))',
1
)),
'',
' case ' + op.ACCEPT_N + ':', // ACCEPT_N n
' stack.push(input.substr(peg$currPos, bc[ip + 1]));',
' peg$currPos += bc[ip + 1];',
' ip += 2;',
' break;',
'',
' case ' + op.ACCEPT_STRING + ':', // ACCEPT_STRING s
' stack.push(peg$consts[bc[ip + 1]]);',
' peg$currPos += peg$consts[bc[ip + 1]].length;',
' ip += 2;',
' break;',
'',
' case ' + op.FAIL + ':', // FAIL e
' stack.push(null);',
' if (peg$silentFails === 0) {',
' peg$fail(peg$consts[bc[ip + 1]]);',
' }',
' ip += 2;',
' break;',
'',
' case ' + op.REPORT_SAVED_POS + ':', // REPORT_SAVED_POS p
' peg$reportedPos = stack[stack.length - 1 - bc[ip + 1]];',
' ip += 2;',
' break;',
'',
' case ' + op.REPORT_CURR_POS + ':', // REPORT_CURR_POS
' peg$reportedPos = peg$currPos;',
' ip++;',
' break;',
'',
' case ' + op.CALL + ':', // CALL f, n, pc, p1, p2, ..., pN
indent10(generateCall()),
'',
' case ' + op.RULE + ':', // RULE r
' stack.push(peg$parseRule(bc[ip + 1]));',
' ip += 2;',
' break;',
'',
' case ' + op.SILENT_FAILS_ON + ':', // SILENT_FAILS_ON
' peg$silentFails++;',
' ip++;',
' break;',
'',
' case ' + op.SILENT_FAILS_OFF + ':', // SILENT_FAILS_OFF
' peg$silentFails--;',
' ip++;',
' break;',
'',
' default:',
' throw new Error("Invalid opcode: " + bc[ip] + ".");',
' }',
' }',
'',
' if (ends.length > 0) {',
' end = ends.pop();',
' ip = ips.pop();',
' } else {',
' break;',
' }',
' }'
].join('\n'));
if (options.cache) {
parts.push(indent2(generateCacheFooter('stack[0]')));
}
parts.push([
'',
' return stack[0];',
'}'
].join('\n'));
return parts.join('\n');
}
function generateRuleFunction(rule) {
var parts = [], code;
function c(i) { return "peg$c" + i; } // |consts[i]| of the abstract machine
function s(i) { return "s" + i; } // |stack[i]| of the abstract machine
var stack = {
sp: -1,
maxSp: -1,
push: function(exprCode) {
var code = s(++this.sp) + ' = ' + exprCode + ';';
if (this.sp > this.maxSp) { this.maxSp = this.sp; }
return code;
},
pop: function() {
var n, values;
if (arguments.length === 0) {
return s(this.sp--);
} else {
n = arguments[0];
values = utils.map(utils.range(this.sp - n + 1, this.sp + 1), s);
this.sp -= n;
return values;
}
},
top: function() {
return s(this.sp);
},
index: function(i) {
return s(this.sp - i);
}
};
function compile(bc) {
var ip = 0,
end = bc.length,
parts = [],
value;
function compileCondition(cond, argCount) {
var baseLength = argCount + 3,
thenLength = bc[ip + baseLength - 2],
elseLength = bc[ip + baseLength - 1],
baseSp = stack.sp,
thenCode, elseCode;
ip += baseLength;
thenCode = compile(bc.slice(ip, ip + thenLength));
ip += thenLength;
if (elseLength > 0) {
stack.sp = baseSp;
elseCode = compile(bc.slice(ip, ip + elseLength));
ip += elseLength;
}
parts.push('if (' + cond + ') {');
parts.push(indent2(thenCode));
if (elseLength > 0) {
parts.push('} else {');
parts.push(indent2(elseCode));
}
parts.push('}');
}
function compileLoop(cond) {
var baseLength = 2,
bodyLength = bc[ip + baseLength - 1],
bodyCode;
ip += baseLength;
bodyCode = compile(bc.slice(ip, ip + bodyLength));
ip += bodyLength;
parts.push('while (' + cond + ') {');
parts.push(indent2(bodyCode));
parts.push('}');
}
function compileCall(cond) {
var baseLength = 4,
paramsLength = bc[ip + baseLength - 1];
var value = c(bc[ip + 1]) + '('
+ utils.map(
bc.slice(ip + baseLength, ip + baseLength + paramsLength),
stackIndex
)
+ ')';
stack.pop(bc[ip + 2]);
parts.push(stack.push(value));
ip += baseLength + paramsLength;
}
/*
* Extracted into a function just to silence JSHint complaining about
* creating functions in a loop.
*/
function stackIndex(p) {
return stack.index(p);
}
while (ip < end) {
switch (bc[ip]) {
case op.PUSH: // PUSH c
/*
* Hack: One of the constants can be an empty array. It needs to be
* handled specially because it can be modified later on the stack
* by |APPEND|.
*/
parts.push(
stack.push(ast.consts[bc[ip + 1]] === "[]" ? "[]" : c(bc[ip + 1]))
);
ip += 2;
break;
case op.PUSH_CURR_POS: // PUSH_CURR_POS
parts.push(stack.push('peg$currPos'));
ip++;
break;
case op.POP: // POP
stack.pop();
ip++;
break;
case op.POP_CURR_POS: // POP_CURR_POS
parts.push('peg$currPos = ' + stack.pop() + ';');
ip++;
break;
case op.POP_N: // POP_N n
stack.pop(bc[ip + 1]);
ip += 2;
break;
case op.NIP: // NIP
value = stack.pop();
stack.pop();
parts.push(stack.push(value));
ip++;
break;
case op.NIP_CURR_POS: // NIP_CURR_POS
value = stack.pop();
parts.push('peg$currPos = ' + stack.pop() + ';');
parts.push(stack.push(value));
ip++;
break;
case op.APPEND: // APPEND
value = stack.pop();
parts.push(stack.top() + '.push(' + value + ');');
ip++;
break;
case op.WRAP: // WRAP n
parts.push(
stack.push('[' + stack.pop(bc[ip + 1]).join(', ') + ']')
);
ip += 2;
break;
case op.TEXT: // TEXT
stack.pop();
parts.push(
stack.push('input.substring(' + stack.top() + ', peg$currPos)')
);
ip++;
break;
case op.IF: // IF t, f
compileCondition(stack.top(), 0);
break;
case op.IF_ERROR: // IF_ERROR t, f
compileCondition(stack.top() + ' === null', 0);
break;
case op.IF_NOT_ERROR: // IF_NOT_ERROR t, f
compileCondition(stack.top() + ' !== null', 0);
break;
case op.WHILE_NOT_ERROR: // WHILE_NOT_ERROR b
compileLoop(stack.top() + ' !== null', 0);
break;
case op.MATCH_ANY: // MATCH_ANY a, f, ...
compileCondition('input.length > peg$currPos', 0);
break;
case op.MATCH_STRING: // MATCH_STRING s, a, f, ...
compileCondition(
eval(ast.consts[bc[ip + 1]]).length > 1
? 'input.substr(peg$currPos, '
+ eval(ast.consts[bc[ip + 1]]).length
+ ') === '
+ c(bc[ip + 1])
: 'input.charCodeAt(peg$currPos) === '
+ eval(ast.consts[bc[ip + 1]]).charCodeAt(0),
1
);
break;
case op.MATCH_STRING_IC: // MATCH_STRING_IC s, a, f, ...
compileCondition(
'input.substr(peg$currPos, '
12 years ago
+ ast.consts[bc[ip + 1]].length
+ ').toLowerCase() === '
+ c(bc[ip + 1]),
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 :-)
12 years ago
1
);
break;
case op.MATCH_REGEXP: // MATCH_REGEXP r, a, f, ...
compileCondition(
c(bc[ip + 1]) + '.test(input.charAt(peg$currPos))',
1
);
break;
case op.ACCEPT_N: // ACCEPT_N n
parts.push(stack.push(
bc[ip + 1] > 1
? 'input.substr(peg$currPos, ' + bc[ip + 1] + ')'
: 'input.charAt(peg$currPos)'
));
parts.push(
bc[ip + 1] > 1
? 'peg$currPos += ' + bc[ip + 1] + ';'
: 'peg$currPos++;'
);
ip += 2;
break;
case op.ACCEPT_STRING: // ACCEPT_STRING s
parts.push(stack.push(c(bc[ip + 1])));
parts.push(
eval(ast.consts[bc[ip + 1]]).length > 1
? 'peg$currPos += ' + eval(ast.consts[bc[ip + 1]]).length + ';'
: 'peg$currPos++;'
);
ip += 2;
break;
case op.FAIL: // FAIL e
parts.push(stack.push('null'));
parts.push('if (peg$silentFails === 0) { peg$fail(' + c(bc[ip + 1]) + '); }');
ip += 2;
break;
case op.REPORT_SAVED_POS: // REPORT_SAVED_POS p
parts.push('peg$reportedPos = ' + stack.index(bc[ip + 1]) + ';');
ip += 2;
break;
case op.REPORT_CURR_POS: // REPORT_CURR_POS
parts.push('peg$reportedPos = peg$currPos;');
ip++;
break;
case op.CALL: // CALL f, n, pc, p1, p2, ..., pN
compileCall();
break;
case op.RULE: // RULE r
parts.push(stack.push("peg$parse" + ast.rules[bc[ip + 1]].name + "()"));
ip += 2;
break;
case op.SILENT_FAILS_ON: // SILENT_FAILS_ON
parts.push('peg$silentFails++;');
ip++;
break;
case op.SILENT_FAILS_OFF: // SILENT_FAILS_OFF
parts.push('peg$silentFails--;');
ip++;
break;
default:
throw new Error("Invalid opcode: " + bc[ip] + ".");
}
}
return parts.join('\n');
}
code = compile(rule.bytecode);
parts.push([
'function peg$parse' + rule.name + '() {',
' var ' + utils.map(utils.range(0, stack.maxSp + 1), s).join(', ') + ';',
''
].join('\n'));
if (options.cache) {
parts.push(indent2(
generateCacheHeader(utils.indexOfRuleByName(ast, rule.name))
));
}
parts.push(indent2(code));
if (options.cache) {
parts.push(indent2(generateCacheFooter('s0')));
}
parts.push([
'',
' return s0;',
'}'
].join('\n'));
return parts.join('\n');
}
var parts = [],
startRuleIndices, startRuleIndex,
startRuleFunctions, startRuleFunction;
parts.push([
'(function() {',
' /*',
' * Generated by PEG.js 0.7.0.',
' *',
' * http://pegjs.majda.cz/',
' */',
'',
' function peg$subclass(child, parent) {',
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 :-)
12 years ago
' function ctor() { this.constructor = child; }',
' ctor.prototype = parent.prototype;',
' child.prototype = new ctor();',
' }',
'',
' function SyntaxError(expected, found, offset, line, column) {',
' function buildMessage(expected, found) {',
' function stringEscape(s) {',
' function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); }',
'',
/*
* 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 escape all control and non-ASCII
* characters. Note that "\0" and "\v" escape sequences are not used because
* JSHint does not like the first and IE the second.
*/
' return s',
' .replace(/\\\\/g, \'\\\\\\\\\')', // backslash
' .replace(/"/g, \'\\\\"\')', // closing double quote
' .replace(/\\x08/g, \'\\\\b\')', // backspace
' .replace(/\\t/g, \'\\\\t\')', // horizontal tab
' .replace(/\\n/g, \'\\\\n\')', // line feed
' .replace(/\\f/g, \'\\\\f\')', // form feed
' .replace(/\\r/g, \'\\\\r\')', // carriage return
' .replace(/[\\x00-\\x07\\x0B\\x0E\\x0F]/g, function(ch) { return \'\\\\x0\' + hex(ch); })',
' .replace(/[\\x10-\\x1F\\x80-\\xFF]/g, function(ch) { return \'\\\\x\' + hex(ch); })',
' .replace(/[\\u0180-\\u0FFF]/g, function(ch) { return \'\\\\u0\' + hex(ch); })',
' .replace(/[\\u1080-\\uFFFF]/g, function(ch) { return \'\\\\u\' + hex(ch); });',
' }',
'',
' var expectedDesc, foundDesc;',
'',
' switch (expected.length) {',
' case 0:',
' expectedDesc = "end of input";',
' break;',
'',
' case 1:',
' expectedDesc = expected[0];',
' break;',
'',
' default:',
' expectedDesc = expected.slice(0, -1).join(", ")',
' + " or "',
' + expected[expected.length - 1];',
' }',
'',
' foundDesc = found ? "\\"" + stringEscape(found) + "\\"" : "end of input";',
'',
' return "Expected " + expectedDesc + " but " + foundDesc + " found.";',
' }',
'',
' this.expected = expected;',
' this.found = found;',
' this.offset = offset;',
' this.line = line;',
' this.column = column;',
'',
' this.name = "SyntaxError";',
' this.message = buildMessage(expected, found);',
' }',
'',
' peg$subclass(SyntaxError, Error);',
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 :-)
12 years ago
'',
' function parse(input) {',
' var options = arguments.length > 1 ? arguments[1] : {},',
''
].join('\n'));
if (options.optimize === "size") {
startRuleIndices = '{ '
+ utils.map(
options.allowedStartRules,
function(r) { return r + ': ' + utils.indexOfRuleByName(ast, r); }
).join(', ')
+ ' }';
startRuleIndex = utils.indexOfRuleByName(ast, options.allowedStartRules[0]);
parts.push([
' peg$startRuleIndices = ' + startRuleIndices + ',',
' peg$startRuleIndex = ' + startRuleIndex + ','
].join('\n'));
} else {
startRuleFunctions = '{ '
+ utils.map(
options.allowedStartRules,
function(r) { return r + ': peg$parse' + r; }
).join(', ')
+ ' }';
startRuleFunction = 'peg$parse' + options.allowedStartRules[0];
parts.push([
' peg$startRuleFunctions = ' + startRuleFunctions + ',',
' peg$startRuleFunction = ' + startRuleFunction + ','
].join('\n'));
}
parts.push('');
parts.push(indent8(generateTables()));
parts.push([
'',
' peg$currPos = 0,',
' peg$reportedPos = 0,',
' peg$cachedPos = 0,',
' peg$cachedPosDetails = { line: 1, column: 1, seenCR: false },',
' peg$maxFailPos = 0,',
' peg$maxFailExpected = [],',
' peg$silentFails = 0,', // 0 = report failures, > 0 = silence failures
''
].join('\n'));
if (options.cache) {
parts.push(' peg$cache = {},');
}
parts.push([
' peg$result;',
''
].join('\n'));
if (options.optimize === "size") {
parts.push([
' if ("startRule" in options) {',
' if (!(options.startRule in peg$startRuleIndices)) {',
' throw new Error("Can\'t start parsing from rule \\"" + options.startRule + "\\".");',
' }',
'',
' peg$startRuleIndex = peg$startRuleIndices[options.startRule];',
' }'
].join('\n'));
} else {
parts.push([
' if ("startRule" in options) {',
' if (!(options.startRule in peg$startRuleFunctions)) {',
' throw new Error("Can\'t start parsing from rule \\"" + options.startRule + "\\".");',
' }',
'',
' peg$startRuleFunction = peg$startRuleFunctions[options.startRule];',
' }'
].join('\n'));
}
parts.push([
'',
' function text() {',
' return input.substring(peg$reportedPos, peg$currPos);',
' }',
'',
' function offset() {',
' return peg$reportedPos;',
' }',
'',
' function line() {',
' return peg$computePosDetails(peg$reportedPos).line;',
' }',
'',
' function column() {',
' return peg$computePosDetails(peg$reportedPos).column;',
' }',
'',
' function peg$computePosDetails(pos) {',
' function advance(details, pos) {',
' var p, ch;',
'',
' for (p = 0; p < pos; p++) {',
' ch = input.charAt(p);',
' if (ch === "\\n") {',
' if (!details.seenCR) { details.line++; }',
' details.column = 1;',
' details.seenCR = false;',
' } else if (ch === "\\r" || ch === "\\u2028" || ch === "\\u2029") {',
' details.line++;',
' details.column = 1;',
' details.seenCR = true;',
' } else {',
' details.column++;',
' details.seenCR = false;',
' }',
' }',
' }',
'',
' if (peg$cachedPos !== pos) {',
' if (peg$cachedPos > pos) {',
' peg$cachedPos = 0;',
' peg$cachedPosDetails = { line: 1, column: 1, seenCR: false };',
' }',
' peg$cachedPos = pos;',
' advance(peg$cachedPosDetails, peg$cachedPos);',
' }',
'',
' return peg$cachedPosDetails;',
' }',
'',
' function peg$fail(expected) {',
' if (peg$currPos < peg$maxFailPos) { return; }',
'',
' if (peg$currPos > peg$maxFailPos) {',
' peg$maxFailPos = peg$currPos;',
' peg$maxFailExpected = [];',
' }',
'',
' peg$maxFailExpected.push(expected);',
' }',
'',
' function peg$cleanupExpected(expected) {',
' var i;',
'',
' expected.sort();',
'',
' for (i = 1; i < expected.length; i++) {',
' if (expected[i - 1] === expected[i]) {',
' expected.splice(i, 1);',
' }',
' }',
' }',
''
].join('\n'));
if (options.optimize === "size") {
parts.push(indent4(generateInterpreter()));
parts.push('');
} else {
utils.each(ast.rules, function(rule) {
parts.push(indent4(generateRuleFunction(rule)));
parts.push('');
});
}
if (ast.initializer) {
parts.push(indent4(ast.initializer.code));
parts.push('');
}
if (options.optimize === "size") {
parts.push(' peg$result = peg$parseRule(peg$startRuleIndex);');
} else {
parts.push(' peg$result = peg$startRuleFunction();');
}
parts.push([
'',
' if (peg$result !== null && peg$currPos === input.length) {',
' return peg$result;',
' } else {',
' peg$cleanupExpected(peg$maxFailExpected);',
' peg$reportedPos = Math.max(peg$currPos, peg$maxFailPos);',
'',
' throw new SyntaxError(',
' peg$maxFailExpected,',
' peg$reportedPos < input.length ? input.charAt(peg$reportedPos) : null,',
' peg$reportedPos,',
' peg$computePosDetails(peg$reportedPos).line,',
' peg$computePosDetails(peg$reportedPos).column',
' );',
' }',
' }',
'',
' return {',
' SyntaxError: SyntaxError,',
' parse : parse',
' };',
'})()'
].join('\n'));
ast.code = parts.join('\n');
};