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