Debugging Decision Code

Warning

If you are embedding into a C++ program, it is very, very recommended that when you include a C header file, you encapsulate it with an extern block, like so:

extern "C" {
    #include <decision.h>
    #include <dsheet.h>
}

Introduction

As of the 0.3.0 update, you can debug Decision code through the C API! This means that when you compile Decision code in debug mode (more on this later), you can tell the debugger to tell you when:

  • A non-execution wire transfers a value from one socket to another, and what the value transfered was.

  • An execution wire activates.

  • A node activates.

  • A node calls a function or subroutine.

  • A function or subroutine returns.

You can also set breakpoints which, when hit, will pause the debugger.

  • Node breakpoints are hit when the node is activated.

  • Wire breakpoints are hit when:

    • A value is transfered if the wire is a non-execution wire.

    • The wire is activated if the wire is an execution wire.

Compiling in Debug Mode

In order to debug a sheet, you need to compile the sheet in debug mode. This will make a couple of changes to the compilation process:

  • When generating the bytecode, the code generator will also store debugging information alongside the bytecode, e.g. where a node starts in bytecode, which instructions call functions, etc.

  • Optimisation will no longer occur. This is because we don’t want to lose debugging information. For example, say there are two consecutive NOT instructions in the bytecode, made from two Not nodes in the Decision code. Optimisation would usually remove these instructions, but when debugging, we still want to know when those two Not nodes activate!

To compile a sheet in debug mode, you need to set the debug property in a CompileOptions structure:

CompileOptions options = DEFAULT_COMPILE_OPTIONS;
options.debug          = true;

// This sheet will now be compiled in debug mode.
Sheet *sheet = d_load_file("debug.dc", &options);

Debugging Agendas

Agendas describe what you want to happen when certain events occur during the debugging process. In code, they are in the form of function pointers:

typedef void (*OnWireValue)(Sheet *sheet, Wire wire, DType type, LexData value)

Called when a value is transfered over a wire during a debugging session.

typedef void (*OnExecutionWire)(Sheet *sheet, Wire wire)

Called when an execution wire is activated during a debugging session.

typedef void (*OnNodeActivated)(Sheet *sheet, size_t nodeIndex)

Called when a node is activated during a debugging session.

typedef void (*OnCall)(Sheet *sheet, const NodeDefinition *funcDef, bool isC)

Called when a call occurs during a debugging session.

typedef void (*OnReturn)()

Called when a return occurs during a debugging session.

Once you have functions in the above forms, you can then add them to a DebugAgenda structure:

DebugAgenda agenda      = NO_AGENDA;
agenda.onWireValue      = &onWireValue;
agenda.onExecutionWire  = &onExecutionWire;
agenda.onNodedActivated = &onNodeActivated;
agenda.onCall           = &onCall;
agenda.onReturn         = &onReturn;

Breakpoints

If you want to set breakpoints, you can specify where you want the breakpoints to be set:

DebugNodeBreakpoint nodeBreaks[] = {
    {sheet, 2}, // The 3rd node in the sheet.
    {NULL, 0}   // Array needs to end with a NULL entry!
};

DebugWireBreakpoint wireBreaks[] = {
    {sheet, {{2, 2}, {3, 1}}}, // The wire that goes from the 3rd socket of
                               // the 3rd node to the 2nd socket of the 4th
                               // node.
    {NULL, {{0, 0}, {0, 0}}}   // Array needs to end with a NULL entry!
};

// Don't forget to add them to the agenda!
agenda.nodeBreakpoints = nodeBreaks;
agenda.wireBreakpoints = wireBreaks;

Then, you can tell the debugger to let you know when the breakpoints are hit with the given function pointers:

typedef void (*OnNodeBreakpoint)(Sheet *sheet, size_t nodeIndex)

Called when a node breakpoint is hit.

typedef void (*OnWireBreakpoint)(Sheet *sheet, Wire wire)

Called when a wire breakpoint is hit.

Then you can add them to the agenda:

agenda.onNodeBreakpoint = &onNodeBreakpoint;
agenda.onWireBreakpoint = &onWireBreakpoint;

Debugging Sessions

To create a debugging session, use:

DebugSession d_debug_create_session(struct _sheet *sheet, DebugAgenda agenda)

Create a debugging session.

Return

A debugging session in it’s starting state.

Parameters
  • sheet: The sheet to debug.

  • agenda: The agenda the session should use.

You can then start/continue a debugging session with:

bool d_debug_continue_session(DebugSession *session)

Continue a debugging session until either a breakpoint is hit, or the VM halts.

Return

True if the debugger hit a breakpoint, false if the VM halted.

Parameters
  • session: The session to continue.

Tip

You can put d_debug_continue_session as the condition of a while loop! This way, the session will continue until the sheet exits, and you can process something after each breakpoint hit!

Once you are done debugging, you should use:

void d_debug_stop_session(DebugSession *session)

Stop a debugging session, freeing all of the memory malloc’d by the session. The session should not be used afterwards.

Parameters
  • session: The session to stop.

Partial Example

This example just prints out when nodes get activated.

#include <ddebug.h>
#include <decision.h>
#include <dsheet.h>

#include <stdio.h>

void onNodeActivated(Sheet *sheet, size_t nodeIndex) {
    Node node = sheet->graph.nodes[nodeIndex];
    const NodeDefinition *nodeDef = node.definition;

    printf("Debug> Node %s on line %zu in sheet %s has been activated!\n",
        nodeDef->name, node.lineNum, sheet->filePath);
}

int main() {
    // Load the sheet in debug mode.
    CompileOptions options = DEFAULT_COMPILE_OPTIONS;
    options.debug          = true;

    Sheet *sheet = d_load_file("debug.dc", &options);

    // Set up the debugging agenda.
    DebugAgenda agenda     = NO_AGENDA;
    agenda.onNodeActivated = &onNodeActivated;

    // Create the session.
    DebugSession session = d_debug_create_session(sheet, agenda);

    // Keep continuing the session until the sheet exits.
    while (d_debug_continue_session(&session)) {
        printf("Debug> A breakpoint was hit!\n");
    }

    // Once we're done, stop the session.
    d_debug_stop_session(&session);

    // Free the sheet.
    d_sheet_free(sheet);

    return 0;
}

To see a full example that uses all of the capabilities of the debugger, feel free to look at tests/c/debugging.c!