Algol 68 scripts and compilation
last modified November 14, 2026
The interpreter has two ways to run a program. By default it parses the source,
builds the tree, and walks it, which starts in milliseconds and needs no
toolchain. With -O0 to -O3 it translates the tree to
C, hands the file to the system C compiler, loads the resulting shared object,
and runs that instead, which takes longer to start but executes loops several
times faster.
This article runs a program both ways, turns a source file into a command with a shebang, splits a program across two files, follows the C compiler through the artefacts it leaves, reuses the compiled code, and measures the difference. It ends with the checks that compilation quietly removes.
Scripts with a shebang
A file whose first line names the interpreter can be executed directly. The
line starts with #! and gives the path of a68g;
/usr/bin/env finds it on the PATH rather than fixing
one installation directory. The interpreter skips that line, so the same file
also runs under a68g explicitly.
#!/usr/bin/env a68g
BEGIN
print(("hello from a script", newline))
END
The execute bit is what makes the kernel run the file, and the shebang is
what tells the kernel which program to start. Without the bit the shell
reports a permission error, and without the shebang the kernel treats the
file as a shell script and fails on BEGIN.
$ chmod +x hello.a68 $ ./hello.a68 hello from a script $ a68g hello.a68 hello from a script
Splitting a program across files
A program of a few hundred lines is easier to keep in several files. The
pragma PR read "lib.a68" PR inserts the contents of another file
at that point, before the parser looks at the enclosing unit, so the reading
file sees the declarations of the read file as if they were written in place.
PROC average = ([]INT values) REAL:
(
INT sum := 0;
FOR i FROM LWB values TO UPB values DO
sum +:= values[i]
OD;
sum / (UPB values - LWB values + 1)
);
PROC largest = ([]INT values) INT:
(
INT max := values[LWB values];
FOR i FROM LWB values + 1 TO UPB values DO
IF values[i] > max THEN max := values[i] FI
OD;
max
);
PR read "lib.a68" PR
BEGIN
[]INT data = (4, 8, 15, 16, 23, 42);
print(("count ", whole(UPB data - LWB data + 1, 0), newline));
print(("largest ", whole(largest(data), 0), newline));
print(("average ", fixed(average(data), 0, 2), newline))
END
The path is relative to the working directory, not to the file that contains
the pragma. Running the program from the directory that holds both files
works; running it from elsewhere with an absolute path fails, because
lib.a68 is then looked for in the current directory.
$ a68g report.a68 count 6 largest 42 average 18.00 $ cd /tmp $ a68g /tmp/opencode/a68b/proj/report.a68 a68g: 1: error while opening source file "lib.a68" (no such file or directory) (detected at character "r").
Compiling through C
The -O2 option turns on compilation and passes the level to the C
compiler. The interpreter writes a C file next to the source, compiles it to
an object file, links a shared object with the same stem, loads it, and runs
it. Only the shared object remains afterwards; the intermediate files are
removed unless --keep is given.
PR read "lib.a68" PR
BEGIN
[]INT data = (4, 8, 15, 16, 23, 42);
print(("count ", whole(UPB data - LWB data + 1, 0), newline));
print(("largest ", whole(largest(data), 0), newline));
print(("average ", fixed(average(data), 0, 2), newline))
END
The output is the same as the interpreted run, because the compiled program
computes the same values. The generated C file is readable and shows how each
Algol 68 unit became a C function call, which makes --keep useful
when a program behaves differently under compilation.
$ a68g -O2 report.a68 count 6 largest 42 average 18.00 $ ls report.* report.a68 report.so $ a68g -O2 --keep report.a68 $ ls report.* report.a68 report.c report.so
--verbose prints the front-end phases and then the exact commands
that were run, so the C compiler and the linker are visible with their flags.
The two lines below are the whole back end for this program.
$ a68g -O2 --verbose report.a68 ... a68g: optimiser (code compiler) gcc -I. -I/usr/include -O2 -g -fPIC -c -o "report.o" "report.c" ld -export-dynamic -shared -o "report.so" "report.o" a68g: dynamic linker a68g: genie count 6 largest 42 average 18.00
Reusing the compiled code
The shared object is not a standalone program: it is a library that the
interpreter loads. Running it directly does not work, because the C file has
no main and expects the interpreter's runtime. Its advantage is
that it can be loaded again without the C compiler, which is what
--rerun does.
BEGIN
INT total := 0;
FOR i TO 20000000 DO
total +:= i MOD 7
OD;
print((whole(total, 0), newline))
END
The first compiled run pays for gcc and ld; the
second loads the existing shared object and skips both. If the shared object
is missing, --rerun stops with an error rather than falling back
to the interpreter.
$ a68g -O2 bench.a68 60000003 $ a68g --rerun bench.a68 60000003 $ rm bench.so $ a68g --rerun bench.a68 a68g: exiting: source/a68g.c: 989: cannot stat, ./bench.so (no such file or directory)
Measuring the difference
The loop in bench.a68 runs twenty million additions and
remainders, which is long enough to measure and short enough to wait for. The
times below were taken on one machine with /usr/bin/time, three
runs per mode; they include the start-up cost, and the compiled times include
the C compiler.
BEGIN
INT total := 0;
FOR i TO 20000000 DO
total +:= i MOD 7
OD;
print((whole(total, 0), newline))
END
The interpreted run takes about seven times as long as the compiled one. Even
-O0, which asks the C compiler for no optimisation, is more than
twice as fast as the tree walker, because the C compiler still turns each
unit into machine code. -O2 and -O3 are
indistinguishable here: the loop is already simple and the optimiser has
nothing left to remove.
$ /usr/bin/time -f "%e s" a68g bench.a68 60000003 1.26 s $ /usr/bin/time -f "%e s" a68g -O0 bench.a68 60000003 0.47 s $ /usr/bin/time -f "%e s" a68g -O1 bench.a68 60000003 0.22 s $ /usr/bin/time -f "%e s" a68g -O2 bench.a68 60000003 0.18 s $ /usr/bin/time -f "%e s" a68g -O3 bench.a68 60000003 0.18 s
What compilation removes
The speed comes at a price that is easy to miss, because the interpreter does not announce it. The compiled program does not check row bounds and does not check that a variable was initialised. An out-of-bounds read returns whatever the memory holds, and the program continues as if nothing happened.
BEGIN []INT row = (1, 2, 3); print((row[5], newline)) END
Interpreted, the same file stops with "index out of bounds" and status 1. Compiled, it prints whatever the memory beyond the row holds and returns success; the value changes from run to run and from one optimisation level to the next. The check is not moved or deferred: it is absent from the generated code.
$ a68g oob.a68
3 print((row[5], newline))
1
a68g: runtime error: 1: index out of bounds (detected in [] "SIMPLOUT" collateral-clause starting at "(" in this line).
$ echo $?
1
$ a68g -O2 oob.a68
-583554550
$ echo $?
0
Division by zero loses its message as well. The compiled program divides with
the hardware instruction, so the process ends with a floating point exception
and the shell reports signal 8, which appears as status 136. Assertions are
the exception to the rule: ASSERT is compiled in and still stops
the program with "false assertion".
BEGIN INT z := 0; print((10 OVER z, newline)) END
The practical rule is to develop and test with the interpreter, where every check runs, and to compile only code that has passed those tests. A compiled program should also be given a time limit when it processes input that the author does not control, because a wrong index no longer stops it.
$ a68g dz.a68
3 print((10 OVER z, newline))
1
a68g: runtime error: 1: attempt at INT division by zero (numerical result out of range) (detected in [] "SIMPLOUT" collateral-clause starting at "(" in this line).
$ a68g -O2 dz.a68
Floating point exception (core dumped)
$ echo $?
136
What compilation does not change
The language and the output are the same in both modes. The prelude is still there, formatted transput still rounds the same way, the garbage collector still runs, and the shared object is tied to the machine and the C library that produced it. Compiling is a way to run a program faster, not a way to package it for another computer.
The debugging article shows the interpreter options that help when the compiled and interpreted runs disagree, and the unit testing article shows the harness that should pass before a program is compiled.
Source
Revised Report on the Algorithmic Language Algol 68
In this article we made a source file executable with a shebang, split a
program with PR read, compiled it through C, inspected the
artefacts and the compiler commands, reused the shared object with
--rerun, measured a sevenfold speed-up, and found the runtime
checks that compilation removes.
Author
List all Algol 68 tutorials.