Skip to content

Mettle v0.11.0

Choose a tag to compare

@marquisburg marquisburg released this 12 Jun 22:14
59825ec

v0.10.0 shipped the optimizer as a little helper. v0.11.0 does the same for safety: the compiler now catches use-after-free, dangling returns, loop off-by-ones, and null dereferences before the binary exists, explains what went wrong and where, and when it can't catch something statically, the runtime names the faulting address instead of printing an anonymous pointer.

The vectorizer also grew up: runtime scalar broadcasts, integer lanes, and a confirmed miscompile class found and fixed across the entire recognizer suite.

Compiler: memory safety diagnostics

Hard build errors

  • Returning the address of a stack local, directly or through an alias
  • Constant-size memory ops that exceed the destination array's capacity
  • Constant array index out of bounds, including through pointer aliases (var p = &a[2]; p[6] is a build error naming a[8])
  • Loop off-by-one: j <= 8 over an int32[8] fails with the exact comparison to change
  • Divide-by-zero and over-width shift, with honest hardware explanations (x << 32 on int32 produces x; the hardware masks the count)

Warnings with fixes

  • Use-after-free and double-free
  • Stack address stored into a global
  • Allocation that never escapes and is never freed (suggests defer free(p) at the allocation site)
  • Null dereference through an unguarded pointer (any conditional on the pointer ends the claim)

Whole-program ownership inference

The compiler flattens imports into one program for codegen. It now uses the same call graph to compute per-function ownership summaries bottom-up: does this function free its parameter, store it, return a fresh allocation? The analysis re-runs with those summaries to catch UAF and leaks across call boundaries, through helper functions, and through wrapper allocators like make() -> malloc. Zero annotations required. Only definite summaries propagate; the zero-false-positive budget holds.

Runtime: access violation forensics

0x10 is no longer anonymous. With -s, a fault now explains the address:

Attempted to write inaccessible memory at 0x0000000000000010
This address is null plus offset 16: a field or array access through a null pointer
  --> tests/debug_crash.mettle:13:14 in main
  • Null-plus-offset: prints the field offset
  • Stack region: dangling-frame note
  • Freed heap: names the block, its size, and that it was already freed

--native-heap is now a UAF detector. Freed page-backed blocks keep their mapping with all access revoked and sit in a quarantine ring. Freed small blocks are poisoned 0xDD. A dangling read or write faults immediately with classification text, at the point of access, not three functions away where the corruption surfaces.

The static analyzer and the runtime fault handler compound: a UAF that escapes compile-time analysis faults instantly at runtime with the block named. Linux signal path covered.

Vectorizer: correctness and generalization

Confirmed miscompile fixed

Loops with a nonzero induction variable start (j = 3; while (j < n)) were silently replayed as 0..n by SIMD recognizers. Confirmed live repro returning 100 instead of 97. Fixed via a shared ir_iv_zero_at_header guard, one implementation, no duplicates. Eight recognizers had the same hole and all received the fix: sum_i32, dot_i32, dot_i8, exp_f32, memcmp byte-loop, clamp_i32, reverse-copy, both SLP MAC matchers, i2f, outer-lane vectorizer, and all pointer-walk matchers.

VLOOP generalized

  • Runtime scalar broadcasts: any loop-invariant scalar symbol (param or local) now vectorizes as a broadcast. out[i] = (a[i]*s1 + b[i])*s2 with runtime coefficients vectorizes at 9.8x over scalar.
  • Integer lanes: new IR_OP_SIMD_VLOOP_I32 opcode covers int32/uint32 unit-stride maps and +-reductions over + - * & | ^ <<literal DAGs, 8 lanes per iteration. Results are bit-exact against scalar. Int maps 3.9x, int reductions 5.8x.

uint32 wrap semantics

Unsigned sub-64-bit arithmetic now wraps mod 2^width consistently in all four modes (encoder, MIR, fallback backend, return canonicalization). Root cause was binary_emit_mov_reg_reg32 silently no-oping on dst==src, eating every uint32 zero-extension. Fixed with a dedicated binary_emit_movzx_reg_reg32 and three-layer canonicalization.

VS Code extension

  • Full Debug Adapter Protocol: F5, conditional breakpoints, live variable editing, crash reporting via named-pipe debug runtime
  • Optimization report panel: regression detection, verified fix application, click-to-source
  • @inline! and @noalloc contracts surface in the panel
  • Complete language service: go-to-definition, references, rename, outline, completion, signature help, inlay hints, CodeLens, hovers, compiler-backed diagnostics
  • All memory diagnostic warnings and errors surface in the VS Code Problems panel with fix suggestions attached

Other

  • DCE pass: dead code elimination in IR, improved compile speeds
  • IR optimizer speedups: hash value maps, dead copy-propagation pruning, getenv caching
  • type_checker.c and ir_lowering.c split into focused modules
  • Bounded self-recursion inlining
  • Site-aware inliner with repeated-refusal folding
  • Linux build fix: explicit -pthread -ldl for glibc < 2.34
  • 416/416 tests, 400/400 differential-fuzz seeds clean

What's Changed

  • Add source-level debugger and interactive --explain optimization report panel with verified fixes by @marquisburg in #41
  • Memory safety diagnostics, access violation forensics, and vectorizer correctness by @marquisburg in #42

Full Changelog: v0.10.0...v0.11.0