Skip to content

Mettle v0.16.0

Choose a tag to compare

@suidvandiewereld suidvandiewereld released this 17 Aug 02:42
· 458 commits to main since this release

Mettle v0.16.0

The next generation.

250 commits since v0.15.1. Mettle is now a single repository: the compiler, the backend, the linker, and the runtime build from one tree with no pinned dependency between them. This release adds a checked-access memory-safety mode, compile-time reflection, verified hot code swapping, and gives string a real library after fixing the reason it never had one.


Breaking changes

Narrowing conversions need an explicit cast

Widening still happens silently; narrowing reports M0119 and stops the build.

var n: int64 = 300
var a: int32 = n           // M0119
var b: int32 = (int32)n    // says the wrap is intended

This will touch existing code. The diagnostic names the file, the line, and the range the destination type holds.

print and println take string, not cstring

Call sites passing a cstring need print_cstr / println_cstr.

== and != on strings compare contents

They previously compared the 16-byte record as a scalar and were always false, including "ab" == "ab". Code that worked around this with streq still compiles; code that silently never matched now matches.

Other

  • A parameter can no longer be shadowed by a local of the same name.
  • Heap allocation is typed at the boundary. Allocators hand out rawptr, which converts to and from every pointer type, so var a: int32* = malloc(n) and free(a) need no cast.
  • The editor extensions moved to their own project.
  • libmtlc is no longer a pinned dependency. libmtlc.version is gone and the backend compiles from src/. It still ships as a release asset and the fetchers still work; what changed is that its API version (mtlc_version(), now libmtlc 0.2.0) is independent of the toolchain version. See Is libmtlc still a separate artifact?

Memory safety: --safe

--safe checks every access the compiler cannot prove in bounds, and reports what survived.

The work went into making the checks cheap: indices resolved against the heap, globals, and stack locals; an index recognized as coeff * counter + invariant + const; a single whole-loop check hoisted through bodies that branch and rejoin; a surviving check lowered to a comparison rather than a call. A checked function keeps its register allocation.

What it does not cover is written down in docs/memory-safety.md.


Hot code swapping

A function can be replaced in a running process, at a point the program names.

policy(5);                                  // 6
mettle_swap_stage(&policy, &policy_v2);
policy(5);                                  // still 6, staged but not applied
quiesce;
policy(5);                                  // 50

Staging records an intent; quiesce; is the only place it takes effect, so a replacement can never land mid-operation.

@swappable marks a function replaceable and keeps the call boundary a swap redirects: it implies @noinline, and @swappable @inline is refused, because an inlined body has no call to redirect.

The binding is a slot holding a function pointer, so applying a swap is one pointer-sized store. No code is modified, no page is made writable, and a thread already inside the old body finishes there.

Tool What it does
mettle swap-check old new runs the differential harness over two functions; refuses a changed signature
layoutof(T) layout digest, so a swap cannot silently reinterpret a value

A program with no quiesce; never links the swap runtime.


Compile-time reflection

comptime for walks a type's fields, Type and Field queries answer at compile time, and at module scope it generates declarations.

New in this release: fieldof(T, name) reaches a Field through a compile-time string, which is the one way into the field table a metaprogram can compose. Every other spelling needs the name written in source.

comptime for f in typeof(Packet).fields {
  total = total + (int32)fieldof(Packet, f.name).type.size;
}

Compile-time strings now compare, so a contract can span two declarations.

mettle expand prints the expansion as source, and mettle trace names the iteration behind each value instead of merging them:

21 |   total = total + f.offset ...   <- (field `kind`) total = 100; (field `seq`) total = 505

Strings

string had two representations at once: a value was a pointer to a {chars, length} record, a local was the record itself. Sites disagreed silently, and returning a string built in the callee wrote its fields through an uninitialized pointer. Nothing in the corpus returned a string, so nothing caught it.

string is now an aggregate and moves like the struct it is: copied whole, returned through a hidden pointer, stored inline.

That unblocked a real library. std/conv gains:

str_slice(s, start, len) the view at start, clamped to what s holds
str_starts_with / str_ends_with 1 or 0
str_eq_at(s, offset, needle) whether needle's bytes sit at offset
str_find / str_find_byte first index, or -1
str_contains 1 or 0
str_trim / str_trim_start / str_trim_end whitespace removed
str_split_once(s, sep) (head, tail, found)
str_to_i64(s) (value, ok); needs no terminator
i64_to_str(n, buf, buf_len) writes into buf, returns a view of it

Every returned string is a view into its input: nothing allocates, nothing copies.

var head: string = "";
var tail: string = "";
var found: int32 = 0;
(head, tail, found) = str_split_once("id=907", "=");

var value: int64 = 0;
var ok: int32 = 0;
(value, ok) = str_to_i64(tail);        // 907, no allocation anywhere

atoi, atol, cstr_len, and streq keep cstring as the C boundary.


Optimizer and vectorizer

The vectorizer reads shapes it previously declined:

  • an if that only selects a value, read as a value
  • counting under a predicate, without a branch
  • a comparison used as a value
  • a scan seeded from the first element
  • a global array's base hoisted above its loop
  • byte maps run in int32 lanes
  • a running maximum recognized as the operator it is

--explain reports what each loop and call became and why. Its fix suggestions are applied to a clone and re-checked before printing, and a guard that was discarding every proven inline fix is repaired.


Correctness

Each of these was a wrong answer, not a missed optimization:

  • == on strings was always false
  • returning a string built in the callee crashed, or returned garbage under --release
  • a predicated accumulate read the branch it had just retired
  • one arm of a select read the other arm's write
  • a call with many arguments built the wrong frame
  • three miscompiles around the integer range rule
  • defer did not run on break, continue, labeled jumps, or switch cases

Diagnostics

One mistake now produces one diagnostic, and the parser resyncs at block boundaries so a syntax error no longer cascades.

A GPU-only construct compiled for a CPU target used to report an internal compiler error naming an IR opcode number. It now names the construct and the flag:

error: 'tensor_mma' in function 'gemm_tile' runs on a GPU and has no CPU
translation. Compile the module that defines this kernel with --emit-ptx
(NVIDIA) or --emit-spirv (OpenCL), and keep it out of the host program

Runtime

Each optional component is independently excisable, and the absence is checked on every build rather than asserted. The swap and string runtimes are written in Mettle, compiled by the compiler that ships them.


Installer

The Windows installer is rebuilt in the language's own palette, with artwork generated from the brand mark at every display-scaling step, in light and dark. A guard fails the build if the version drawn on the banner is not the version being built. It reports the correct version and links to the right repository, both of which were stale.


Documentation

docs/ideology.md sets out the rules the language holds itself to and what each one costs. docs/memory-safety.md covers --safe, and the backend has its own reference under docs/libmtlc/.

The compiler-debugging pages documented four dump flags the compiler rejects; they now describe what exists. Eight working flags that appeared nowhere are documented, and every example presented as a complete program compiles.


Tests

The suite is at 927 checks, from 636 .mettle fixtures at v0.15.1 to 792.

Full Changelog: v0.15.1...v0.16.0