This is part two of a three part post about JavaScript REPL console for Windows. In part one I have explained motivation and some initial steps to get the interesting part – how make JavaScript to debug itself.
As mentioned previously, full source code of JSREPL is freely available under GPL 2 license on github.
Breakpoints
To me, the most essential capability of a debugger is to stop execution of the code in an arbitrary place and inspect the state of the program. Stepping through code, callstack, etc. is all secondary to breakpoints. For this reason, I have focused on getting the ability to stop the execution first.
The way to stop the execution is quite simple – just call a function (let’s name it ‘inner function), which runs REPL and does all the state inspections inside the REPL. However, due to the way JavaScript variable binding works, inside that inner function, variables can only bind to its scope and enclosing scopes through eval. One way to solve this problem is to declare a function inside the function I am trying to debug, but doing so by hand is quite tedious – I would have to do that every time I want to place a breakpoint.
The solution is to call the breakpoint function through eval
, which in turn declares an anonymous utility function to evaluate expressions in the scope of the function being debugged.
Figure 6. Breakpoint function.
1 function dbgBreak(msg) {
2 return "DBG.repl(\"" + msg.toString() +
3 "\" , function (v) { return eval(v); }, " +
4 "typeof arguments != \"undefined" && arguments != null ? arguments.callee : null)";
5 }
This code returns a string
, which when evaluated triggers a breakpoint. The breakpoint runs debug REPL (line two). There are three parameters passed onto debug REPL – a breakpoint label (so that I know which breakpoint triggered), an anonymous function, which evaluates expressions in the scope of the function being debugged, and a reference to the function object, if present.
The breakpoint in the source code looks like this:
Figure 7. Setting a static breakpoint.
1 eval(dbgBreak("BP1"));
When the following code is executed, it breaks into debug REPL.
Figure 8. Sample script to illustrate breakpoint.
1 function A(param) {
2 println("param=", param);
3 eval(dbgBreak("BP1"));
4 return param + 1;
5 }
Figure 9. Example of breakpoint.
1 Interactive JavaScript REPL console. Version 1.1
2
3 When no parameters are passed on command line, JSREPL starts
4 in interactive mode with the following commands available:
5 println(<expr>[, …]) – prints expressions to stdout with \n.
6 print(<expr>[,…]) – prints expressions to stdout without \n.
7 \ at the end of line – indicates continuation for multiline input.
8 load <file> – loads and executes file in current session.
9 list <fn> – list the source of the function. <fn> must
10 – be function name without parentheses.
11 bp <fn> <line> – inserts a breakpoint in function <fn> before
12 line <line>. Lines are zero-based.
13 bd <id> – removes breakpoint with id <id>.
14 bl – lists all breakpoints.
15 quit – terminates execution.
16
17 When a file name is passed as a first parameter on a command line,
18 it is executed in the context of this console and all arguments
19 are passed on to the script as is.
20
21 # load ..\bptest.js
22
23 # A(1)
24 param=1
25
26 dbg break – "BP1" ? – displays list of debugger commands.
27 dbg>
The commands and expressions I typed are highlighted in blue. At this point, we have a way to interrupt the execution, but we still need to complete debug REPL.
Debug REPL
Debug REPL has to be able to inspect the state of a program (we already made provisions for that in our breakpoint implementation). It also needs to display a call stack, list function source and resume execution. Listing function source is reusing the same code used in JavaScript REPL, described in part one, while resuming execution is simply returning from debug REPL. That leaves us with the call stack.
The third parameter passed onto the DBG.repl
method is a function object. We can easily navigate the call stack using its caller
property.
Figure 10. Stack walking function.
1 walkStack: function (fn) {
2 var stack = new Array();
3 while (fn != null) {
4 stack.push(this.parseFunction(fn));
5 fn = fn.caller;
6 }
7 return stack;
8 }
As before, this is a method of DBG object. The parseFunction()
method does a little trickery extracting the function name from the source of the function, as well as determines formal parameter names, then matches them with actual parameter values.
Figure 11. Parsing function source code.
1 parseFunction: function (fn) {
2 var fnText = fn;
3 if (typeof(fn) == "function") {
4 fnText = fn.toString();
5 }
6 var res = "<Global>";
7
8 fnText = fnText.replace(/\/\/.*(?=\r\n|\n\r|$)/g,"");
9 fnText = fnText.replace(/\/\*(?:\*(?!\/)|[^*])*\*\
10 fnText = fnText.replace(/\r\n|\n\r/g, "");
11
12
13 var r = fnText.match(/^\s*function(?:\s+(\w*)|)\s*\(/)
14 if (r != null) {
15 res = RegExp.$1 == "" ? "<anonymous>" : RegExp.$1.toString();
16 }
17
18
19 var reParamName = /\s*(\w*|"(?:\\"|[^"])*")\s*(,|\))/;
20 var params = new Array();
21 var lastIndex = RegExp.lastIndex;
22 while (lastIndex >= 0 && (r = fnText.substr(lastIndex).match(reParamName)) != null) {
23 lastIndex = RegExp.lastIndex != -1 ? lastIndex + RegExp.lastIndex : -1;
24 if (RegExp.$1.length > 0) {
25 params.push(RegExp.$1);
26 }
27 if (RegExp.$2 == ")") {
28 break;
29 }
30 }
31 return { name: res, params: params, func: fn};
32 }
Let’s see the code in action! I am using the following sample script to set the breakpoint.
Figure 12. Sample script to illustrate debug REPL.
1 function A(param) {
2 println("param=", param);
3 eval(dbgBreak("BP1"));
4 return param + 1;
5 }
6
7 function B(p1, p2, p3) {
8 return A(p2, p1, 20);
9 }
10
11 function C() {
12 return B("Text", 10);
13 }
14
When executed, debug REPL starts.
Figure 13. Debug REPL session log.
1 # load ..\bptest.js
2
3 # C(1, "a", null)
4 param=10
5
6 dbg break – "BP1" ? – displays list of debugger commands.
7 dbg> ?
8
9 dbg commands:
10
11 g – resume execution.
12 w – print call stack.
13 l <fun> – print source of function <fun>.
14 <fun> is either function name or
15 stack level (as displayed by w).
16 bl – list all breakpoints.
17 q – quit execution of the script.
18 <expr> – evaluate <expr> in the context of
19 the current function.n
20 dbg> w
21 Call stack:
22
23 Level Function
24 ~~~~~ ~~~~~~~~~~~~
25 0 A(param=10, "Text", 20)
26 1 B(p1="Text", p2=10, p3=)
27 2 C(1, "a", null)
28 dbg> l 2
29 0 function C() {
30 1 return B("Text", 10);
31 2 }
32 dbg> l 1
33 0 function B(p1, p2, p3) {
34 1 return A(p2, p1, 20);
35 2 }
36 dbg> l 0
37 0 function A(param) {
38 1 println("param=", param);
39 2 eval(dbgBreak("BP1"));
40 3 return param + 1;
41 4 }
42 dbg> param
43 10
44 dbg> param=20
45 20
46 dbg> g
47 21
48 #
49
At this point, the debugger has quite a bit of functionality, and while static breakpoints I have to set in my source code might look like tedious, the mechanism is also very powerful – I can set conditional breakpoints with ease.
Now if only I could set the breakpoints dynamically… Stay tuned for the next part.