Custom evaluation facilities

# 7. 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.

##

7.1 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:

- a trace facility following entering and leaving functions
- interactive debugger for stepping through evaluation of an expression.
- profiler functionality, by having the callback functions do the bookkeeping on counts of function calls for instance.

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:

`CustomEval'Expression()` - return expression currently on the top call stack for evaluation.
`CustomEval'Result()` - when the `leave` argument is called this function returns what the evaluation of the top expression will return.
`CustomEval'Locals()` - returns a list with the current local variables.
`CustomEval'Stop()` - stop debugging execution

##

7.2 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.

##

7.3 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;

##

7.4 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:

- keep track of the memory allocated and freed, and show any memory leaks when you quit the program.
- show which files are loaded to read function definitions and when. This is only done when the
`--verbose-debug` flag is passed to the program at startup.
- keep a file name and line number for each object loaded from file, for debugging purposes.
- show you the stack trace when evaluation goes into an infinite recursion (equivalent of always using
`TraceStack`) and print file names and line numbers for all rules.

Future versions will have the ability to step through code and to watch local and global variables while executing, modifying them on the fly.