Algol*24

Algol*24

A retro-modern language: classic Pascal syntax over unbounded integers, full Unicode, gradual types, closures and a foreign function interface. The compiler is written in Algol-24 and compiles itself.

v0.1.4 · pre-1.0 · on Homebrew

class Doughnut;
begin
    procedure Cook();
    begin
        WriteLn ('Fry until golden!');
    end
end

class BostonCream(Doughnut);
begin
    procedure Cook();
    begin
        super.Cook();
        WriteLn ('Pipe full of custard and coat with chocolate!');
    end
end

BostonCream().Cook();

Install

It is on Homebrew.

$ brew tap schildawg/algol24
$ brew trust schildawg/algol24
$ brew install algol24

The middle line is required rather than advisory: Homebrew will not load a formula from a third-party tap until you have said that you trust it.

$ algc --version
algc 0.1.4

The formula compiles foreign calls in and links against the libffi that macOS ships, so the installed compiler depends on nothing but the operating system. The specification and the library reference are installed beside it.

Bottles are built for Apple Silicon. Anywhere else — an Intel Mac, Linux — Homebrew builds from source, which needs a C compiler and takes about two seconds. There is no Windows build yet.

The build

It compiles itself.

What is checked in is the compiler's own output. bootstrap/ holds the C that Algol-24 emitted when it last compiled itself, so building needs a C compiler and nothing else — no toolchain, no package manager, no bootstrap binary to trust.

// two stages: the seed builds with cc, then compiles the real thing
$ ./bootstrap/build.sh
$ algc --compile --out=build hello.a24
$ cc -std=c11 -O2 -o hello build/*.c

The build then diffs stage two's output against the checked-in seed. They must be identical. A difference means the seed is stale, which is exactly what you expect after editing the compiler — and --reseed is how you say so deliberately rather than by accident.

The language

Types are optional, and checked when written.

An unannotated name is Any, compatible with everything in both directions. Writing no type is never itself an error, and adding one can only add checking — so annotating more is always safe.

var Name := 'Ada';              // inferred
var Count : Integer := 0;       // declared
const Limit := 100;             // cannot be reassigned

function Add (A : Integer, B : Integer) : Integer;
begin
    Exit A + B;
end

Semicolons terminate rather than separate, so the one before end is required. Exit returns; there is no return. Functions are values, they nest, and they close over what they see.

The capability

Classic on the surface, not underneath.

An Integer is unbounded: arithmetic past the machine width grows rather than wrapping or raising, and demotes again the moment a result fits. There is no build switch. Source, identifiers and text are Unicode throughout, and text is measured in characters rather than bytes — so Length ('你好') is 2.

class Point;
var
    X : Integer;
    Y : Integer;

begin
    operator + (Other : Point) : Point;
    begin
        Exit Point (X + Other.X, Y + Other.Y);
    end
end

Classes with operators a program may define, closures and nested subprograms, exceptions, enumerations, five collections, modules including circular ones, and a foreign function interface that reaches C — so a program can call SDL or libm directly. The examples draw a pie chart and the word hello in five scripts, with color emoji, into an SDL2 window.

The guarantee

The specification is the authority.

Where the implementation and the specification disagree, it is the implementation that is wrong. The difference is recorded as a defect and carries a program that reproduces it.

284rules, each claimed by a case
232unit tests in Algol-24
245cases, under both processors

Every case runs twice — interpreted and compiled — and both halves must be green. A case the interpreter gets right and the compiled program gets wrong is a gap, and a gap is a failure rather than a note. A defect's test is stranger still: it records the wrong behavior and passes for as long as that behavior lasts, turning red the day the defect is fixed.

Then there is the compiler itself. It compiles itself, compiles itself again with the result, and the two emissions must be byte-identical. That is a determinism check as much as a correctness one — no hash-order iteration, no timestamps, no pointer values may reach the emitted text. A compiler can be entirely correct and still fail it, and nothing else would notice.

$ ./fixedpoint.sh
generation 3 is identical to generation 2

The name

Why Algol, and why 24.

Algol-24 is not a successor to ALGOL 60. It is its own language, and a modern one. The name is for the lineage that runs through Pascal, and because the two look strikingly alike on the page.

The 24 is a vintage, not a version. It marks 2024, when the work started. There will be no Algol-26.

Algol is also a star — β Persei, the Demon Star, an eclipsing binary whose two components pass in front of one another every 2.87 days. Two bodies that have to be observed together, whose agreement over time is the whole measurement. For a language with an interpreter and a compiler that are required to agree, the name turned out to be doing more work than it was given.

Provenance

It began as Lox.

Algol-24 descends from Bob Nystrom's Lox, from Crafting Interpreters — a debt that is still visible in the architecture, in the Scanner → Parser → Resolver → Interpreter pipeline, and in the empty vm/ directory waiting for the bytecode machine from the book's third part. The syntax became Pascal's because Pascal was the first language its author loved. Everything since has been the features worth keeping from forty years of using other people's.