Debugging and tracing

The tracing facility

Elpi’s interpreter is instrumented. Each traced event is one application of a rule of Formal semantics: curgoal marks the goal now active, rule the backchain step that picks a clause for it, newgoal the premises that step pushes, assign an extension of the substitution, new-hyps and new-quant the ==> and pi steps. The operational semantics rewrites a single configuration and keeps no call stack, so the event stream is flat and linear; a separate elaborator rebuilds the goal tree from the parent/child links in the stream, and that reconstructed tree is what the interactive browser shows.

elpi prog.elpi -test -trace-on -trace-at run 1 9999 -trace-only 'user:' prints one line per traced event (a goal selected (run), a rule tried (select), a variable assigned (assign), and more) between step 1 and step 9999 of the run trace point:

$ elpi prog.elpi -test -trace-on -trace-at run 1 5 -trace-only 'user:'
  rid:0 step:1 gid:4 user:curgoal = main ...
  rid:0 step:1 gid:4 user:rule = backchain
  rid:0 step:1 gid:5 user:newgoal = double 5 X0
  ...

-trace-only-pred REX narrows the trace to goals matching a predicate name; -trace-skip REX excludes matching items instead. Events prefixed user: come from the program; dev: ones are for debugging Elpi itself. trace.counter "NAME" N reads a named counter ("run" counts solved goals) for a program to condition its own debug output on, the way std.spy does.

elpi prog.elpi -test -trace-on json FILE -trace-at run 1 9999 writes a machine-readable trace instead of a text one. elpi-trace-elaborator, a separate binary shipped with Elpi, reads such a trace from standard input and groups it into “cards”, one per step with its rule and its subgoals; the VS Code extension’s trace browser (Getting started) displays them and steps through them interactively, rather than as a flat log:

The VS Code trace browser, with a column of cards on the left (one per traced step, each naming its rule and predicate) and, on the right, the successful attempt and subgoals of the step currently selected.

The trace browser.

The spy predicate

std.spy G runs G and prints it on entry, and again on exit or on failure, through std.debug-print, so it is subject to the same overriding as any other library hook (The standard library). std.spy! is the same with a cut, for a goal that should only ever succeed once.

code/spy-tour.elpi:

1% std.spy prints a call's arguments on entry and on exit (or failure),
2% without needing any command-line flag.
3
4pred double int -> int.
5double X Y :- Y is X * 2.
6
7main :- std.spy (double 5 R), print "result:" R.
----<<---- enter:  double 5 X0
---->>---- exit:  double 5 10
result: 10

Debugging the parser

elpi -print-ast FILE prints a program as parsed, before any compilation pass; elpi -print FILE prints it after most of them, spilling (Spilling) included, so any question about how a particular piece of surface syntax desugared can be checked directly. elpi FILE -deps prints the accumulate graph of FILE and everything it pulls in, as a Graphviz digraph, useful for untangling a large project’s file structure (File structure and attributes).

Conditional debug rules

A debug rule can be removed from the program entirely, rather than merely kept quiet: :if "NAME" (Rule attributes) drops a rule at compile time unless NAME is defined with elpi -D NAME, so a trace-style helper costs nothing when debugging is off, instead of running and choosing not to print:

code/conditional.elpi:

 1% A debug-only rule: present in the program, but only fires when the
 2% compiler variable DEBUG is defined (run with `elpi conditional.elpi -test
 3% -D DEBUG` to see it fire).
 4
 5func trace string.
 6
 7:if "DEBUG"
 8trace Msg :- print "[debug]" Msg, !.
 9trace _.
10
11main :- trace "checkpoint 1", print "done".
done

Compiled plain, the guarded rule is gone and trace falls through to the catch-all, printing nothing; -D DEBUG compiles it back in:

$ elpi conditional.elpi -test -D DEBUG
[debug] checkpoint 1
done