alloyc-zig

A reference implementation of Alloy, built to quickly iterate and validate my ideas for the language.

Period
Jun 2026 – Aug 2026
Role
Designed and directed, AI-implemented
Status
active
compiler
24,338 LOC Zig
specification
1,067 lines
commits
21

An experiment on my own design. The language and the specification are mine; the implementation was written by an AI working strictly to that spec. Every gap it hit came back to me to resolve. I never let it decide the language.

Open source ZigLLVMLSPDAP

Highlights

  • Four years of design written down properly: a ~1,000-line formal specification covering lexical and syntactic grammar, static semantics, execution semantics, the standard library and compile-time evaluation.
  • It resolves the flaw that killed the previous compiler. Named types are nominally distinct, structural compatibility applies only where one side is an inline type, and narrowing requires an explicit cast; the rule AlloyLang specified but could never enforce.
  • It has the features the earlier attempts specified and never built: interfaces, closures, constructors, and compile-time macros that can generate types and static data.
  • A complete toolchain rather than just a compiler: checker, comptime interpreter, LLVM IR backend, language server, debug adapter, formatter and a VS Code extension.
  • The specification, the packaged compiler and the standard library live in their own repository, consumed as a submodule by both this compiler and the self-hosted one, so there is exactly one definition of the language.

The implementation is AI-written. I designed the language, wrote the specification and set the compiler's structure; I did not write the Zig.

This is the reference implementation, and it is still moving. The self-hosted compiler is checked against this.

What this is, before anything else

The implementation is AI-written. I did not write the Zig. What is mine is the language, the specification, and the structure of the compiler.

What this project demonstrates is therefore not implementation but technical direction. I wrote a specification precise enough that a complete toolchain could be built against it without me, decided the compiler's stage boundaries, and adjudicated every question the implementation raised where the spec was silent. The result covers a checker, a comptime interpreter, an LLVM backend, a language server, a debug adapter and a formatter, and there is a standard library written in the language on top of it. The work was specifying a system precisely enough to be built by someone other than its designer, and holding the design while that happened.

Why I did it this way

I had designed the same language three times and never finished a compiler for it. AlloyScript lasted days. AlloyLang died with type checking tangled into code generation. The C++ rewrite got as far as a front end and stopped there. None of them ever got far enough to tell me whether the design was useful, or whether writing real programs in Alloy would be better than writing them in C++, which was the entire point of the exercise.

So I specified the language properly and had it built, to see what the design looked like when something actually finished it.

What I did

I wrote the specification first and entirely from my own ideas: about 1,000 lines covering lexical grammar, syntactic grammar, the type system and static semantics, execution semantics, the standard library and compile-time evaluation. Writing it forced me to resolve the design questions the earlier attempts had left open or got wrong, including nominal versus structural compatibility, what interfaces are, what compile-time evaluation means, and which casts are implicit.

I also set the compiler's structure: the stages, what each one owns, and specifically that checking is its own phase with the code generator downstream of it. That is the correction to AlloyLang, and it is a structural decision rather than an implementation one, which is why it was mine to make.

Every time the implementation hit something the spec did not cover, it came back to me and I decided. I never let it choose on my behalf, because a hole in the spec is a hole in the language and those are mine to fill. That loop turned out to be the useful part. Being able to specify an idea, see it implemented and read real code written against it within a day or two meant I could validate or reject language features far faster than by hand, and most of the later additions to Alloy were formed that way.

What the design finally got right

Nominal typing that can actually be enforced. Named types are distinct, structural compatibility applies only where one side is an inline type, and narrowing to a specific named type requires an explicit cast. This is the exact rule AlloyLang's spec argued for and its compiler could never check, because by the time it compared types they had already been lowered to LLVM and Health and Stamina were both i32.

Interfaces, which the earlier attempts specified as traits and never built. Compile-time evaluation on a value-substitution model, where a comptime expression has its node tree stripped and replaced with the result once it finishes. That was the original reason for the whole project: the compile-time reflection I wanted and C++ made painful.

What came out of it

A full toolchain rather than just a compiler: tokenizer, parser, resolution, checker, comptime interpreter, LLVM IR backend, language server, debug adapter, formatter and a VS Code extension. Roughly 24,000 lines of Zig.

There is also a standard library, about 480 lines written in Alloy itself, covering strings, vectors, options, iterables, numbers, IO and process. It is the first body of ordinary code written in the language against a compiler that finishes.

What it is for now

This is the reference implementation. It is what the self-hosted compiler is written against and checked by, and it is still moving. The specification, the packaged compiler and the standard library were split into their own repository, which both compilers consume as a submodule, so there is exactly one definition of the language and neither implementation can quietly drift from it.

Recent work has been the things the earlier attempts never reached: constructors, closures, macros that generate types and static data, explicit borrowing of reference results, and keeping the language server's analysis alive across edits.