Here I will describe a way to use the current jsdIDebuggerService
API in Mozilla to debug dynamically
evaluated Javascript. The Mozilla Javascript engine (SpiderMonkey)
and its debugger API were not design to support eval() debugging.
Dynamically
evaluated code appears in the debugger as mysterious or non-existent lines in
files containing the eval() call. Single stepping and breakpoints are
largely not usable. This means debuggers fail for dynamic code. Since
Web 2.0 relies on client-side analysis of server-provided data
(eg JSON and AJAX) and, increasingly, on
client-side specification and download of server-provided code (eg Dojo),
eval() is increasingly important. The problem is documented in Bugzilla as
Bug #307984.
We need a fix.
payload.js:
payload.js is as follows:
Error: b is not defined
However the error will not be reported at line 4 of payload.js, but
rather at line 8 of dynloader.js. Changing lines in
dynloader.js around line 8 won't change the error message or its
reported location, though adding lines above line 5 will.
While this seems at first to be a Javascript implementation error, an alternative implementation is not at all obvious. The error in this case does not occur at a "file" and "line"; it occurs at a line in a buffer created dynamically by the programmer's code. A more accurate error message would give the line as 5 but report the file as "unknown". This would not be more helpful to programmers. The current solution reports the error one line before the line in the file containing the "eval()" call (line 4 in dynloader.js) plus the line number of the error in the eval buffer (line 4 in payload.js) giving line 8.
A run-time debugger can be more helpful by reporting the buffer of source
code containing the error and the line of the error relative to that buffer.
In principle this is the job of the Javascript engine, but changing that code and
distributing it is hard. Here I will show how the Javascript
debugger interface,
jsdIDebuggerService can be used to support eval() debugging.
(I'll call this interface jsd for short).
To give a developer better information on the source of errors in eval()
buffers we need to
eval() buffers. The third is required because the
SpiderMonkey engine records line numbers relative to the file of the source that
calls eval() rather than relative to the source of the error.
<script> tags or
Javascript functions. The jsd documentation mentions "top-level" scripts, which are blocks
of source, so I will use the term "leveled scripts" to refer to script objects representing
source. The other kind of leveled script is an "eval-level" script, representing the
source text evaluated dynamically by eval(). Leveled scripts define objects
in the scope of their caller: top-level scripts define global objects like window properties
while eval-level scripts define objects in the function that calls the eval().
All other scripts are "unleveled", meaning they are Javascript functions that create a new
scope. Unleveled scripts always come from leveled scripts: during the compilation of
leveled scripts, unleveled scripts are created. Any given script can be one of four types:
html
or js file). The eval-level scripts and unleveled scripts from eval-level are
mapped to the url of top-level script or unleveled script from top-level that called the eval().
The line numbers simply start at the point of the eval() call.
When debuggers use this information, users see the debugger step through lines of source
in the top-level file that do not make sense.
jsd refer to the jsdIScript interface of
scripts; programmers think in terms of source.
Therefore to debug eval() code we need to know the eval-level scripts and their source.
To begin our fix-up we need to get control when an eval() is called. There is no specific
call back into the debugger API for eval(). Empirically, a sign for eval() is a
stack frame with 1) an empty string as its function name and 2) a calling-frame above it on the stack.
We'll call these frames "eval-level".
(An empty string as a function name without a calling-frame is a "top-level" frame).
The jsd
scriptHook
allows us to see each function after it is compiled so we can test each one to see if it has an empty
string function name; scripts that fail to compile never trigger the scriptHook.
eval() body trigger
jsd.errorHook; if compile succeeds
jsd.scriptHook.onScriptCreated is called. That gives us control to test the function name
for blank names.
However, neither jsd.errorHook nor
jsd.scriptHook.onScriptCreated are passed the call stack, so we cannot
analyze for the caling-frame.
Therefore along both paths we have to pass control to a second debugger hook, one that
is passed the stack.
Along the compile-error path we return
false from jsd.errorHook. This tells the Javascript engine to
call the jsd.debugHook of type jsdIExecutionHook.
We do the rest of the work in jsd.debugHook.
Here we examine the stack (the stack appears as if the jsd.errorHook function
was called at the point of the error). The stack is passed to our errorHook as a Javascript object
of type jsdIStackFrame; this is the top frame of the stack (up is newer frames).
Each frame has a script property of type jsdIScript representing
the active function in that frame. The functionName property of the script object
identifies the function.
This character string is commonly "anonymous" for today's Javascript styles, but the value for eval based frames
seems to be a string with no characters "" (empty-string).
According to the documentation, top-level functions
also have empty names (meaning zero length but not null strings).
Top-level functions contain Javascript inside script
tags but outside any Javascript functions.
These top-level un-named functions are easy to distinguish from un-name eval-level functions
by simply seeing if the frame
containing the empty-string-named function is top-most or not.
(We don't know if other functions may also give
empty-string for functionName, but it seems
unlikely).
For run-time correction we need to trap the eval() before it runs and mark all of the functions that it defines as coming
from the its expression body. We can trap the eval() by looking for scripts (i.e. functions) with empty-string names
in a jsd.scriptHook.onScriptCreated. We need to distinguish eval-level from top-level functions, but
onScriptCreated has no stack to check. Therefore when we see an empty-string-named function, we set a breakpoint on it at
PC (program counter) zero triggering a call to jsd.breakpointHook.
(Such breakpoints cannot interfere with the operation of the conventional debugger
since the conventional debugger cannot see the eval code anyway.) As soon as we return from onScriptCreated, the function runs
and breaks immediately into our jsd.breakpointHook; we can undo our breakpoint. We now have the stack frame of the just-running
empty-string function. If it has a caller, it must be an eval-level
function.
We come back to the problem of marking all of eval-defined functions when we look at saving the source of the eval body.
errorHook for errors and onScriptCreated for script creation. We use
the first hook to call the second hook. In each case the second hook is an jsdIExecutionHook that gets passed a stack.
We can look through this stack for a frame with a script named with the empty-string and test if this frame is top or not.
In the latter case we have an eval-level frame.
expr in eval(expr). Fortunately
jsdIStackFrame provides us with a key tool: it supports a special eval() call. We can
use it to get the source code by passing "var src = new String("+evalExpr+")" and storing the result
src as our source. This is all that is needed for the compile path.
For the run-time path we need to know when functions were created by our eval().
Empirically all unleveled scripts are emitted from the javascript engine as their leveled script is compiled.
Also empirically, a leveled script is compiled completely before any other action by the engine.
So from the debugger's point of view, unleveled scripts start appearing in the onScriptCreated
callback. Then, when the leveled script compilation is complete, it appears in the onScriptCreated
callback. We can queue all the scripts until the leveled script appears. If this leveled script is top-level,
we discard the queue. If the leveled script is eval-level, then we create a map assigning each queued unleveled script
to the corresponding eval-level script. By consulting this table instead of the built-in mapping of
scripts to (erroneous) points in the top-level file, the debugger can correctly represent the state of
the machine to programmers.
foo in var foo = function() {...};).
What name shall we assign to eval-level scripts?
In
Bug #307984 the proposal was to name eval code via
a data: URI that encoded the source code and call site in the URI. This has the advantage that possession
of the URI ensures the possession of the source and the name is as unique as the source. It has two disadvantages,
the programmer gets no clues from the name as to the origin of the source and for systems like dojo the URI can be
quite large and numerous.
The double-hook technique allows us to report compile and run-time errors to programmers directly against the lines of code. This is a significant improvement over the current state. Ideally we would also like to apply the complete debugger functionality to the eval code, including single stepping and setting breakpoints. Breakpoints in jsd are set on scripts, but the eval-level and unleveled scripts from eval-level are deleted when they go out of scope. So the debugger has to reset breakpoints in the new script objects created for eval-level source each time we go through a debug-cycle.
At this point we debug eval code based on dynamic buffers. However developers work with code in files. Can we get closer?
In general, analysis of eval bodies requires dynamic (run-time) dependency analysis to allow
backtracking to source. This is much more work, but we might be able to make progress with some heursitics
for important special cases.
For special eval-ed code that is dynamically loaded by XMLHttpRequest we would like to identify the
the URL that was loaded to get the eval body, relate the URL to a file, and flag the source line.
Further investigations will be needed to see what is possible along these lines.
nsIStackFrame returned by Components.stack
is a weaker cousin of the jsdIStackFrame. The latter gives access
to the jsdIScript running in the frame, the this object, and
the scope object of the frame.