Custom evaluation facilities

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.


The basic infrastructure for custom evaluation

The name of the function is CustomEval, and the calling sequence is:

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:


A simple interactive debugger

The following code allows for simple interactive debugging:

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.


Profiling

A simple profiler that counts the number of times each function is called can be written such:

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;


The Yacas Debugger


Why introduce a debug version?

The reason for introducing a debug version is that for a debugger it is often necessary to introduce features that make the interpreter slower. For the main kernel this is unacceptable, but for a debugging version this is defendable. It is good for testing small programs, to see where a calculation breaks. Having certain features only in the debug version keeps the release executable can be kept lean and mean, while still offering advanced debug features.


How to build the debug version of Yacas ?

The debug version has to be built separately from the "production" version of Yacas (all source files have to be recompiled).

To build the debug version of yacas, run configure with

./configure --enable-debug

and after that

make

as usual.


What does the debug version of yacas offer?

The Yacas debugger is in development still, but already has some useful features.

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.