Yacas supports a special form of evaluation where hooks are placed when evaluation enters or leaves an expression.
This section will explain the way custom evaluation is supported in Yacas, and will proceed to demonstrate how it can be used by showing code to trace, interactively step through, profile, and write custom debugging code.
Debugging, tracing and profiling has been implemented in the debug.rep/ module, but a simplification of that code will be presented here to show the basic concepts.
CustomEval(enter,leave,error,expression); |
Here, expression is the expression to be evaluated, enter some expression that should be evaluated when entering an expression, and leave an expression to be evaluated when leaving evaluation of that expression.
The error expression is evaluated when an error occurred. If an error occurs, this is caught high up, the error expression is called, and the debugger goes back to evaluating enter again so the situation can be examined. When the debugger needs to stop, the error expression is the place to call CustomEval'Stop() (see explanation below).
The CustomEval function can be used to write custom debugging tools. Examples are:
In addition, custom code can be written to for instance halt evaluation and enter interactive mode as soon as some very specific situation occurs, like "stop when function foo is called while the function bar is also on the call stack and the value of the local variable x is less than zero".
As a first example, suppose we define the functions TraceEnter(), TraceLeave() and TraceExp() as follows:
TraceStart() := [indent := 0;]; TraceEnter() := [ indent++; Space(2*indent); Echo("Enter ",CustomEval'Expression()); ]; TraceLeave() := [ Space(2*indent); Echo("Leave ",CustomEval'Result()); indent--; ]; Macro(TraceExp,{expression}) [ TraceStart(); CustomEval(TraceEnter(), TraceLeave(), CustomEval'Stop(),@expression); ]; |
allows us to have tracing in a very basic way. We can now call:
In> TraceExp(2+3) Enter 2+3 Enter 2 Leave 2 Enter 3 Leave 3 Enter IsNumber(x) Enter x Leave 2 Leave True Enter IsNumber(y) Enter y Leave 3 Leave True Enter True Leave True Enter MathAdd(x,y) Enter x Leave 2 Enter y Leave 3 Leave 5 Leave 5 Out> 5; |
This example shows the use of CustomEval'Expression and CustomEval'Result. These functions give some extra access to interesting information while evaluating the expression. The functions defined to allow access to information while evaluating are:
DebugStart():= [ debugging:=True; breakpoints:={}; ]; DebugRun():= [debugging:=False;]; DebugStep():=[debugging:=False;nextdebugging:=True;]; DebugAddBreakpoint(fname_IsString) <-- [ breakpoints := fname:breakpoints;]; BreakpointsClear() <-- [ breakpoints := {};]; Macro(DebugEnter,{}) [ Echo(">>> ",CustomEval'Expression()); If(debugging = False And IsFunction(CustomEval'Expression()) And Contains(breakpoints, Type(CustomEval'Expression())), debugging:=True); nextdebugging:=False; While(debugging) [ debugRes:= Eval(FromString( ReadCmdLineString("Debug> "):";") Read()); If(debugging,Echo("DebugOut> ",debugRes)); ]; debugging:=nextdebugging; ]; Macro(DebugLeave,{}) [ Echo(CustomEval'Result(), " <-- ",CustomEval'Expression()); ]; Macro(Debug,{expression}) [ DebugStart(); CustomEval(DebugEnter(), DebugLeave(), debugging:=True,@expression); ]; |
This code allows for the following interaction:
In> Debug(2+3) >>> 2+3 Debug> |
The console now shows the current expression being evaluated, and a debug prompt for interactive debugging. We can enter DebugStep(), which steps to the next expression to be evaluated:
Debug> DebugStep(); >>> 2 Debug> |
This shows that in order to evaluate 2+3 the interpreter first needs to evaluate 2. If we step further a few more times, we arrive at:
>>> IsNumber(x) Debug> |
Now we might be curious as to what the value for x is. We can dynamically obtain the value for x by just typing it on the command line.
>>> IsNumber(x) Debug> x DebugOut> 2 |
x is set to 2, so we expect IsNumber to return True. Stepping again:
Debug> DebugStep(); >>> x Debug> DebugStep(); 2 <-- x True <-- IsNumber(x) >>> IsNumber(y) |
So we see this is true. We can have a look at which local variables are currently available by calling CustomEval'Locals():
Debug> CustomEval'Locals() DebugOut> {arg1,arg2,x,y,aLeftAssign,aRightAssign} |
And when bored, we can proceed with DebugRun() which will continue the debugger until finished in this case (a more sophisticated debugger can add breakpoints, so running would halt again at for instance a breakpoint).
Debug> DebugRun() >>> y 3 <-- y True <-- IsNumber(y) >>> True True <-- True >>> MathAdd(x,y) >>> x 2 <-- x >>> y 3 <-- y 5 <-- MathAdd(x,y) 5 <-- 2+3 Out> 5; |
The above bit of code also supports primitive breakpointing, in that one can instruct the evaluator to stop when a function will be entered. The debugger then stops just before the arguments to the function are evaluated. In the following example, we make the debugger stop when a call is made to the MathAdd function:
In> Debug(2+3) >>> 2+3 Debug> DebugAddBreakpoint("MathAdd") DebugOut> {"MathAdd"} Debug> DebugRun() >>> 2 2 <-- 2 >>> 3 3 <-- 3 >>> IsNumber(x) >>> x 2 <-- x True <-- IsNumber(x) >>> IsNumber(y) >>> y 3 <-- y True <-- IsNumber(y) >>> True True <-- True >>> MathAdd(x,y) Debug> |
The arguments to MathAdd can now be examined, or execution continued.
One great advantage of defining much of the debugger in script code can be seen in the DebugEnter function, where the breakpoints are checked, and execution halts when a breakpoint is reached. In this case the condition for stopping evaluation is rather simple: when entering a specific function, stop. However, nothing stops a programmer from writing a custom debugger that could stop on any condition, halting at e very special case.
ProfileStart():= [ profilefn:={}; ]; 10 # ProfileEnter() _(IsFunction(CustomEval'Expression())) <-- [ Local(fname); fname:=Type(CustomEval'Expression()); If(profilefn[fname]=Empty,profilefn[fname]:=0); profilefn[fname] := profilefn[fname]+1; ]; Macro(Profile,{expression}) [ ProfileStart(); CustomEval(ProfileEnter(),True, CustomEval'Stop(),@expression); ForEach(item,profilefn) Echo("Function ",item[1]," called ", item[2]," times"); ]; |
which allows for the interaction:
In> Profile(2+3) Function MathAdd called 1 times Function IsNumber called 2 times Function + called 1 times Out> True; |
To build the debug version of yacas, run configure with
./configure --enable-debug |
and after that
make |
as usual.
When you build the debug version of yacas, and run a command, it will:
Future versions will have the ability to step through code and to watch local and global variables while executing, modifying them on the fly.