ZetCode

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.

fail.a68
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.

fail.a68
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.

sum.a68
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.

avg.a68
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.

sum.a68
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.

sum.a68
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.

loop.a68
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

The Algol 68 Genie project

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Algol 68 tutorials.