Mettle 0.17.0
I've always had Mettle focus on being a compiler-centric project. The problems I wanted were in the backend: register allocation, vectorizers, a linker of its own, and getting all of it to run with no LLVM underneath. The language on top was mostly whatever the compiler needed in order to have something to compile.
0.17.0 changes that. I spent the last 2 weeks building and further fleshing out the language itself, and here's what I'm sharing with you!
Breaking changes
Read this section before upgrading. Code written against 0.16.3 may not compile.
Plain enums are opaque. An enum was an integer to the type checker. It widened to int32 without a cast, two unrelated enums compared as equal, and a Color went where a Role was declared. Enums now take part in no implicit conversion, a written cast is the only way across, and a switch on an enum names its variants.
print_int is gone. 408 call sites migrated. Use interpolation: println("{value}").
Narrow integer arithmetic wraps at its declared width. Only a store into a narrow location used to truncate, so big + big > 0 answered yes for two int32 values whose sum is negative, and interpolation printed 4000000000 for an int32. Lowering now cuts +, -, * and << back to width. Programs relying on the old 64-bit intermediate will compute different answers, and the new ones are the documented answers.
std/conv and std/net report failure through Result and Option.
| Was | Now |
|---|---|
str_find, str_find_byte answered -1 |
Option<int64> |
str_split_once answered a 3-tuple |
Option<StrSplit> |
str_to_i64 answered a second int |
Result<int64, string>, with str_to_i64_or |
net_init, socket_tcp, socket_udp, sockaddr_in, sockaddr_in_any, send_all |
Result, carrying the platform error code |
Linux links natively by default. The internal ELF linker writes the image; ld remains the fallback and --linker gcc still forces the driver.
Uninitialized aggregates start zeroed, as the docs always promised. A string local used to begin as a wild pointer.
! answers a bool. A for initializer gets its own scope, as does a shadowing var. Arrays decay to their address in every pointer position; passing a bare array used to pass its first 8 bytes and silently no-op fgets(buf, ...).
Language
char, a distinct one-byte type.s[i],for c in s, and"{c}"prints text.- String interpolation,
"{expr}"for any expression. Result<T, E>andOption<T>instd/core. Absence and failure are different questions and each gets its own answer.- Multidimensional arrays.
int32[3][4]is three rows of four; dimensions read left to right, sogrid[i][j]takesifrom the first. Both dimensions bounds-check, at compile time for a constant index and under--safefor a computed one. - Recursive tagged enums, two types may point at each other.
- Generic type-argument inference from what the arguments already say.
++and--, slices carrying their extent (T[],T[..]), heap arrays that know their length, function-pointer arrays,comptimegeneration from a constant table, and cross-moduleexport const.
Targets and linking
Shared libraries on Linux. The ELF linker binds a .so, emits one, and publishes a program's symbols so a library can call back in. raylib is the case it was built for.
mettle --build app.mettle -o app -L/opt/acme/lib -lacme --rpath /opt/acme/libNew: -l, -L, --rpath, --dynamic-linker, --export-dynamic, --shared, --soname.
Bare metal. Inline assembly, volatile that keeps its guarantee, @naked and @interrupt entries, cross-compilation via --target, a chosen link address via --image-base, flat images via --emit-flat, and 16-bit code generation.
Native ELF images with program headers, section headers and a symbol table, behind a format-neutral object reader serving both COFF and ELF.
Correctness
Roughly 100 miscompiles and internal compiler errors fixed. The recurring themes were narrow integer width, unsigned shift and divide read as signed, shadowing across locals, globals, parameters, match arms and for initializers, aggregate and tagged-enum copies, closure float ABI, and stack alignment at heap calls.
Three worth naming, all found late and all shipped in earlier builds:
- Vectorized SiLU and exp reprocessed their own output. The kernels ran a final overlapping vector to cover the remainder, which is sound writing into a separate destination and wrong in place: overlapped elements got
f(f(x)). Exactly8 - (n % 8)elements wrong for everyn > 8not a multiple of 8, a 57% error on SiLU. SwiGLU is a transformer feed-forward block, so the LLM engine was corrupted for any hidden dimension not divisible by 8. Every vectorizer test used a round count, so the remainder path had never been executed by anything. - A parameter assigned once in the body aliased its source. The call that passes an argument writes the parameter before any instruction runs, and that write has no IR to count, so
var t: int64 = b; b = 5; return t;returned 5 andgcdreturned 0 under-s. - The ELF linker rejected clang's GOT load. Only the
movform of a GOTPCRELX relocation could be relaxed; clang emitsadd rdx, [rip+disp32], so the native link failed, fell back told, and still exited 0.
The test corpus grew from 799 to 943 .mettle cases. The suite now runs every program it previously only compiled, proves it ran every case, compiles the codegen corpus at -O as well, adds a parallel differential harness over the examples, sweeps every in-place SIMD kernel by length rather than by round counts, and gates cyclomatic complexity against a recorded budget.
Diagnostics and tooling
Diagnostics render as a framed source table with syntax colour, through a shared style layer the --explain report also draws through. Crash locations order totally, so a fault names the same statement on both platforms. Crash reporting is on by default at function granularity for about 8KB and no codegen cost, with --no-crash-report to opt out. --dump-ast prints the tree. Every documentation snippet is verified against the compiler, and the code reference is generated from the compiler's own help tables behind a drift gate.
Performance
Compile speed roughly doubled on both platforms, all of it from removing quadratics: a declaration index in place of one scan per name, label and branch targets answered from an index, hash keys computed once, word-at-a-time string comparison, and an arena allocator. Generated code gained a real memory model in the redundancy pass, alias analysis backed by an index, load elimination over the dominator tree, SLP pairing of adjacent float64 statements, dense switch dispatch through a table, and spill costs charged by loop depth.
Full Changelog: v0.16.3...v0.17.0