Algol 68 debugging
last modified November 14, 2026
A program that produces the wrong answer does so for one of a few reasons: a value was not what the author believed, a branch was taken that should not have been, or a loop ran one time too many. The Algol 68 Genie interpreter offers four ways to find out which: the runtime report that accompanies a crash, a stack backtrace of the frames that led to it, a trace of every unit that is evaluated, and an interactive debugger in which the program can be stopped and inspected.
This article starts from the report that a failed index check prints, adds a backtrace to it, follows a running program unit by unit, stops a program at a chosen line in the debugger, sets a watchpoint, and limits the running time of a loop that never ends.
Reading a runtime report
The interpreter checks bounds, arithmetic, and initialisation while the program runs. When a check fails it prints the source line, a marker under the offending expression, and a message, then ends the program with status 1. The report is the first debugging tool, because it names the line and the kind of check without any preparation from the programmer.
PROC inner = (INT n) VOID: ( []INT row = (1, 2, 3); print((row[n], newline)) ); PROC outer = (INT n) VOID: inner(n); BEGIN outer(5) END
The row has three elements and the call passes 5, so the subscript check
fails inside inner. The marker sits under the subscript
expression, and the message says that the index is out of bounds. Nothing was
printed before the failure, because the argument to print is
evaluated first.
$ a68g fail.a68
4 print((row[n], newline))
1
a68g: runtime error: 1: index out of bounds (detected in [] "SIMPLOUT" collateral-clause starting at "(" in this line).
Stack backtraces
The report names one line, but the mistake may have been made by the caller.
The --backtrace option makes the interpreter print every active
frame after the message: the line that was executing, the frame's size, its
parameters, and the values of its local variables. Backtracing is off by
default in this build.
PROC inner = (INT n) VOID: ( []INT row = (1, 2, 3); print((row[n], newline)) ); PROC outer = (INT n) VOID: inner(n); BEGIN outer(5) END
The first frame is the one that failed, with its row printed element by
element. The next frames show the callers: inner received 5 from
outer, and outer received 5 from the main block.
Reading upwards from the failure to the call site is how the offending
argument is found. The addresses and frame numbers in the transcript depend
on the build; the values do not.
$ a68g --backtrace fail.a68
Stack backtrace
3 []INT row = (1, 2, 3);
-
Stack frame 6 at frame(512), level=6, size=80 bytes
Dynamic link=frame(448), static link=frame(448), parameters=frame(448)
Procedure frame=no
In main thread
frame(568=512+56) [] INT "row", 3 element(s)
[1] +1
[2] +2
[3] +3
3 element(s) written (100%)
1 PROC inner = (INT n) VOID:
-
Stack frame 5 at frame(448), level=5, size=64 bytes
Dynamic link=frame(384), static link=frame(384), parameters=frame(448)
Procedure frame=yes
In main thread
frame(504=448+56) INT "n" +5
7 PROC outer = (INT n) VOID:
-
Stack frame 4 at frame(384), level=5, size=64 bytes
Dynamic link=frame(328), static link=frame(112), parameters=frame(384)
Procedure frame=yes
In main thread
frame(440=384+56) INT "n" +5
11 outer(5)
-
Stack frame 3 at frame(328), level=5, size=56 bytes
Dynamic link=frame(112), static link=frame(112), parameters=frame(0)
Procedure frame=no
In main thread
Tracing execution
A program that does not crash needs a different tool. The
--trace option prints each unit as the interpreter evaluates it,
with the source line, a marker under the unit, and the name of the internal
routine that runs it. The trace shows the order of evaluation, which is not
always the order in which the lines are written.
BEGIN
INT total := 0;
FOR i TO 4 DO
total +:= i * i
OD;
print(("total = ", whole(total, 0), newline))
END
The first line of the trace reports the sizes of the runtime areas. The loop body appears eight times, twice for each of the four iterations: once for the assignment and once for the voiding of the assignment's value, which is what a serial clause does with a unit that is not last. The printed line is then evaluated in pieces, argument by argument, before the output appears. The transcript below is cut after the first two iterations and the first arguments of the call.
$ a68g --trace sum.a68
genie: frame stack 6144k, expression stack 2048k, heap 49152k, handles 8192k
BEGIN MODE DOUBLE = LONG REAL, QUAD = LONG LONG REAL;
-
1 BEGIN
-
genie_unit
2 INT total := 0;
-
genie_unit
2 INT total := 0;
-
genie_unit
3 FOR i TO 4 DO
-
genie_unit
4 total +:= i * i
-
genie_unit
4 total +:= i * i
-
genie_voiding
4 total +:= i * i
-
genie_voiding
4 total +:= i * i
-
genie_voiding
6 print(("total = ", whole(total, 0), newline))
-
genie_unit
6 print(("total = ", whole(total, 0), newline))
-
genie_unit
The trace ends with the program's own output, printed immediately after the name of the unit that produced it. A trace of a long program is large, so the option is most useful on a reduced case that still shows the fault.
$ a68g --trace sum.a68 | tail -1 genie_unittotal = 30
Stopping in the debugger
The --debug and --monitor options start the
interactive debugger, and in this build they behave the same way. The program
stops at a temporary breakpoint before its first unit, and if a runtime error
occurs later the debugger is entered again with the message
Monitor entered after an error. From the prompt the commands set
breakpoints, run the program, and inspect variables.
BEGIN
[]INT data = (4, 8, 15, 16, 23, 42);
INT sum := 0;
FOR i TO UPB data DO
sum +:= data[i]
OD;
INT count := UPB data - LWB data;
INT avg = sum OVER count;
print(("average ", whole(avg, 0), newline))
END
The program computes an average, but it divides by
UPB data - LWB data, which is one less than the number of
elements. It prints 21 instead of 18. A breakpoint on the line that divides
stops the program before the wrong value is used, and frame
prints every variable in the main block.
$ a68g --debug avg.a68
Execution begins ...
Temporary breakpoint (now removed)
1 BEGIN
-
This is the main thread
(a68g) breakpoint 8
(a68g) continue
Continuing ...
Breakpoint
8 INT avg = sum OVER count;
-
This is the main thread
(a68g) frame
2 []INT data = (4, 8, 15, 16, 23, 42);
-
Stack frame 3 at frame(168), level=5, size=152 bytes
Dynamic link=frame(112), static link=frame(112), parameters=frame(0)
Procedure frame=no
In main thread
frame(224=168+56) REF INT "count" refers to frame(304)
frame(248=168+80) REF INT "sum" refers to frame(312)
frame(272=168+104) INT "avg" uninitialised value
frame(280=168+112) [] INT "data", 6 element(s)
[1] +4
[2] +8
[3] +15
[4] +16
[5] +23
[6] +42
6 element(s) written (100%)
frame(304=168+136) LOC INT +5
frame(312=168+144) LOC INT +108
The frame lists count as a reference to a cell holding 5, while
data has six elements. The bug is visible without stepping
further. The commands x and evaluate print a single
expression, and next advances to the next unit without entering
a procedure call, which is useful when the next line calls one.
(a68g) x count
(REF INT) refers to frame(304)
(INT) +5
(a68g) x sum
(REF INT) refers to frame(312)
(INT) +108
(a68g) next
Continuing ...
Temporary breakpoint (now removed)
9 print(("average ", whole(avg, 0), newline))
-
This is the main thread
(a68g) continue
Continuing ...
average 21
Genie finished in 0.00 seconds
Watchpoints
A watchpoint stops the program whenever an expression becomes true, so the
program does not have to be stopped at a known line. The expression is given
after breakpoint watch and is written without quotes; quoting it
makes it a string, and the debugger deletes the watchpoint with an error
about the mode.
BEGIN
INT total := 0;
FOR i TO 4 DO
total +:= i * i
OD;
print(("total = ", whole(total, 0), newline))
END
The watchpoint is set after the first breakpoint, when total has
a value. From then on the debugger stops before every unit at which the
expression is true, so it reports the loop body that pushed the total past
10 and again the print line. A watchpoint on a variable that has not been
initialised yet stops the program with an uninitialised-value error instead.
$ a68g --debug sum.a68
(a68g) breakpoint 3
(a68g) continue
Breakpoint
3 FOR i TO 4 DO
-
This is the main thread
(a68g) x total
(REF INT) refers to frame(248)
(INT) +0
(a68g) breakpoint watch total > 10
(a68g) continue
Watchpoint (total > 10)
4 total +:= i * i
-
This is the main thread
(a68g) x total
(REF INT) refers to frame(248)
(INT) +14
(a68g) continue
Watchpoint (total > 10)
6 print(("total = ", whole(total, 0), newline))
-
This is the main thread
(a68g) x total
(REF INT) refers to frame(248)
(INT) +30
The phases of a run
The --verbose option prints the phases that the interpreter
passes through before the program starts: tokenising, preprocessing, four
parser phases, the mode table, the symbol table, the mode checker, the
coercion enforcer, the application checker, the static scope checker, and the
portability checker. A phase that does not appear, or an error located in one
of them, tells which part of the front end rejected the program.
BEGIN
INT total := 0;
FOR i TO 4 DO
total +:= i * i
OD;
print(("total = ", whole(total, 0), newline))
END
The list ends with genie, the phase that evaluates the tree, and
then the program's own output follows. When the interpreter is asked to
compile with -O2, the same list gains the code generator, the
code compiler, and the calls to the C compiler and the linker.
$ a68g --verbose sum.a68 a68g: initialiser a68g: tokeniser a68g: preprocessor a68g: parser phase 1 a68g: standard environ builder a68g: parser phase 2 a68g: parser phase 3 a68g: mode table builder a68g: symbol table builder a68g: parser phase 4 a68g: mode checker a68g: coercion enforcer a68g: application checker a68g: static scope checker a68g: portability checker a68g: genie total = 30
Stopping a runaway program
A loop whose condition never becomes false hangs the terminal. The
--timelimit option interrupts the interpreter after the given
number of seconds and reports the loop that was executing, with the same
marker as any other runtime error.
BEGIN
INT n := 0;
WHILE TRUE DO
n +:= 1
OD
END
The run below was limited to two seconds. The program ends with status 1, the line and marker identify the loop, and the message names the time limit. The same option is useful in a test script, where a hang is worse than a failure.
$ a68g --timelimit 2 loop.a68
4 n +:= 1
1
a68g: runtime error: 1: time limit exceeded (detected in VOID loop-clause starting at "WHILE" in line 3).
$ echo $?
1
Choosing a tool
The runtime report is always available and costs nothing, so it answers most
questions about a crash. --backtrace adds the call frames when
the failing line is in a procedure that many callers use, and
--trace shows the order of evaluation when the values are right
but the sequence is not. The debugger is the tool for a wrong value that
does not crash: a breakpoint and one frame command often replace
a dozen print statements.
Two related options complete the set. --check, described in the
error handling article, parses a file
without running it. --debug and --monitor keep the
debugger available after a runtime error, so the state at the failure can be
inspected instead of printed. The listings
article covers the tree and mode reports that the interpreter can write for a
program that runs correctly.
Source
Revised Report on the Algorithmic Language Algol 68
In this article we read a runtime report, added a stack backtrace, traced a program unit by unit, stopped a program at a breakpoint, inspected its frames, set a watchpoint, listed the phases of a run, and limited the running time of a loop.
Author
List all Algol 68 tutorials.