Mettle v0.16.3
Mettle v0.16.3
28 commits since v0.16.2. The language surface is unchanged, so existing code compiles as before. This release is backend work: the loop recognizers gained a shared foundation and a rot gate, the register-allocated backend picked up most of what it previously bailed on, the compile-time interpreter grew a full memory and call model, and three correctness fixes landed.
Correctness
Byte vectorizer sign extension (release-only miscompile). A byte load's lane value is the zero-extended byte whatever the element's declared signedness, because every scalar backend loads bytes with movzx. Honoring the declared signedness led the kernel to select vpmovsxbd for int8 arrays, so a release build of if ((int32)a[i] > 100) saw -106 where a debug build saw 150. Fixed at both decode sites and covered by a regression test.
mingw gcc 16 dropped emulated TLS. MTLC_THREAD_LOCAL now degrades to a plain static where the toolchain no longer supplies emutls, so the compiler builds on gcc 16.
Fingerprint snapshot lifetime. The snapshot owns its label strings, and stddef.h is included where size_t is used.
Loop recognizers
The recognizer pipeline was split into canonical form, a worklist driver, and a tail stage, which replaced repeated full sweeps. Supporting that:
- One strength-reduction table is now shared between the x86 and ARM64 backends. The table is proven against its own test, and the duplicate copy of the math in the backend was deleted.
- Loop recognizers share an affine model. The arithmetic was hardened against overflow, the facts are computed lazily, and the third recognizer sweep is gone.
- Canonical form is checked structurally, so a loop that merely stopped changing no longer passes as canonical.
- A claim baseline (
tests/loop_claims.baseline) fails the build when a kernel a recognizer used to claim goes unclaimed. Silent kernel rot was previously invisible. Regenerate withtools/regen-loop-claims.ps1. - A loop fingerprint diagnostic and an e-class pilot landed. The pilot was trimmed to what it actually measured, and the measurement is recorded alongside it.
Register-allocated backend
Functions that previously fell back to the older path now go through the allocated backend:
- Heap allocation, global aggregates reached by name, and the byte-offset fill walk are lowered natively.
- Global dirtiness is tracked flow-sensitively, so a function that both writes a global and makes a call can still be allocated.
- MIR carries strings as the aggregates they are, and the string value convention is honored through loads, stores, and call arguments.
- Float stack parameters are homed, float stack arguments are passed, integer literals passed to float parameters are folded at the call site, and any scalar coerces into a float argument.
- Indirect calls are typed through temps.
rotate_addis lowered natively,popcntis lowered, and vectorized reductions, scalar-reading vloops, and four more audited kernels cross the kernel bridge.- Nonzero fill starts fold into the kernel base and count.
Compile-time interpreter
ir_interp now models the whole memory and call model, and string literals have real backing memory. This widens what --verify and --pgo can evaluate at compile time instead of bailing.
Full Changelog: v0.16.2...v0.16.3