pencil-tracer
is a library that takes a JavaScript or CoffeeScript program as
input, and outputs instrumented JavaScript that records a line-by-line trace of
the program's execution when it runs.
This library was developed for Pencil Code as a GSoC 2015 project.
$ npm install pencil-tracer
$ cake build
$ cake test
To quickly try it out, clone this repository and run these cake
tasks.
$ cake -f test/traces/js/simple.js instrument
$ cake -f test/traces/js/simple.js trace
The first task instruments the given file and shows you the output. The second task does a trace on the given file and shows you the trace.
var pencilTracer = require('pencil-tracer');
// javascript
var output = pencilTracer.instrumentJs('var x = 3;');
// coffeescript
var coffeeScript = require('coffee-script');
var output = pencilTracer.instrumentCoffee('x = 3', coffeeScript);
Two functions are exported: instrumentJs
and instrumentCoffee
.
instrumentJs
takes some code and an options object. instrumentCoffee
takes
the same arguments, as well as a CoffeeScript compiler as the second argument
(this lets you use a specific version of CoffeeScript, including Iced
CoffeeScript).
Both functions return a string containing the instrumented code. When run, the
instrumented code will make a call to pencilTrace()
for each line, passing it
an object like this:
{
type: 'after',
location: {
first_line: 1,
first_column: 1,
last_line: 1,
last_column: 5
},
vars: [{ name: 'x', value: 3 }]
}
type
is 'before'
or 'after'
for normal executed code. It can also be
'enter'
or 'leave'
when a function is entered or left.
instrumentJs
and instrumentCoffee
take the following options:
traceFunc
: the function that will be called for each event (default:'pencilTrace'
).ast
: if true, returns the instrumented AST instead of the compiled JS.bare
(CoffeeScript only): if true, tells coffeescript not to wrap the output in a top-level function.sourceMap
: if true, returns a source map as well as the instrumented code.includeArgsStrings
: if true, each tracked function call will include a string containing the arguments passed to the function.
pencil-tracer.js
is a browserified (UMD) version of the library.
All pencil-tracer
gives you is a string of instrumented JavaScript. It is up
to you to run that code and collect the events. Here is an example program that
does that, using Contextify
to the run the instrumented code in a sandbox.
var pencilTracer = require('pencil-tracer');
var Contextify = require('contextify');
var code = pencilTracer.instrumentJs('var x = 3;');
var sandbox = {
pencilTrace: function(event) {
sandbox.pencilTraceEvents.push(event);
},
pencilTraceEvents: []
};
Contextify(sandbox);
sandbox.run(code);
console.log(sandbox.pencilTraceEvents);
For the most part, every ordinary statement gets instrumented with 'before'
and 'after'
events. For example,
var x;
x = 1;
x++;
This program would be instrumented like so:
<var x;>
<x = 1;>
<x++;>
Where <
is shorthand for pencilTrace('before', ...);
and >
is shorthand
for pencilTrace('after', ...);
. I'll continue using this shorthand for the
rest of this section.
Function declarations get instrumented like an ordinary statement.
// javascript
<function square(x) {
return x * x;
}>
In JavaScript, a semicolon by itself is called an empty statement. Each empty statement gets instrumented like any other statement.
// javascript
<;>
The condition expression is instrumented in if
and unless
statements.
// javascript
if (<false>) {
...
} else if (<true>) {
...
} else {
...
}
# coffeescript
if <false>
...
else if <true>
...
else
...
<i += 1> unless <false>
The object expression is instrumented.
// javascript
with (<obj>) {
...
}
The expression being switched on is instrumented, and each case expression is instrumented.
// javascript
switch (<3>) {
case <1>:
...
case <2>:
...
case <3>:
...
default:
...
}
# coffeescript
switch <3>
when <1> then ...
when <2>, <3> then ...
else ...
The expression being returned or thrown is instrumented.
// javascript
return <true>;
throw <"error!">;
# coffeescript
return <true>
throw <"error!">
Only the error variable of the catch
clause is instrumented, if it exists.
// javascript
try {
...
} catch (<err>) {
...
} finally {
...
}
# coffeescript
try
...
catch <err>
...
finally
...
The loop condition is instrumented.
// javascript
while (<true>) {
...
}
# coffeescript
while <true>
...
Note: the loop
keyword in CoffeeScript is syntax sugar for while true
, so
it will be instrumented in the same way.
The loop condition is instrumented.
// javascript
do {
...
} while (<true>);
Each of the three expressions in the head of the for
loop is instrumented, if
they exist.
// javascript
for (<var i = 0>; <i != 3>; <i++>) {
...
}
In the case of a for (;;) { ... }
loop, the middle conditional expression is
instrumented.
// javascript
for (;<>;) {
...
}
The object being iterated over and the variables being assigned to are both instrumented.
// javascript
for (<key> in <obj>) {
...
}
# coffeescript
for <key, value> of <obj>
...
for <elem, idx> in <ary>
...
The comma operator in JavaScript is known as a sequence expression, and even though it can be used to put multiple statement-like expressions in a single expression, the subexpressions are not instrumented in any special way.
// javascript
<x = (i++, i++, i);>
# coffeescript
<x = (i += 1; i += 1; i)>
The head of the class is instrumented, and each method definition is instrumented.
# coffeescript
<class Person extends Entity>
<constructor: (@firstName, @lastName) ->
...>
<fullName: ->
...>
CoffeeScript allows when
clauses on its loops, which act as guards. If a loop
has a guard, the guard expression will be instrumented.
# coffeescript
for <n> in <[1, 2, 3, 4, 5]> when <n % 2 is 0>
...
CoffeeScript's list comprehensions are just ordinary loops, which were covered above, but it may helpful to show an example of how they are instrumented.
# coffeescript
<odd_squares = (<n * n> for <n> in <[1, 2, 3, 4, 5]> when <(n * n) % 2 is 1>)>
A program's execution can be traced by collecting the events that are triggered by the instrumented code. This simple program demonstrates the four types of events that can be triggered:
var square = function (x) {
return x * x;
};
var y = square(3);
Here is what the program looks like after being instrumented:
var _returnVar;
pencilTrace({type: 'before', location: {first_line: 1, ...}, vars: [{name: 'square', value: square, functionDef: true}]});
var square = function (x) {
var _returnOrThrow = { type: 'return', value: undefined };
pencilTrace({type: 'enter', location: {first_line: 1, ...}, vars: [{name: 'x', value: x}]});
try {
pencilTrace({type: 'before', location: {first_line: 2, ...}, vars: [{name: 'x', value: x}]});
_returnOrThrow.value = x * x;
pencilTrace({type: 'after', location: {first_line: 2, ...}, vars: [{name: 'x', value: x}]});
return _returnOrThrow.value;
} catch (err) {
_returnOrThrow.type = 'throw';
_returnOrThrow.value = err;
throw err;
} finally {
pencilTrace({type: 'leave', location: {first_line: 1, ...}, returnOrThrow: _returnOrThrow});
}
};
pencilTrace({type: 'after', location: {first_line: 1, ...}, vars: [{name: 'square', value: square, functionDef: true}]});
pencilTrace({type: 'before', location: {first_line: 5, ...}, vars: [{name: 'y', value: y}]});
var y = (_returnVar = square(3));
pencilTrace({type: 'after', location: {first_line: 5, ...}, vars: [{name: 'y', value: y}], functionCalls: [{name: 'square', value: _returnVar}]});
(The location
property also includes first_column
, last_line
, and
last_column
fields, which are left out for readability here.)
Each event is an object with a type
of either 'before'
, 'after'
,
'enter'
, or 'leave'
. You can collect these events into a full trace by
providing a pencilTrace()
implementation like this:
var pencilTraceEvents = [];
var pencilTrace = function (event) {
pencilTraceEvents.push(event);
}
This would produce the following trace of the program above:
[{type: 'before', location: {first_line: 1, ...}, vars: [{name: 'square', value: undefined, functionDef: true}]},
{type: 'after', location: {first_line: 1, ...}, vars: [{name: 'square', value: <function>, functionDef: true}]},
{type: 'before', location: {first_line: 5, ...}, vars: [{name: 'y', value: undefined}]},
{type: 'enter', location: {first_line: 1, ...}, vars: [{name: 'x', value: 3}]},
{type: 'before', location: {first_line: 2, ...}, vars: [{name: 'x', value: 3}]},
{type: 'after', location: {first_line: 2, ...}, vars: [{name: 'x', value: 3}]},
{type: 'leave', location: {first_line: 1, ...}, returnOrThrow: {type: 'return', value: 9},
{type: 'after', location: {first_line: 5, ...}, vars: [{name: 'y', value: 9}], functionCalls: [{name: 'square', value: 9}]}]
As this example shows, each statement in the original program will trigger a
'before'
and 'after'
event (with variable values that are used in that
statement), and each instrumented function will trigger an 'enter
' event
(with argument values) and a 'leave'
event (with either the return value or
the the thrown error in the case of an exception).
Every event has type
and location
properties. location
is the start and
end location of the original code that this event is associated with.
{
type: 'before' or 'after' or 'enter' or 'leave',
location: {
first_line: 1-indexed integer,
first_column: 1-indexed integer,
last_line: 1-indexed integer,
last_column: 1-indexed integer
},
...
}
Triggered before each instrumented statement. A vars
property contains the
variables and values used in the original code that this event is associated
with. Each object in vars
has a name
property and a value
property.
{
type: 'before',
location: { ... },
vars: [ ... ]
}
For every 'before'
event, there is an 'after'
event with the same
location
and the same variable names in vars
, but if any variables were
updated by the code that this event is associated with, their new values will
be available in vars
. after
events also contain a functionCalls
property containing names and values of function calls used in the code.
{
type: 'after',
location: { ... },
vars: [ ... ],
functionCalls: [ ... ]
}
Triggered at the beginning of a body of a function. The vars
property contains
argument names and values. The location
will give the start and end of the
entire function body.
{
type: 'enter',
location: { ... },
vars: [ ... ]
}
Triggered after a function returns or throws an error. The returnOrThrow
property contains an object with two properties: type
tells you whether the
function returned normally or threw an error, and value
tells you the return
value or the error object that was thrown. The location
will be the same as
the 'enter'
event's location
.
{
type: 'leave',
location: { ... },
returnOrThrow: {
type: 'return' or 'throw',
value: ...
}
}
Statements containing blocks, such as if statements and loops, are handled differently than ordinary statements. For example, consider this while loop:
var x = 3;
while (x--) {
console.log(x);
}
Instead of instrumenting it like this:
var x = 3;
pencilTrace({type: 'before', ...});
while (x--) {
console.log(x);
}
pencilTrace({type: 'after', ...});
It's much more useful to instrument it like this:
var x = 3;
var _temp;
while (pencilTrace({type: 'before', ...}), _temp = x--, pencilTrace({type: 'after', ...}), _temp) {
console.log(x);
}
Here we instrument the conditional expression of the while loop. This way we
can show that the condition is being executed on every iteration, and we can
track how the value x
is being changed.