Skip to content

Commit

Permalink
Implement basic support for tracing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dmajda committed Mar 30, 2015
1 parent 675561f commit da57118
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 17 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ You can tweak the generated parser with several options:
`PEG.buildParser`
* `--extra-options-file` — file with additional options (in JSON format) to
pass to `PEG.buildParser`
* `--trace` — makes the parser trace its progress

### JavaScript API

Expand Down Expand Up @@ -134,9 +135,10 @@ the input is invalid. The exception will contain `offset`, `line`, `column`,
parser.parse("abcd"); // throws an exception

You can tweak parser behavior by passing a second parameter with an options
object to the `parse` method. Only one option is currently supported:
object to the `parse` method. The following options are supported:

* `startRule` — name of the rule to start parsing from
* `tracer` — tracer to use

Parsers can also support their own custom options.

Expand Down
6 changes: 6 additions & 0 deletions bin/pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function printHelp() {
util.puts(" grammar)");
util.puts(" -o, --optimize <goal> select optimization for speed or size");
util.puts(" (default: speed)");
util.puts(" --trace enable tracing in generated parser");
util.puts(" --plugin <plugin> use a specified plugin (can be specified");
util.puts(" multiple times)");
util.puts(" --extra-options <options> additional options (in JSON format) to pass");
Expand Down Expand Up @@ -112,6 +113,7 @@ var options = {
cache: false,
output: "source",
optimize: "speed",
trace: false,
plugins: []
};

Expand Down Expand Up @@ -140,6 +142,10 @@ while (args.length > 0 && isOption(args[0])) {
.map(trim);
break;

case "--trace":
options.trace = true;
break;

case "-o":
case "--optimize":
nextArg();
Expand Down
1 change: 1 addition & 0 deletions lib/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var compiler = {
objects.defaults(options, {
allowedStartRules: [ast.rules[0].name],
cache: false,
trace: false,
optimize: "speed",
output: "parser"
});
Expand Down
203 changes: 187 additions & 16 deletions lib/compiler/passes/generate-javascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,60 @@ function generateJavascript(ast, options) {
}
}

function generateRuleHeader(ruleIndexCode) {
function generateRuleHeader(ruleNameCode, ruleIndexCode) {
var parts = [];

parts.push('');

if (options.trace) {
parts.push([
'peg$trace({',
' type: "rule.enter",',
' rule: ' + ruleNameCode,
'});',
''
].join('\n'));
}

if (options.cache) {
return [
'',
parts.push([
'var key = peg$currPos * ' + ast.rules.length + ' + ' + ruleIndexCode + ',',
' cached = peg$cache[key];',
'',
'if (cached) {',
' peg$currPos = cached.nextPos;',
'',
].join('\n'));

if (options.trace) {
parts.push([
'if (cached.result !== peg$FAILED) {',
' peg$trace({',
' type: "rule.match",',
' rule: ' + ruleNameCode + ',',
' result: cached.result',
' });',
'} else {',
' peg$trace({',
' type: "rule.fail",',
' rule: ' + ruleNameCode,
' });',
'}',
''
].join('\n'));
}

parts.push([
' return cached.result;',
'}',
''
].join('\n');
} else {
return '';
].join('\n'));
}

return parts.join('\n');
}

function generateRuleFooter(resultCode) {
function generateRuleFooter(ruleNameCode, resultCode) {
var parts = [];

if (options.cache) {
Expand All @@ -65,6 +100,24 @@ function generateJavascript(ast, options) {
].join('\n'));
}

if (options.trace) {
parts.push([
'',
'if (' + resultCode + ' !== peg$FAILED) {',
' peg$trace({',
' type: "rule.match",',
' rule: ' + ruleNameCode + ',',
' result: ' + resultCode,
' });',
'} else {',
' peg$trace({',
' type: "rule.fail",',
' rule: ' + ruleNameCode,
' });',
'}'
].join('\n'));
}

parts.push([
'',
'return ' + resultCode + ';'
Expand Down Expand Up @@ -158,7 +211,7 @@ function generateJavascript(ast, options) {
' params, i;',
].join('\n'));

parts.push(indent2(generateRuleHeader('index')));
parts.push(indent2(generateRuleHeader('peg$ruleNames[index]', 'index')));

parts.push([
/*
Expand Down Expand Up @@ -337,7 +390,7 @@ function generateJavascript(ast, options) {
' }'
].join('\n'));

parts.push(indent2(generateRuleFooter('stack[0]')));
parts.push(indent2(generateRuleFooter('peg$ruleNames[index]', 'stack[0]')));
parts.push('}');

return parts.join('\n');
Expand Down Expand Up @@ -657,9 +710,15 @@ function generateJavascript(ast, options) {
' var ' + arrays.map(arrays.range(0, stack.maxSp + 1), s).join(', ') + ';',
].join('\n'));

parts.push(indent2(generateRuleHeader(asts.indexOfRule(ast, rule.name))));
parts.push(indent2(generateRuleHeader(
'"' + js.stringEscape(rule.name) + '"',
asts.indexOfRule(ast, rule.name)
)));
parts.push(indent2(code));
parts.push(indent2(generateRuleFooter(s(0))));
parts.push(indent2(generateRuleFooter(
'"' + js.stringEscape(rule.name) + '"',
s(0)
)));

parts.push('}');

Expand All @@ -668,7 +727,8 @@ function generateJavascript(ast, options) {

var parts = [],
startRuleIndices, startRuleIndex,
startRuleFunctions, startRuleFunction;
startRuleFunctions, startRuleFunction,
ruleNames;

parts.push([
'(function() {',
Expand Down Expand Up @@ -696,7 +756,65 @@ function generateJavascript(ast, options) {
' }',
'',
' peg$subclass(peg$SyntaxError, Error);',
'',
''
].join('\n'));

if (options.trace) {
parts.push([
' function peg$DefaultTracer() {',
' this.indentLevel = 0;',
' }',
'',
' peg$DefaultTracer.prototype.trace = function(event) {',
' var that = this;',
'',
' function log(event) {',
' function repeat(string, n) {',
' var result = "", i;',
'',
' for (i = 0; i < n; i++) {',
' result += string;',
' }',
'',
' return result;',
' }',
'',
' function pad(string, length) {',
' return string + repeat(" ", length - string.length);',
' }',
'',
' console.log(',
' event.line + ":" + event.column + " "',
' + pad(event.type, 10) + " "',
' + repeat(" ", that.indentLevel) + event.rule',
' );',
' }',
'',
' switch (event.type) {',
' case "rule.enter":',
' log(event);',
' this.indentLevel++;',
' break;',
'',
' case "rule.match":',
' this.indentLevel--;',
' log(event);',
' break;',
'',
' case "rule.fail":',
' this.indentLevel--;',
' log(event);',
' break;',
'',
' default:',
' throw new Error("Invalid event type: " + event.type + ".");',
' }',
' };',
''
].join('\n'));
}

parts.push([
' function peg$parse(input) {',
' var options = arguments.length > 1 ? arguments[1] : {},',
' parser = this,',
Expand Down Expand Up @@ -750,7 +868,31 @@ function generateJavascript(ast, options) {
].join('\n'));

if (options.cache) {
parts.push(' peg$cache = {},');
parts.push([
' peg$cache = {},',
''
].join('\n'));
}

if (options.trace) {
if (options.optimize === "size") {
ruleNames = '['
+ arrays.map(
ast.rules,
function(r) { return '"' + js.stringEscape(r.name) + '"'; }
).join(', ')
+ ']';

parts.push([
' peg$ruleNames = ' + ruleNames + ',',
''
].join('\n'));
}

parts.push([
' peg$tracer = "tracer" in options ? options.tracer : new peg$DefaultTracer(),',
''
].join('\n'));
}

parts.push([
Expand Down Expand Up @@ -947,6 +1089,21 @@ function generateJavascript(ast, options) {
''
].join('\n'));

if (options.trace) {
parts.push([
' function peg$trace(event) {',
' var posDetails = peg$computePosDetails(peg$currPos);',
'',
' event.offset = peg$currPos;',
' event.line = posDetails.line;',
' event.column = posDetails.column;',
'',
' peg$tracer.trace(event);',
' }',
'',
].join('\n'));
}

if (options.optimize === "size") {
parts.push(indent4(generateInterpreter()));
parts.push('');
Expand Down Expand Up @@ -982,8 +1139,22 @@ function generateJavascript(ast, options) {
' }',
'',
' return {',
' SyntaxError: peg$SyntaxError,',
' parse: peg$parse',
].join('\n'));

if (options.trace) {
parts.push([
' SyntaxError: peg$SyntaxError,',
' DefaultTracer: peg$DefaultTracer,',
' parse: peg$parse'
].join('\n'));
} else {
parts.push([
' SyntaxError: peg$SyntaxError,',
' parse: peg$parse'
].join('\n'));
}

parts.push([
' };',
'})()'
].join('\n'));
Expand Down
Loading

2 comments on commit da57118

@craigulliott
Copy link

@craigulliott craigulliott commented on da57118 Jul 19, 2016

Choose a reason for hiding this comment

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

Incase it helps someone... I landed here from a google search, and ran into an issue because of a typo in the notes for this commit.

It should say...

You can customize tracing by passing a custom tracer to parser's
|parse| method using the |tracer| option:

parser.parse(input, { tracer: tracer });

The key in the options object is tracer, not trace.

Thanks!

@dmajda
Copy link
Contributor Author

@dmajda dmajda commented on da57118 Jul 21, 2016

Choose a reason for hiding this comment

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

@craigulliott Sorry about that, I’m sure it took a while to figure out. And thanks for the note.

Please sign in to comment.