Contents

Changelog (mere)

Major implementation milestones recorded per-slice (newest first). See git log for detailed commit messages.


v0.1.294 — 2026-08-22

_Every formatted value leaked its scaffolding._

Both native backends build a formatted string in an asprintf buffer, copy it into the current region at the __lang_str_of_cstr boundary -- and never free the buffer. One vasprintf buffer is 160 bytes on macOS, so a million str_of_int calls leaked 48 MB. The list formatter was worse: it rolled its accumulator through asprintf on every element (__acc = __buf), leaking the whole previous prefix each step -- quadratic bytes in the list length.

Found from the outside: mere-ruby's new bench/region_reuse.sh asked whether a region whose block chain GREW past one block hands its memory back in a reusable form (the property a compaction loop stands on), and the answer looked like no -- peak footprint grew linearly, ~38 MiB per iteration, while the runtime's own block counters insisted everything was returned. A minimal C probe cleared libc (the same malloc/free pattern is flat at any iteration count), and leaks named the 300,000 vasprintf buffers.

The boundary now has an owning twin: __lang_str_take_cstr copies into the region and frees its argument. All 13 asprintf sites in the C backend and all 6 in the LLVM backend go through it; getenv/argv/literal sites stay on the borrowing one. The C list/json formatters free their rolling accumulator.

After the fix the original question answers itself: released grown chains are fully reusable through plain libc -- 1, 8, and 32 sibling ~64 MB regions all peak at 36-39 MiB. No runtime block pool is needed.

parity 119/0, dune test 2536/0, ctest, stack_overflow, selfhost_check, url/encoding parity, debug_info, wasm_sourcemap, lsp_smoke -- all run locally before push, plus leaks --atExit reporting zero on the probe and on a list-show program.


v0.1.293 — 2026-08-21

_Whoever names a copier decides that it exists._

v0.1.290 taught __mcopy for an arrow type to deep-copy a closure's env, which put copier CALLS in two places that had never emitted one before: a record's copier, for a field holding a closure, and an env copier, for a captured value. CI went red for four releases -- 16 of 119 parity programs, every trait_* one among them -- with the emitted C naming a function that was never defined:

v.mu_add = __mcopy_closure_int_closure_int_int(r, v.mu_add); / undeclared /

Two independent faults, each hiding behind the other's failure.

The first: `ty_tag` names types that `ty_is_concrete` rejects. ty_tag erases an unresolved type variable to int, so a capture of list (str, 'a) is named __mcopy_list_tuple_str_int -- while the collector that decides which copiers to emit walks the same type, finds it not concrete, and drops it. A name with no definition. Registration now goes through ty_as_tagged, which returns the type ty_tag actually names, including the region slot that ty_tag renders as __heap (erasing that to int would reintroduce the mpng P5 shape: one type under two names).

The second: a polymorphic record's fields were read three times, and two of the readers had a different answer. A trait dictionary Num__dict 'a is monomorphized at its instance -- for Num__dict m7 the struct field is closure_m7_closure_m7_m7 -- but the copier emitter read r_fields directly and named __mcopy_closure_int_closure_int_int, the wrong type under the wrong name, while the collector dropped the field entirely for holding a TyParam. Only the closure-typedef collector, which had hit this before, substituted the arguments. The rule now lives in one place, record_field_types_at, and all three read it.

Also: the inner-lifted half of the env-copier list is no longer filtered on captures = []. A closure with no captures has no env at all and needs no copier; an inner-lifted fn always gets an env struct and its use site assigns __copy unconditionally. Measured, not reasoned -- restoring the filter fails exactly one parity program (prop_list).

Gates: parity 119/0 (was 103/16), dune test 2536/0, plus ctest, stack_overflow, host_matrix, selfhost_check, url/encoding parity, infer_scaling, debug_info, wasm_sourcemap, lsp_smoke, window_check -- none of which CI had reached since v0.1.290, because it stops at the first failing gate.


v0.1.292 — 2026-08-21

_The copier moves into the env, and a closure is two pointers again._

v0.1.290 put the env copier in the closure struct, which made a closure three pointers. v0.1.291 answered the cost by emitting two different closure shapes -- three pointers for programs that use a region, two for those that do not. That is two rules where there should be one, and it left the memory win unavailable to exactly the programs that wanted it: an interpreter that adds a region block to reclaim its temporaries pays the pointer on every frame of its dispatch, and one of CRuby's bootstraptest pairs loses its stack (ld caps -stack_size at 512 MB on arm64, so there is no room to buy back).

The copier now lives in a header on the ENV: {__lang_region* __r; void* (*__copy)(__lang_region*, void*);} at the front of every env struct. __mcopy for an arrow reads it through the void* it already has. The closure struct is {env, fn} again, for every program, and the region-conditional emission and the typer flag it needed are gone.

An FFI adapter used to hold a borrowed pointer directly as its env, which a header cannot be read from. Those envs are now real structs -- a header plus the borrowed pointer -- allocated in the default region with __copy = NULL, which says "do not copy me, I am permanent" in the same field that says "copy me like this" for a generated env. One rule, read the same way everywhere.

Measured on an interpreter written in Mere: with a region block per statement, 200k plain method calls hold 1181 MiB against 1358-1996 before, the same number three runs out of three, and run in 1.16-1.20s against 1.43-1.58s. Its corpus is 157/157 either way.

Two sentences in the first version of this entry were wrong, and the correction is worth more than they were. They said the memory win now costs nothing and that CRuby's bootstraptest was back to its baseline. Neither holds: the pair v0.1.291 was written around still fails with the closure at two pointers, so the third pointer was not its cause -- or not its only one. That pair runs a thread with while true; // =~ "" end beside a regex loop, and whether it finishes depends on which limit trips first, the interpreter's step budget or the native stack. The same source overflows at -O1 and does not at -Os: a canary on the line, not a measurement of a change. The interpreter's own record now says so, so that its err=59 is not read as a fresh regression.

The reasons to prefer this shape stand without those sentences: one closure layout instead of two, no per-frame pointer, and an FFI env that answers the same question the same way a generated one does.

2536 tests pass across four backends.


v0.1.291 — 2026-08-21

_An env records its region, and a program that uses no region pays nothing._

v0.1.290 gave a closure a copier for its env and then copied unconditionally. That is neither idempotent nor bounded. An env that captures a closure copies that closure's env in turn, so a chain of them is a chain of copies, and a threaded test in an interpreter written in Mere overflowed the native stack: stack overflow (recursion too deep), where the same program passed before. Copying also duplicated identity — two copies of one env are two mutable states, and a closure that writes through what it captured would write to the wrong one.

Each env now carries the region it was allocated in, and the copier returns its argument unchanged when that region is the destination. Copies inside one region — the common case, and every copy in a program with no region blocks — become no-ops, and a cross-region copy still walks only as deep as the chain that actually crosses. This is the elision the memory model deferred as needing type-level region tracking on values; one pointer per env does it at runtime.

Found by CRuby's bootstraptest, run against an interpreter written in Mere: 1697 pairs, of which exactly one moved from pass to error. Neither that interpreter's own 157-program corpus nor this repo's 2533 tests noticed, because the shape needs a closure captured inside a closure inside a thread. A wider gate is worth having even when it is somebody else's.

The idempotence fixed the copying, and did NOT fix what the bootstraptest pair was actually measuring: v0.1.290 made the closure struct three pointers instead of two, and a closure is passed BY VALUE through every frame of an interpreter's dispatch. One pointer per frame took that program over its stack limit -- and there is no room to raise it, because ld refuses -stack_size above 512 MB on arm64, which is exactly where the interpreter already was.

So the copier is now emitted only for programs that USE a region block. With no region block __lang_current_region IS the default region, an env cannot outlive its region, and the closure keeps its pre-v0.1.290 shape: two pointers, an env in the default region, a shallow copy. Charging a cost to programs that cannot benefit is the wrong trade, and the measurement said which programs those are. Verified end to end: the interpreter rebuilt without a region block is back to pass=1569 err=58, its baseline, and its generated C carries no copier field at all.

The flag that answers "does this program use a region" is set by the typer, not during emission -- the typer walks the whole program first, and a closure may be emitted before the region block that appears later in the file. It is reset per compilation, because the language server and the test harness both compile many programs in one process, and a leftover true would give a region-less program the shape of whatever was compiled before it.

v0.1.290 also shipped with version.ml left at 0.1.289: its changelog entry was written after the suite ran, so the version test's failure was not seen before the commit. Both are corrected here.


v0.1.290 — 2026-08-21

_A closure carries the copier that lets it leave a region._

Closure environments were allocated in the default (program-lifetime) region, and the reason was written where the deep-copy family is defined: "closures copy shallowly: their envs live in the default region". A closure's env is a type-erased void*, so nothing could copy it into another region — and a permanent allocation was the only answer that could not dangle. The cost is invisible in a small program and decisive in a large one: an interpreter written in Mere allocated 2548 closure envs' worth of default-region references against 1346 current-region ones, so every call leaked whatever it closed over.

A closure now has a third member beside env and fn: void* (*copy)(region*, void*), generated per env type next to the env's own typedef, where the field types are known. It region-allocates a fresh env, copies the struct across, then re-copies each captured field through that field's own __mcopy_ — so a captured string is deep-copied rather than left pointing into the region that is about to be released. __mcopy for an arrow calls it when it is set, and a closure leaves a region block the way every other value does.

Two shapes deliberately keep the old behaviour, and get it for free from C's zero-initialization of omitted designated initializers: a closure with no captures passes env == NULL, and an FFI adapter holds a borrowed pointer it does not own. Both leave copy zero and are copied shallowly.

The test that asserted the old rule now asserts the new one, in the same three places it was checked: the allocation follows __lang_current_region, the closure literal carries .copy = __mcopy_env_..., and a captured str reaches the copier. 2533 tests pass across four backends; the change is C-only, since the other backends do not share this representation.

What this does NOT do on its own: an interpreter with no region blocks has __lang_current_region == &__lang_default_region, so nothing moves until the blocks exist. It removes the reason they could not pay off.


v0.1.289 — 2026-08-20

_A record field is a C struct member, and one name answered two questions._

The other half of the keyword surface. v0.1.286 routed a closure CAPTURE's name through c_safe_name; a user record's fields never went through anything, so type t = { short: str } emitted const char* short; — a keyword where a declarator belongs. Same argument as v0.1.56's, which chose a uniform mu_ prefix over a reserved-word list because the list "was inherently incomplete and recurred six times", so the same prefix is used and there is one rule to know rather than two. Fields live in a per-struct namespace, so a prefix collides with nothing; what it buys is that no C keyword reaches a declarator.

Fourteen sites: two definitions (the struct body and the monomorphised one) and twelve uses — literal, update, field access, pattern binding, show_, to_json, from_json, eq, cmp, __mcopy, and a map key's equality and hash. Plus two hand-written runtime literals, for mk_logger and mk_metrics, which spell the field names out and had to spell them the same way.

One real bug came out of doing it, and it is the interesting one. from_json used the same fname twice in one format string — once as the C designator and once as the JSON KEY. Prefixing both renamed every key in every serialised record, which four parity programs said immediately. The designator is an identifier and the key is what the document says; one name, two questions, and only one of them wanted namespacing.

And the loud-failure property earned its keep. Eleven assertions in the suite pin this struct's spelling from various sides and all eleven went red at once, which is exactly what v0.1.56 argued a uniform prefix would buy: a path that forgets it breaks every record rather than only the unluckily-named ones. Separating "the assertion is stale" from "the codegen is broken" was one measurement — the four JSON parity programs compile and run — and after that the assertions were bookkeeping.

Two sites were missed on the first pass and both were found by gates rather than by reading: the cmp line, because the replacement written for it was character-identical to the original and the patch skipped it, and mk_logger's runtime literal. host_matrix named the second in one line. mk_metrics is still nocompile on the C backend and was before this — a function pointer type mismatch, unrelated.

Verified on Darwin: dune runtest 2531/0, parity 119/0, ctest 14/0, host_matrix with no change, selfhost ok, stack_overflow ok. Verified on Linux in a container: fifteen programs through the C backend and eleven through LLVM, including a record with short, long and inline as field names, all matching the interpreter.


v0.1.288 — 2026-08-20

_The LLVM backend names a stack overflow on Linux, which took dlsym and a computed array length._

Three releases and two wrong shapes to get here, all of them the same underlying fact: LLVM IR has no preprocessor, so a runtime detail that differs by platform has to be selected at run time or not at all.

v0.1.285 made the Darwin-only pthread pair extern_weak and guarded the call, which stopped the link failure and left the bounds unknown off Darwin. v0.1.287 found that the handler was not even being installed there — stack_t, struct sigaction, the SA_* values and SIGBUS all differ — and selected seven measured constants off one icmp. What was left was the bounds, and the obvious move does not work: declaring pthread_getattr_np extern_weak breaks the DARWIN build, because Mach-O's linker refuses an undefined weak reference where ELF resolves it to zero. Measured, not assumed — it was tried and reverted.

dlsym(RTLD_DEFAULT, …) asks at run time and needs no reference at link time, which is what wanting an optional symbol actually calls for. Measured: it is in libc on both platforms, so no extra link flag, and RTLD_DEFAULT is itself platform-dependent — 0 against -2 — which the same icmp selects. glibc reports the LOW address and the size, the other way round from the Darwin pair.

platform C backend LLVM backend macOS/arm64 names it names it Linux/x86_64 names it names it Linux/aarch64 names it names it

So the divergence is gone and both the things that recorded it are gone with it. The parity pin for uncaught_stack_overflow is deleted, and scripts/stack_overflow.sh expects one answer everywhere instead of a per-platform one — if a platform stops naming the fault that is now a regression rather than a fact about the platform. The gate's second mode, for a backend expected not to name it, is removed rather than left: nothing reached it, and a branch nothing reaches cannot be told from a branch that is wrong.

One thing worth the line because it was caught late and cheaply: the three symbol-name constants had their array lengths written out by hand and two of the three were off by one. They are computed from String.length now. LLVM catches that, but only after the file is emitted, and nothing about the source made it visible.


v0.1.287 — 2026-08-20

_The LLVM handler installs itself on Linux, which needed seven measured constants._

v0.1.285 stopped the LLVM backend failing to link off Darwin by making the two Darwin-only pthread calls extern_weak and guarding them, and left the fault unnamed there. What it did not do was notice that the handler was not being installed at all: stack_t, struct sigaction, the SA_* flag values and SIGBUS are all different on glibc, so sigaltstack was handed a size where it expected flags, refused, and the early return meant no handler ever arrived. The process died with the shell reporting the signal.

The platform is a runtime fact here, not an emit-time one. IR has no preprocessor, and choosing when the IR is written would make -ll output specific to the machine that produced it -- which it has never been. pthread_get_stackaddr_np is already declared weak, so its address is non-null on Darwin and null everywhere else: one icmp and every constant below is a select.

Measured on macOS/arm64, Linux/x86_64 and Linux/aarch64 rather than recalled, because a wrong offset here writes a flag word into a signal mask and the handler simply never comes:

si_addr in siginfo_t 24 / 16 stack_t ss_size, ss_flags 8, 16 / 16, 8 struct sigaction sa_flags 12 (of 16 bytes) / 136 (of 152) SA_SIGINFO | SA_ONSTACK 65 / 134217732 SIGBUS 10 / 7

The observable difference on Linux is small and exact: Segmentation fault from the shell becomes segmentation fault from our own handler, and the exit status becomes 1 -- what the interpreter exits with -- instead of 139. So parity's pinned divergence for uncaught_stack_overflow moves from EXIT(139) to MSG: the process now fails the way the interpreter fails and only the message differs. The pin catching that is what it is for.

The name still needs the bounds, and that is one measured step away. The glibc pair is pthread_getattr_np + pthread_attr_getstack, and declaring them extern_weak does not work: ELF resolves an undefined weak reference to zero and Mach-O's linker refuses it outright, so adding them broke the Darwin build. dlsym with RTLD_DEFAULT is the portable way to ask for an optional symbol, and RTLD_DEFAULT is itself platform-dependent (0 against -2), which the same icmp can select. Not done here: it is a separate change with its own verification, and shipping it half-measured is how this release's predecessor got the layout wrong.


v0.1.286 — 2026-08-20

_A capture the walker forgot, twice, and the shape that needs three things at once._

The browser dogfood is the first program here to ask the C backend to compile something large, and it did not compile at all: 29 errors, in two families. The first was field names not going through c_safe_name and is fixed above. This is the second, and it is the one that had been invisible for a reason worth writing down.

`known` decides what is NOT captured, and `host_locals` is what rescues a name from it. known is builtins plus top-level names plus externs — things referenced directly in the generated C rather than through an env. host_locals is subtracted from it, which is how a frame-local that happens to share a builtin's name gets captured anyway. The top-level driver has always passed [f.param] for exactly this. Two places lost it.

A curried parameter's name was discarded. The walker's own Fun case read Ast.Fun (_, _, body), so descending through fn (cs) -> fn (id) -> ... recorded cs and forgot id — and id is the identity builtin, so it sat in known, and an inner let rec reading the parameter captured nothing. The lifted function then named an identifier that was never declared, reported by the C compiler thousands of lines from the decision.

And descending into a lifted body reset the list. walk_in_fn p [] fn_body threw away the enclosing frame's names, so one lift further in the outermost parameter looked like something already in scope. One level worked because the enclosing frame was the top-level one, whose parameter the driver had recorded.

Three things have to line up, which is why neither showed up before. The name shadows a builtin; it is a curried parameter rather than the first one, or read from a nested lift; and it is read from a lifted function. Any two of the three and the program compiles.

Two parity programs, one per defect, each verified to catch its own by reverting the fix. The second was written with an ordinary name first and passed with the fix reverted — it never reached the code it was for, and only poisoning said so. It names its parameter after a builtin now, and the comment says why.

MERE_LIFT_DEBUG=1 prints what this pass decided: per lifted function its host, parameter and captures, and per lift the body's free variables alongside the names known blocked. Kept rather than deleted after use — a missing capture surfaces as use of undeclared identifier in generated C, and the alternative to reading this is guessing which of the four skip conditions in the fixpoint fired.

The browser dogfood compiles now, and the measurement its north-star gate owed can be taken: a 559-byte page in 45 ms, of which 19 is loading the font and 18 is style and layout, at a 39 MiB high-water mark.


v0.1.285 — 2026-08-20

_"On every backend" was verified on one platform, and CI said so for four days._

v0.1.271 named the stack overflow, and its title says "on every backend". It was true on Darwin. On Linux it stopped the C backend compiling and the LLVM backend linking — every program, not an edge case — and parity has been red since the commit after it. Four consecutive days of failures, latterly hidden behind a stale version assertion.

Three platform-specific defects in one runtime block.

pthread_getattr_np is glibc's way to ask a thread for its stack, and glibc declares it only under _GNU_SOURCE. Without the macro the emitted C called an undeclared function, which clang 16 and later treat as an ERROR — and -w, which the parity harness passes, does not silence an error. The #define now comes before every header, which is the only place it works.

static char __lang_sigstack[SIGSTKSZ * 4] is a variable length array at file scope on modern glibc, where SIGSTKSZ expands to sysconf(_SC_SIGSTKSZ) rather than a constant. It is a literal 65536 now, comfortably above MINSIGSTKSZ on both platforms.

And the one that had no preprocessor to hide behind. The C backend picks between the Darwin pair and the glibc call with #ifdef; LLVM IR cannot, so the Darwin names went out on every target and nothing linked. They are declare extern_weak now and the call is guarded: on Darwin they resolve and the fault keeps its name, and elsewhere they are null, the bounds stay unknown, and the handler falls through to the plain segmentation fault it already reported for a fault outside the stack. That costs the NAME off Darwin and nothing else, and it needs no platform detection at emit time — so -ll output is still the same file wherever it was produced. A name derived from an assumed stack size would be wrong for any program linked with a bigger one, which is worse than no name.

What let it through is the shape worth keeping. The suite checks this feature by looking for stack overflow (recursion too deep) and sigaltstack in the EMITTED TEXT. Those assertions stayed green throughout, because a string is present whether or not the file it is in compiles. scripts/stack_overflow.sh runs a program that recurses until it dies and reads what it says, per backend, with the per-platform answers pinned rather than smoothed over. It is in CI.

platform C backend LLVM backend Darwin stack overflow (recursion too deep) stack overflow (recursion too deep) other stack overflow (recursion too deep) a plain crash, no name

Measured on Linux in a container, not inferred: the C backend now compiles and matches the interpreter on the first twelve parity programs, where before none of them built, and it names the overflow there for the first time.


v0.1.284 — 2026-08-19

_Generated inputs for the differential gates, and five NUL-length defects they found._

contrib/prop generates values; it does not compare them. A property test written with it is an ordinary parity case, so the other four backends are the oracle and there is nothing to commit as an expectation — the same shape as the browser dogfood's reftest, moved from pixels to values.

Three decisions, each a way this could have measured nothing. It does not use random_int: that builtin asks the host, five hosts would draw five sequences, and every line would report DIFF while the gate reported on itself. Values are a function of an index rather than a carried state, so a differing line in the diff already names its input — which is why there is no shrinker, the smallest reproducer is i and it is printed. Every intermediate stays below 2^45, because the interpreter's int is 63-bit and the compiled backends' is 64-bit.

The edge tables are not a draw. Every integer defect this suite has found sat on one of those values and a uniform draw reaches them with probability about zero, so a generator that only drew would be weaker than the hand-written gates it extends. NUL is in the byte pool on purpose.

Five defects on the first run, all one family

Code that was correct while a str ended at its first NUL, and silently wrong after v0.1.264 gave a str a length header.

builtinbackendwhat was wrong
str_containsC, LLVMstrstr — a needle beginning with NUL matched every haystack
str_index_ofLLVMstrstr — answered 0 where the C backend's length-aware search answers -1
str_starts_withLLVMstrncmp, and no length check on the haystack — the only one of the four starts/ends implementations across the two backends written that way
str_joinCthe sizing pass asked __lang_str_size and the copying pass asked strlen, so an element holding a NUL was measured at its full length and copied only to the NUL. One function, two notions of how long a string is
str_ptr, read_linesCfound by sweeping every strlen in the backend after the first four, not by waiting for a gate to point at them

Two recorded rather than fixed

Each pinned in its own case so it keeps being asked.

test/parity/prop_utf8.mere — UTF-8 character splitting. For "A" ++ chr 128 the interpreter says two characters and the three compiled backends say one, so utf8_at returns two bytes and codepoint_at then refuses. Index 0 is "A" and what follows cannot change that, so the interpreter is right. The span computation is prelude Mere, one source compiled by all four, which means the difference is under it — a separate measurement, not a guess to make while writing a test.

test/parity/llvm_loop_guard_global.mere — a loop whose bound is a top-level binding stops after one iteration on LLVM. All three ingredients are needed and each was removed in turn to check: the bound must be a top-level let and not a literal, the body must evaluate a try_or whose thunk calls a prelude function, and it must be recursive. A program folding this way would silently process the first element and report success.

The axes are kept apart

prop_int leaves multiplication out and prop_list leaves list_product out, both with the reason written down: their operands overflow, the interpreter wraps at 63 bits where the compiled backends wrap at 64, and a gate measuring the width axis while claiming the value axis reports the wrong one when either moves. codepoint_at moved out of prop_str for the same reason.

test_basic: "llvm: declares strstr" asserted how index_of is implemented rather than what it answers. It now asserts the memcmp and that strstr is absent.

Gates: unit 2527-0, parity 117-0 + failing 15-0 (5 declared divergences), ctest 14-0, selfhost 7-7, html_tokenizer ok.


v0.1.283 — 2026-08-18

_Two shadowing bugs, and a gate whose expectation was degenerate._

Both bugs had the same shape: the compiled backends resolve a name globally, and something shadowed it. The interpreter was right in both cases, which is what made them findable at all.

Q-045: a user binding that shadows a builtin the prelude calls


let show = fn (x: int) -> x + 1;
print (str_of_int (show 1))

Three lines, and it made the prelude fail to type — expected 'str', got 'int' at :485 for a program that never mentions the prelude — on all four compiled backends. The prelude's pow calls the show builtin. Not show-specific: str_len and list_len are each called three times in there.

Two independent causes, and the first hid the second.

The desugared program was typed against the accumulated environment. Since desugar_program turns every Top_let into a nested Let, the expression rebinds all of them itself — so passing the accumulated environment added nothing except the one thing it must not: a binding visible to declarations that come before it. It now types against an environment holding only what desugaring drops, which is externs.

That turned the error into a refusal from the monomorphiser, because uniquify_toplevel_shadows never saw the user's show as a shadow — a builtin is not a top-level declaration, so the first binding of that name kept it. It is seeded with the builtin names now, so the user's binding is renamed and references before it — the prelude's — still mean the builtin.

The seed subtracts the prelude's own top-level names: the prelude deliberately shadows ten builtins (pow, divmod, assert, …) and those keep the names they have always had.

Q-046: a parameter named the same as a top-level binding

uniquify_inner_fns_expr renames inner fn bindings that collide with a top-level name, and left Fun parameters alone. So a lifted inner function took no parameter for its captured handler at all, and its body referred to the caller's global of that name.

It was invisible while both had the same type — the wrong binding happened to fit — and surfaced as C that would not compile only when the types diverged. contrib/http2 carried a naming workaround for it; the parameter is renamed now, so that workaround is a comment about history rather than a rule.

Parameters get a different treatment from inner fns and the distinction is the point: an inner fn becomes a symbol and must reserve its name, a parameter does not and only has to not be mistaken for one.

Three test assertions were pinning a symbol name

§30.0 checked that a user-defined is_alpha shadows the builtin by grepping the output for int mu_is_alpha(. The fix renames it to is_alpha__v2, so they matched the prefix instead. Measured before changing: the compiled program answers true and the builtin answers false for the same input, so the behaviour under test was unaffected — the exact name only said it by accident.

The line-break gate was comparing against nothing

linebreak_conformance reported 19338 of 19338 cases differing. want.txt had 19338 lines and not one break mark in it: under this machine's LANG=ja_JP.UTF-8, awk 20200816 compares ÷ equal to × — collation, not bytes — so every mark became ×.

The implementation was right and the expectation was degenerate. It runs under LC_ALL=C now. It passes on GNU awk and under the C locale, which is why CI was green and a Japanese-locale machine was not — the environment difference to suspect first when a gate disagrees with CI. The data is ASCII plus those two symbols, so byte comparison was what was wanted all along.

Verified: 28 CI gates, runtest 2526/0, parity 110/0 + 15/0 (two new parity programs), selfhost_check all passed, host_matrix ok.

contrib — GraphQL introspection and validation — 2026-08-18

_No compiler change. contrib/graphql, two new gates._

Introspection is answered by the ordinary executor

__schema, __type(name:) and __typename. The introspection types are ordinary SDL, generated from graphql-js into introspection_sdl.mere and appended to the document's own definitions — so __Type is an object type like any other and field lookup, nullability, null propagation, list handling and enum coercion all apply to it unchanged. One line, and there is no second executor.

The SDL is generated and committed, like the Unicode and HPACK tables: the specification fixes every name, type and nullability, and a 200-line transcription has a mistake in it.

Introspection is cyclic, so one value has to be lazy. __schema.types lists every type, each type's fields name types, whose fields name types; a strict value cannot hold that and eager construction does not terminate. What bounds the expansion is the query — getIntrospectionQuery() asks for ofType exactly nine levels deep. So gvalue has one non-data arm, GTypeRef of gtype, computed when asked for: the only place in this executor where a field is computed rather than looked up.

The strongest check is not a comparison. Our introspection result fed to graphql-js's own buildClientSchema and printed must equal printSchema(buildSchema(sdl)). It holds exactly when our answer carries the whole schema, it is blind to field order (which the specification does not fix), and nothing transcribes an introspection result. It passed before the JSON comparison did, and both differences that then surfaced were real:

oracle's type map for type Query { a: Int } is Query Int Boolean String and not the other two — Boolean and String because the built-in directives refer to them, which falls out of walking the appended SDL rather than being special-cased.

a.defaultValue. Reading the old field silently dropped @deprecated's default.

`includeDeprecated` defaults to false, and the standard introspection query passes true — so every gate section using it agreed while that was missing entirely. It took a hand-written __type(name: "Colour") { enumValues } to disagree.

Validation, and how a partial validator is gated

19 of the specification's 32 rules. The gateable part is the design:

Every error carries the name of the rule that produced it, and the harness compares the set of rule names against graphql-js running each of its 32 rules individually — the oracle classifying its own output. Not the error list, for two measured reasons: graphql-js returns errors in visitor order interleaved across rules, so a partial validator could never agree about anything; and rule names are about thirty identifiers from the specification's own section titles, small enough that a shared misreading is not a real risk.

Three failures, not one: rejecting what the oracle accepts (the worst — a false positive fails a valid request), missing a rule we claim, and missing a rule we do not claim (DOCUMENTED-GAP, and the rule must be listed). A gap-list entry that never fires fails the harness as stale.

The claim list is checked in both directions: every rule reported must be on it. Without that, dropping a rule from the list while still implementing it left the harness green — poisoning found it, because the list is otherwise consulted only for rules that were missed. A wrong list silently weakens every check that reads it.

The document and the schema are separate arguments, unlike the executor. ExecutableDefinitions is the rule that a document being executed must not contain type-system definitions, so a combined list makes every schema violate it — the first version reported every SDL type as "not executable".

What the harness caught: we rejected { __schema { queryType { name } } } because __schema / __type / __typename are provided by the schema rather than declared in it; is not defined **by** operation versus is never used **in** operation, two prepositions and one helper that put the wrong word in one of them; and a fragment cycle is reported once, at the first fragment in document order.

A harness bug worth naming

Comments inside a node script passed in a double-quoted shell string may contain neither a backtick nor a double quote — one is command substitution, the other ends the string. Both mistakes were made in consecutive edits. The second turned the comparison into a syntax error rather than a wrong answer, which is the good failure of the two.

contrib — HTTP/2 flow control and gRPC streaming — 2026-08-18

_No compiler change. contrib/http2, examples/grpc_hello.mere, and two gates._

Both directions of the flow-control window, SETTINGS_MAX_FRAME_SIZE, and gRPC methods that send or receive more than one message. grpcurl now gets server-streaming, client-streaming and bidirectional answers from a Mere server, and a 300 KB request and a 300 KB reply both go through.

The limits bite in an order, and the first one is not a window. Measured against a python h2 client before any of the code existed:

boundbites atwhat the client does
wbuf capacity, unchecked16385nothing — wrote past the buffer, returned success
SETTINGS_MAX_FRAME_SIZE16385refuses the frame outright
the flow-control window65536accepts no more DATA until it grants credit
inbound, the same window65536stalls at exactly 65535 with nothing from us

A fix that added window accounting without splitting frames would still have failed at 16385. The wbuf row is the one with no symptom at all: mem_copy_bytes is a memcpy into a bump arena that records no allocation sizes, so nothing below could catch it. Demonstrated with a sentinel — 32 bytes into a 16-byte allocation changed the byte after it and reported 32.

Flow control makes the writer a reader. A send whose window is exhausted has to consume the WINDOW_UPDATE that reopens it, so send_data contains a reader.

An export list is not a coverage list

H2.read_settings read each SETTINGS entry's 32-bit value at offset i+4 instead of i+2 — two bytes past every six-byte entry, so the last entry of any payload read off the end. Wrong for as long as the file has existed, and nothing called it: the writer had a parity section from the start, the reader was exported, documented, and never fed a byte, and the server's own comment said its peer's settings were "read and ignored". The first caller found it on the first connection, at byte 42 of a 42-byte payload.

Enumerating every export and counting call sites turned up a second accessor in the same state — H2.error_code, zero callers and zero coverage. It happens to be right. http2_parity.sh now feeds every payload accessor.

What poisoning found that valid traffic could not

the check to "a billion bytes" left the section green: a client that acknowledges as it reads has already granted more credit by the time the next chunk arrives. The gate now has a rude client that advertises 8192 and stops reading.

two rules agree exactly when nothing has been spent. The SETTINGS now arrives mid-response, driving the window to -57343; negative is legal, and a reduction after credit was spent is the only way to get there.

returned to its caller. A peer may reopen a stalled stream with `SETTINGS` alone, and this server would have waited forever. Found only because the delta poison was undetectable for the same reason.

error; the frame loop ignored it and the window-wait loop failed on it. The wrong one was on the path every ordinary frame takes.

a 16384-byte write buffer min(peer, capacity) was always the capacity and reading the setting could not change the answer. The buffer is 65536 now, which makes the term live rather than deleting it.

Two wrong expectations, both the same slip

The boundary in the field is not the boundary on the wire: a 16384-byte reply field is 16393 bytes of DATA once the gRPC prefix, the protobuf tag and a 3-byte varint are added, so it already needs two frames. That got the frame-count expectation wrong, and later a window grant that left the server nine bytes short.

Also h2's local_settings.initial_window_size = ... is silently ignored before initiate_connection — the client advertised 65535, the server correctly sent 65535, and the harness blamed the server.

Streaming is the list having more than one element

The handler takes the request's messages as a bytes list and answers RpcOk, RpcStream or RpcErr; H2Server.unary adapts a one-in-one-out handler. There is no separate path for client-streaming. Interleaving is not supported and is said so rather than approximated: the handler runs after the request half-closes.

examples/hello.proto gained five methods and contrib/proto needed no change — stream was already parsed, and our descriptor for the new schema matches protoc byte for byte.

v0.1.282 — 2026-08-18

_The FFI byte arena gets a bytes bridge._

mem_to_bytes / mem_copy_bytes, the arena's two directions for data that may contain a zero byte — which mem_to_str cannot carry, because it stops there, and which every binary protocol has. Native and Wasm-component both, and scripts/socket_parity.sh requires the two to agree.

What it replaced. contrib/http2/server.mere was reading arena → hex → bytes (two characters per byte, three passes) and writing with `mem_set_u8` once per byte. The write is the one that mattered: a response is now one FFI call instead of one call per byte of it. Same shape as v0.1.222, where file_pwrite could not take a bytes and built one boxed int per byte.

The Wasm helper is a few instructions because the arena is linear memory there. The one thing to get right is that a bytes pointer points at the length, where a str pointer points at its data with the length at ptr-4; a comment says so, and poisoning the layout both ways is caught.

Two mistakes, and the second is the interesting one.

First, the definitions went where the rest of the arena lives — and the generated C said unknown type name 'mere_bytes', because b->data needs the complete struct and that block is emitted earlier. So they moved into the bytes runtime.

Then they were emitted there unconditionally, and __mem — the arena — is only emitted when a program declares an arena extern. Every program that uses bytes without the arena stopped compiling, which is most of them; proto_parity said so on the next run. Two independent conditions, and satisfying one is what made the other easy to miss.

The gate for that turned out to need one more thing: bytes_runtime was a top-level constant, evaluated at module-initialisation time — before any program is parsed — so a Hashtbl lookup inside it would have been false for every program that ever declared the externs. It takes the flag as a parameter now.

Poisoning found a coverage gap too: reading the length with i32.load8_u instead of i32.load passed, because on a little-endian machine the low byte of a length under 256 is the length, and every payload in the corpus was shorter than that. There are 255-, 256- and 300-byte payloads now.

v0.1.281 — 2026-08-18

_IEEE-754 bit access, in two halves because one would not fit._

float_bits_hi / float_bits_lo / float_of_bits, and f32_bits / float_of_f32_bits for float32. All five on all four backends, closing the last thing that stopped a protobuf double or float field from being generated.

The API shape was measured, not chosen. The obvious design is one accessor returning the whole 64-bit pattern. It does not work: read as a signed int64, a double's pattern exceeds this interpreter's native int — which is OCaml's, and 63-bit — for a large share of ordinary values. -1.5, 1e308, inf and nan all do. A single accessor would therefore answer differently on the interpreter than on every compiled backend, for a literal as plain as 1e308, and the honest options were a DIVERGE pin or a different API. Two 32-bit halves are each below 2^32, so there is nothing left to diverge about — and they are also exactly what a wire format wants, since it writes the bytes anyway.

f32_bits narrows to float32 with the backend's own double-to-float conversion, which is round-to-nearest-even. That matters more than it sounds: 1e308 has no float32, and the answer is +inf rather than a wrapped number. Writing that rounding by hand is the part nobody should have to get right twice.

contrib/proto/wire.mere gains put_double / get_double / put_float / get_float on top, so the split stays inside that layer and neither a caller nor a generated codec learns about it. The generator's refusal of double and float is gone, and scripts/proto_gen_parity.sh covers them — 25 schemas now, including 1e308, 1e-308, float32's maximum, and repeated floats, all byte-identical to protoc.

Two mistakes worth recording, both mine and both in the checking rather than the code.

The Wasm arm shifted with i64.shr_s. The top bit of a double's pattern is its sign bit, so an arithmetic shift sign-extends it and float_bits_hi (-0.0) came back as -2147483648 — a value that then compares as negative and divides the wrong way, on that backend only. It is i64.shr_u, and the comment now says why.

Finding it took two detours. First I read a stale binary: dune build 2>&1 | head -2 had exited on SIGPIPE, so the compiler under test was the poisoned one from a mutation run that a timeout had killed mid-restore. Then I "verified" the restore with grep -c 'i64.shr_u', which returned 1 — from a different, pre-existing occurrence elsewhere in the file. A check that cannot tell the two states apart is not a check; the verification is anchored to the arm now.

test/parity/float_bits.mere pins every class — normal, subnormal, both zeros, both infinities, NaN, the range ends — on four backends. NaN is checked through its bits rather than through ==, because == on NaN is false by IEEE and comparing it the obvious way reports a failure that is the comparison's own rule.

2026-08-18 — A protobuf code generator, and two branches nothing reached

_contrib/proto/gen.mere + examples/protoc_mere.mere: a .proto in, Mere source out. examples/grpc_hello.mere now uses it, and its hand-written codec is gone._


mere examples/protoc_mere.mere examples/hello.proto ../contrib/proto/wire.mere \
  > examples/hello_pb.mere

The codec in that example used to be a hand-written field walk and a two-line encoder. Both were correct, and both were a schema transcribed by hand — which is the thing a generator exists to stop.

scripts/proto_gen_parity.sh runs protoc's bytes through the generated codec and back: protoc --encodedecode_Mencode_M, byte-compared. 20 schemas. A round trip through the oracle's bytes catches more than it looks like, because a field the decoder ignores cannot be written back and shows up as a shorter byte string. The one thing it cannot see is a consistent swap of two fields holding equal values, so every field in the corpus has a distinct value.

Three representation choices, each forced by proto3 rather than chosen: a singular message field is a 0-or-1 list (message fields have explicit presence, so "absent" and "present but empty" differ — and it makes a recursive message expressible); an enum is an int and not a variant (a proto3 enum is open, and an unknown number has to survive a round trip); and a generated identifier never begins with the type name, because uppercase-leading is how this language recognises a constructor — M_get_a parses as one and is reported as an unknown constructor at its use site, naming something the schema author never wrote.

The encoder writes fields in field-number order, not declaration order, because protoc does.

Poisoning found two branches nothing reached:

  1. The repeated zigzag and fixed families were not in the corpus. It had singular

sint32 and repeated sint64, so the repeated-sint32 path was generated and never executed — breaking it changed nothing.

  1. protoc always writes packed, so the decoder's unpacked branch was never reached.

proto3 requires a decoder to accept both forms whatever the writer chose, and deleting that branch left the harness green. A decoder that only understands the encoding its own oracle emits works against exactly one kind of writer.

The second one needed input protoc does not produce, so the harness builds it — and the first hand-typed version had field 2's tag as length-delimited, which protoc rejected and the harness reported as its own bug, correctly. Hand-written test bytes are a transcription like any other, so they are computed from the values now. protoc is then asked whether the two spellings mean the same value rather than being told.

2026-08-18 — A .proto parser, and the bootstrap closing

_contrib/proto/parse.mere + contrib/proto/descriptor.mere: a .proto file in, a FileDescriptorSet out, byte-identical to protoc --descriptor_set_out across 72 derived files._

The bootstrap closes here, and that is why it is one harness rather than two. A descriptor set is itself a protobuf message, so the code that reads a schema is serialised by the code that reads wire bytes. If the varint encoder is wrong, this diff says so; if a descriptor field number is wrong, the same diff says so. One oracle checks both layers at once.

The comparison is bytes. A descriptor that decodes to the same text but different bytes is still a different descriptor to anything that hashes or caches it.

Three things were measured off the oracle before anything was written, and each would have been wrong from memory:

`descriptor.proto` is proto2, which reverses a rule the wire harness had just learned. A set field is written even at its default, so an enum value's `number: 0` appears on the wire — where proto3 omits a scalar holding its default and `v: 0` could not be swept at all. Same encoder, opposite rule, decided by the schema being encoded rather than by the encoder. `json_name` is not plain camelCase. my_fieldmyField is the easy one; a_b_caBC, trailing_trailing, __leadLead and num_2_xnum2X are the ones a guess gets wrong. The rule is "drop underscores, upper-case what follows each one". `type_name` carries a leading dot and is fully qualified, and a reference resolves from the innermost scope outward — so the same simple name is a different type at a different depth, and the wrong order still produces a well-formed descriptor.

The last of the 72 files to match differed by two bytes. rpc U (Q) returns (R) {} is not the same as rpc U (Q) returns (R); — the empty body sets MethodOptions to an empty submessage, so protoc writes 22 00 there and nothing for the semicolon form. Presence with no content is the whole difference.

The subset is refused, not skipped. oneof, map, import, option, reserved, optional, extend, group and proto2 are outside it, and protoc accepts every one — so the oracle cannot be asked whether they are wrong. The harness checks instead that each is refused by name, because a skipped construct produces a descriptor that is wrong where nothing looks. Getting that right needed two fixes the corpus found: an absolute type reference (.pk.M) is a different production from a name and needed its own reader, and a field option list met "expected ';'" — a syntax error about a file that is syntactically fine — until it was given a named refusal.

Ten poison mutations, none uncaught.

2026-08-18 — RPC statuses, and an expectation the wire corrected

_A gRPC handler can now fail._

bytes alone could not say "this failed", so an unknown method got a reply that looked like a success. A handler now answers RpcOk of bytes or RpcErr of (int * str), and a failure goes out as a status in the trailers — the request succeeded as HTTP and failed as RPC, so it cannot be expressed as a 4xx, and a failed unary call carries no DATA frame at all.

`grpc-message` is percent-encoded, which is neither optional nor cosmetic: the specification says so and grpc-go decodes it. Measured before implementing — caf%C3%A9 %25 done came back as café % done — so a raw % would have been read as the start of an escape.

And then the wire corrected the harness. The first expectation written for the raw trailer assumed a space and an apostrophe would be encoded. They are not: the rule is "bytes outside printable ASCII, plus % itself", the same as grpc-go's own encoder. The implementation was right and the expectation was a guess; the failing diff is what said which.

The DOCUMENTED-GAP that asserted the absence of statuses did its job on the way out: closing the gap made that assertion fail and report itself stale. Three routes are checked in its place — a method the schema declares and the server does not handle, a method that exists and refuses its input, and a message that needs encoding — and the python client now reads the raw trailers. That is the observation grpcurl cannot give: it prints the decoded message and the rendered code name, so whether the encoding happened is invisible there. Poisoning found nothing uncaught across five mutations, including lower-cased hex digits.

2026-08-18 — A GraphQL executor, and the one case out of 67 that mattered

_contrib/graphql/exec.mere plus scripts/graphql_exec_parity.sh against graphql-js's execute()._

The resolvers are the data. A field is resolved by looking its name up in the parent object, which is graphql-js's default. Both sides get the same schema, the same document and the same root value, so nothing about resolution is transcribed — two hand-written resolver sets would drift and the drift would look like an execution difference.

What that arrangement really exercises is null propagation, and it is why an oracle earns its place. A non-null field that resolves to null is not an error in that field: it destroys the nearest nullable ancestor. { nn } answers "data": null. An item error in [Int!] nulls the whole list with a path of ["ln", 1]. All of it was measured off the oracle before being implemented, because reading behaviour off a specification and reading it off a running implementation are different activities.

Every position is one of two things — GOk (value, errors) or GBubble errors — and the four cases in the specification fall out of those two constructors instead of needing exceptions the language does not have.

Of 67 corpus cases exactly one differed, and it was the interesting one. { o2 { nnx } } with o2: O! produced two errors: the real one about O.nnx and an invented one about Query.o2. The oracle produces one. When a non-null position is null because an error already propagated, no second error is raised — the check that would raise it is never reached, because the propagation went past it. The fix splits the job: raw completes a position with no absorbing, and complete decides what the position's nullability means. A list item goes through complete, so [O] nulls the item and [O!] nulls the list, which now falls out rather than being special-cased.

Poisoning the gate ten ways caught nine immediately. The tenth is worth recording as a coverage lesson: breaking the named fragment's type-condition check went unnoticed because the corpus only had the inline form. Two nearly identical branches, one of them unchecked. Three named-spread cases closed it, and the poison then failed as it should.

Error messages are compared verbatim — the specification does not fix the prose, so matching the reference implementation is the only way to compare them at all, and the oracle is pinned. locations are stripped from both sides: they need source positions the parser does not carry, which is a stated gap rather than an accident.

v0.1.280 — 2026-08-18

_A capture whose name has a dot in it._

An inner let rec that referenced a module-qualified name put that name into the closure's environment struct verbatim, so the C backend emitted


long long Wire.delimited;

and the generated C failed to parse. The error pointed at a line the program did not contain, which is the part that makes this worse than a refusal: the diagnostic was about generated C, not about the source that caused it. The interpreter was always correct, so nothing saw it until a dogfood compiled a wire-format decoder with Wire.delimited inside a field-walking loop.

The shape is narrow, and measuring which variants were affected is what located it. Four were fine — M.k at the top level, a top-level closure using it, that closure passed to a function, and an inner let rec capturing only its enclosing parameter. Only an inner let rec referencing a qualified name reaches the environment-struct emitter instead of being referenced directly.

The fix is that two paths now agree. The inner-LIFTED environment (__env_local->…) already ran capture names through flatten_module_dots; the anonymous-closure environment did not. Three sites — the struct's field declaration, the adapter's substitution, and the creation site that fills the environment in — now flatten too, with the substitution still keyed on the name as written because that is what Var n in the body says.

flatten_module_dots is the identity for a name without a dot, so every program that did not hit this emits byte-identical C. And every output that does change was previously invalid C — a dot in a field name has never been legal — so there is no program whose working output moved.

test/parity/module_qualified_capture.mere pins it on all four backends, including the variants that were not broken, so a future change cannot fix one path and regress the other.

2026-08-18 — A gRPC server in Mere, and two bugs loopback was hiding

_contrib/http2/server.mere plus examples/grpc_hello.mere. grpcurl asks a Mere server for a greeting and gets one._


$ grpcurl -plaintext -protoset hello.protoset -d '{"name":"mere"}' \
    127.0.0.1:50079 hello.Greeter/SayHello
{
  "message": "hello mere"
}

Everything under that line is Mere: the protobuf wire format, the HTTP/2 framing, HPACK, and the connection. TLS is absent by measurement rather than oversight — h2c is what grpcurl -plaintext speaks, 24 literal bytes and no negotiation.

scripts/grpc_parity.sh is the first harness here that does not compare bytes. Every layer underneath already has a byte-level gate; none of them answers whether a client that knows nothing about any of it gets a reply it recognises. Two clients, because they ask different questions: grpcurl (Go) over four connections including an empty proto3 request, and a python h2 client making three requests on one connection — the case grpcurl cannot express for a unary method, and the one that catches a per-stream HPACK decoder.

Poisoning it found two bugs that every other section passed:

  1. Removing the frame reassembly changed nothing. On loopback a request arrives

in a single read, so a server that parses whatever one read handed it works. The comment in server.mere had said reassembly "is not a simplification, it is a bug that happens to pass on a fast loopback" — and the harness proved the comment right by failing to catch its removal. The python client now sends one request split every seven bytes, so the 9-byte header itself straddles two reads.

  1. Removing the SETTINGS acknowledgement changed nothing, because neither client

blocks on it. The client now asserts the ACK arrives.

Both are the mirror of the dead guards found in frame.mere yesterday: there, code that nothing reached; here, behaviour that nothing observed.

Two harness bugs were worth more than they cost. The readiness check waited for the port by connecting to it — which the server counts, since it serves a bounded number of connections and then exits, so the probe ate one and the last real call got "connection refused" while the harness blamed the server. It waits for the server's own "listening" line now. And the unknown-method section printed its DOCUMENTED-GAP without calling anything: a claim, not a check. It calls a method the schema declares and the server does not handle, and requires the documented answer.

One compiler bug came out of writing the example and is recorded rather than fixed: an inner let rec that references a module-qualified constant makes the C backend emit the qualified name as a struct field — long long Wire.delimited;, which is not valid C. Six lines reproduce it, the interpreter is correct, and the error names generated C rather than the program that caused it.

2026-08-18 — HPACK, and three ways a gate can pass without checking anything

_contrib/http2/hpack.mere plus a generated table and a seven-section gate. The implementation went green on the first run; poisoning it is what produced the interesting part._

What had to be complete was measured. grpcurl 1.8.8's first HEADERS frame on a real connection uses static-table indices, Huffman-coded literals and seven dynamic-table insertions — so the decoder is complete (integers, both string forms, all five instructions, eviction) while the encoder is trivial: literal, new name, no indexing, no Huffman, which RFC 7541 permits and which a real client was confirmed accepting end to end. That asymmetry is why a gRPC server is reachable without writing a Huffman encoder.

The dynamic table is connection state and is threaded through the API rather than hidden in a global, because one decoder per connection is correct and two is not, and a parameter makes that visible at the call site.

The tables are generated from the same library the gate uses as its oracle, which is a hole a differential test cannot close: a shared transcription error is invisible. So one section decodes a header block captured from grpc-go 1.57 — a third implementation, in another language, that never saw either table. Nine header names and the table's own accounting come back matching.

Then the gate was deliberately broken, nine ways. Six were caught immediately. The other three were the point:

  1. Valid input does not test a refusal. Removing the Huffman padding check

changed nothing, because well-formed blocks have well-formed padding. Nine malformed blocks now exercise the refusals — the same lesson the GraphQL reject-list taught, arriving again in a different file.

  1. "Does it refuse" and "does it say what was wrong" are different questions.

Removing the index-0, EOS and string-length checks still refused, further downstream, with a message about something else — so the gate stayed green while three named diagnostics had been deleted. Each malformed case now asserts a substring the refusal must contain. That couples the harness to our own wording, which is the acceptable direction; coupling it to the oracle's would be brittle for no gain.

  1. A check whose input never arrives passes. The case file was written with two

columns while the reader expected three, so every expectation was the empty string and grep -q "" matched every message. An empty expectation is now a failure in itself — and so is a reject-list that checked zero cases, which is the rule the builtin matrix learned in another form: a gate reporting ok for no cases cannot be told apart from one that did not run.

One line survives poisoning on purpose. len2 < huff_min_len is a fast path, not a checkhuff_count already answers 0 below the minimum length — and it is labelled so the next reader does not have to work that out. The contrast with the lshr mask deleted yesterday is deliberate: that one was a guard that could not fire, this one is an optimisation that provably cannot change an answer.

2026-08-18 — GraphQL's type-system half, and HTTP/2 frames

_contrib/graphql grows SDL; contrib/http2/frame.mere arrives with a gate against hyperframe. Both gates were then deliberately broken to see whether they could fail, and both had blind spots._

SDL — schema / scalar / type / interface / union / enum / input / directive, with descriptions, implements A & B, argument definitions and defaults. The derived corpus goes from 153 to 366 documents and the round-trip covers all of it unchanged, because graphql-js's print handles type-system definitions too.

Then the half that was missing: everything so far fed valid documents, and a parser that accepts anything passes all of it. A reject-list of 37 documents the oracle rejects found six real defects on its first run:

`{ }`, `type T { }`, `enum E { }` and `input In { }` were all accepted. Every delimited list in this grammar needs at least one element except the two that are values — `[]` and `{}` are legal, `{ }` as a block is not. One helper that names the production replaced seven call sites that each returned `[]`. 007 was accepted (an IntValue may not have a leading zero) and so was 1. (a fraction needs a digit). The lexer now also refuses a number followed by a digit, a . or a name start, so 1.2.3 is an error rather than two tokens.

A harness defect came out of the same exercise: set -e plus a crashing subject made the script stop mid-run having printed no verdict, hiding every later section. The subject now runs through a helper that never aborts and names the section it failed in. set -e stays for the preflight, where a missing oracle should stop everything.

Type-system extensions (extend ...) are refused rather than mis-parsed — reading extend type T as type T would produce a tree that says something the document did not — and the refusal is asserted, so the day extensions land that section fails and says the assertion is stale.

HTTP/2 frames — the preface, the 9-byte header, SETTINGS / WINDOW_UPDATE / RST_STREAM / GOAWAY payloads, and the gRPC message prefix. hyperframe is the oracle: 50 frames byte-identical on encode, the same 50 compared on fields on decode, with the sweep crossing type × flags × stream id × payload size (0, 1, 2, 255, 256, 16383, 16384 — every length-encoding boundary).

One byte has two independent confirmations, which is the only kind of agreement worth having: an empty SETTINGS frame serialises as 000000040000000000, and that is byte-for-byte what grpcurl 1.8.8 was observed sending on a real connection. hyperframe and grpcurl never met.

Poisoning that gate found two things the sweep could not reach:

The `lshr` mask was dead. Every shift here is immediately masked with 255 to extract a byte, and the bits an arithmetic shift copies in sit above bit 7, so a logical shift gives the same answer. Removing the mask changed nothing, which is the definition of dead code. It is not dead in `contrib/proto`, where the shifted value is the loop variable and a negative int64 would never terminate — the contrast is now written down in both files. The writer's stream-id mask was never exercised, because every stream id in the sweep already has the reserved bit clear. A section now writes 0x80000001 and requires the same frame as stream 1. The decode direction was already covered: 0x80000001 must read as 1, not 2147483649.

Both are the same shape — a guard nothing reaches is indistinguishable from a guard that is wrong.

2026-08-18 — Two protocols: the protobuf wire format, and GraphQL documents

_contrib/proto and contrib/graphql, each with a gate against somebody else's implementation._

`contrib/proto/wire.mere` — the Protocol Buffers wire format, both directions, below any schema. scripts/proto_parity.sh holds it to protoc's bytes: one message containing every wire type compared whole, swept value lists crossing every varint length boundary, the int64 edges, and protoc's own bytes read back and rewritten. Interpreter and C backend both.

Three things the harness had to be taught, each by being wrong first:

`bit_shr` is arithmetic, so a negative int64 shifted right never reaches zero and the encoder loops forever. Every right shift here is masked. 0 cannot be swept. proto3 omits a scalar holding its default, so protoc --encode answers the empty string for v: 0 while a schema-less layer writes 0800. That is a question one layer up, not a wire-format disagreement. The zigzag bound is half the varint bound, because zigzag doubles its input.

And one thing that is pinned rather than fixed: the interpreter's int is 63-bit (OCaml's native int) while every compiled backend's is 64-bit, so above 2^62 the same program gives different answers — bit_shl 1 62 is negative on the interpreter and bit_shl 1 63 is zero. Those are recorded as a DIVERGE pin, not a tolerance, so the day the interpreter becomes 64-bit that section fails and says the pin must be retired. Related: an integer literal above 2^62−1 cannot be written at all — it dies with an uncaught Failure("int_of_string") — so the edge values in the harness are computed from 2^62−1 instead. This is the int axis of the open question about builtin parity at particular values, and it is the same shape as the float axis before exponent literals landed in v0.1.260: the reason it was not measured is that it could not be written.

`contrib/graphql` — lexer, parser and printer for executable documents (operations and fragments; SDL is a separate grammar and is not here). scripts/graphql_parity.sh checks it against graphql-js by sending our output back through their parser:


theirs: print(parse(D)) -> A     ours: print(parse(ours(D))) -> B     assert A == B

print is a function of the AST alone, so equality holds exactly when the parses agree — and nothing transcribes an AST. The alternative, serialising both trees into a shared format, needs a serialiser for the oracle's tree that can hold the same misreading as the parser it checks; a differential gate that shares a bug with its subject reports agreement. The corpus is derived rather than written down: every value kind × every position admitting a value, every selection form, every operation shape, nested type expressions. 153 documents.

The gate was then deliberately broken to see whether it could fail. Dropping every directive in the printer: caught. Dropping field aliases in the parser: caught. Making the lexer call every number an IntValue: not caught — the printer emits a numeric literal's lexeme unchanged, so IntValue "1.0" prints 1.0, the oracle re-parses it as a FloatValue, and the two printed documents agree. A round-trip is blind to any distinction that prints identically. A fourth section now asks for the kind directly, and it is the one place that transcribes anything from the oracle's tree — a two-word vocabulary.

The printer refuses a block string whose value would not survive re-parsing rather than emitting an ordinary string: the dedent rule is applied again on the way back in, and downgrading it would round-trip the text while losing block: true from the tree — a wrong answer wearing the face of a parser bug.

Both gates are in CI, both run under dash before being put there, and both oracles are pinned and printed (protoc 27.3, graphql-js 17.0.2).

v0.1.279 — 2026-08-17

_The refusals that were left, and three bugs they walked into._

The enumeration's tail: bytebuf_get / bytebuf_set, write_file_bytes's byte range, len on a value that is not a list, and comparing two functions. Asking them turned up three things that were not about refusals at all.

A program that used a ByteBuf and no `bytes` value did not compile. The C backend emits the bytes runtime when bytes_used is set, and freezing a ByteBuf calls __lang_bytes_alloc — the comment above the emit even says so, so the order of the two runtimes had been thought about and the dependency had not. Third time a use-gate has emitted a call to a function it did not emit.

`len (Some 1)` emitted C that dereferenced `payload.Cons` on an option. The guard for the cons-walking branch asked whether the program contained Nil and Cons anywhere, not whether this type has them, so any program that also mentioned a list took that branch for every polymorphic variant. It asks the type now, and a type with no length is a clean codegen refusal naming the alternatives.

Comparing two functions was a runtime failure on the interpreter and invalid C on the compiled backends== between two closure structs does not compile. The type is known where the comparison is written, so the typer answers there now, with the interpreter's own words.

The refusals themselves: ByteBuf's index failures and the byte-range check both printed and called exit(1), so try_or could not take them and the program stopped where the interpreter carried on. They are catchable failures now, with the messages they already had. test/parity/bytebuf_edges.mere is the gate — interp + C, since bytes/ByteBuf is those two backends by scope.

Also: __lang_str_count still returned i32 on LLVM after v0.1.276 widened it on C and Wasm — a count past two billion came back negative. The sweep missed exactly one backend of the three.


v0.1.278 — 2026-08-17

_The last of the oracle's refusals._

The bytes value has the same index surface as vec, and it had all the same problems one slice later than vec did:

beforeafter
C, LLVMabort() — exit 134, and nothing try_or could catchthe interpreter's catchable failure
Wasm bytes_gettrapped with no message, after truncating the index to 32 bitschecked at full width, and it says what happened
Wasm bytes_sliceno range check at all — copied from wherever the arithmetic pointedrefused

random_int on the Wasm host accepted any bound. There is no RNG wired there yet, which is a documented limitation — but "no RNG here" and "any bound is fine" are different statements, and the second one is a wrong answer. The bound is checked now even though the value it returns is still a deterministic zero.

That closes the enumeration started in v0.1.276: every refusal the interpreter raises is now raised by all four backends, with the same words, catchable in the same way. test/parity/refusals.mere covers the caught side and there are 15 programs in test/parity/fail/.


v0.1.277 — 2026-08-17

_Finishing the enumeration, and two things it turned up on the way._

v0.1.276 asked the oracle's refusals and fixed the nine it had probed. This is the rest of that list.

`bool_of_str` answered false for anything that was not "true" — on all three compiled backends, with a comment in the C source stating that this "matches interp". The interpreter has never done it: it refuses any word that is not one of the two. A claim in a comment is not a check.

`float_of_str` was three different wrong answers to the same program:

inputinterpC / LLVM (atof)Wasm (parseFloat)
"abc"refuses0.0nan
"1.5x"refuses1.51.5
"1_000.5"1000.51000.5`1.0`
"inf"infinf`nan`
"0x1p3"8.08.0`0.0`
""refusesrefusesnan

atof reads a prefix and has no way to say "that was not a number"; on Wasm the host's parseFloat has the same problem plus a smaller idea of what a float is, and its failure value is NaN — which is indistinguishable from the float nan, so nothing could be refused there at all. Validity now comes back from the host separately, because one return value cannot say both.

The oracle is float_of_string (String.trim s), which accepts rather more than a decimal point: hex floats, inf / infinity / nan in any case, signs, and underscores as separators (1__0.5 is 10.5). All four backends now agree across all of it, and test/parity/refusals.mere checks fourteen spellings.

Two things the new gate found on its way in:

makes the output shorter — so the length header said the wrong thing, and str_len (str_unescape "a\nb") was 4 on C and LLVM where the interpreter and Wasm said 3. Printing the value wrote a trailing NUL, which is invisible in a terminal. It took a case that printed an unescaped string to see it.

adding an import to the module without adding it there makes instantiation throw inside the worker — after which the main thread waits on a generator that will never yield. That is a hang, not an error: it cost twenty minutes of a parity run before I looked at the process list.


v0.1.276 — 2026-08-17

_Asking the question mechanically, after finding the same shape by accident twice._

v0.1.274 found string sizes narrowed to 32 bits; v0.1.275 found indices narrowed the same way, one layer down. Both were found by a probe that happened to ask. So the third time the question was asked from the other end: every refusal the interpreter can raise, enumerated from eval.ml, checked against what the compiled backends do with the same input.

Nine diverged, and always in the same direction — the compiled backends had no check at all and returned whatever the byte or pointer arithmetic produced:

inputinterpC / LLVM / Wasm (before)
chr 256refusesa NUL — the cast to unsigned char was the domain check
chr (0-1)refuses0xFF
chr (2^32+65)refuses"A"
ord ""refuses0 (it read byte 0 whatever the length was)
ord "ab"refuses97
str_repeat s (0-1)refuses""
str_unescape "a\q"refuses"aq" — an undefined escape became the letter
owned_vec_get v 5refusesabort(), which try_or cannot catch

Every one is a value a program can produce by accident and then keep computing with. All four backends now raise the interpreter's own failure, with its exact words: chr: 256 out of byte range [0, 255], ord: expected single-char str, got length 2, str_repeat: negative count -1, str_unescape: unknown escape '\q'. test/parity/refusals.mere is the caught side; uncaught_chr_range and uncaught_ord_length join the uncaught ones.

The same sweep turned up three more narrowings and closed them: str_count's result, strbuf_len's result, and sleep_ms's argument were all int.

A note on method: the harness for the first measurement had a bug of exactly the kind these slices keep finding — when a backend failed to compile, the shell variable kept the previous backend's output, so one column of the table was a copy of another. It was caught because random_int has no LLVM lowering and the "LLVM" column reported values anyway. A measurement that cannot fail loudly is not a measurement.


v0.1.275 — 2026-08-17

_An index the collection does not have — answered, for years, with an element._


let v = vec_new ();
let _ = vec_push v 10;
let _ = vec_push v 20;
vec_get v 4294967297     // interp: refuses.  C, LLVM, Wasm: 20.

An index is a Mere int, sixty-four bits of it, and every compiled runtime took one as thirty-two. The bounds check then ran on the truncated value, so 4294967297 was checked as 1 and a two-element vec cheerfully returned its second element and exited 0. All three compiled backends agreed with each other, and none of them agreed with the interpreter. No test caught it because every index in every test was small — the same blind spot the string widths had in v0.1.274, one layer down.

Out of range was its own mess, three ways at once:

beforeafter
interpcatchable failure naming index and lengthunchanged — it was the oracle
Cfprintf + abort(): exit 134, and nothing try_or could catchthe interpreter's failure
LLVMabort(), no messagesame
Wasmunreachable: a trap, no messagesame
char_at / substring, all threeread past the end and returned what was theresame

Every backend now raises the interpreter's own failure, which names both numbers at full width: vec_get: index 4294967297 out of bounds (len = 2). On C and LLVM the message is built with snprintf — LLVM's needed its varargs signature spelled out at the call site, or the arguments arrive through the wrong ABI slots and the message prints numbers nobody passed. Wasm has no snprintf, so it builds the sentence from interned parts with its own show_int and str_concat.

test/parity/index_edges.mere is the caught side (nineteen positions, in range, one past the end, negative, backwards, and past what 32 bits can name); uncaught_vec_index, uncaught_char_at_index and uncaught_substring_range are the uncaught side.

What it found immediately. The Ruby subset's utf8_cp decodes a sequence's continuation bytes before checking they exist, and on binary data — where any byte ≥ 0xC0 looks like a lead byte — the last one sends it past the end of the buffer. It had been reading out of bounds on every SHA-1 call for as long as digest has existed, and nothing showed: the read returned the NUL terminator and the value was discarded, since only the sequence length is used. A check that refuses is how a read like that stops being invisible.


v0.1.274 — 2026-08-16

_A string the machine cannot hold, and two backends that answered with a number._

Asking for str_repeat "ab" 500000000000000 used to produce four different things, and the two most used backends produced a plausible integer and exit 0:

beforeafter
interpFatal error: exception Out of memory, exit 2out of memory, exit 1
C-1530494976, exit 0out of memory, exit 1
LLVM2764472320, exit 0out of memory, exit 1
Wasm(no output), exit 1out of memory, exit 1

Two defects compounded to make that possible.

Mere's int is 64-bit; the runtimes serving it were not. The count reached C through an int parameter and LLVM through an explicit trunc i64 ... to i32, so a 64-bit value arrived as its low 32 bits and asked for a string the program could actually have — a different string, returned without complaint. The same narrowing was in str_len ((int) __lang_str_size), substring's indices, str_index_of's result and utf8_len's count. A 2.15GB string — one byte of address past what a 32-bit offset can name — reported a negative length and a negative match offset. Every string in every test until now was small, so the axis had never been asked.

The allocator never read what malloc answered. When it said no, the next line wrote through the null it returned, and the program died by segfault — the nameless death v0.1.271 removed everywhere else. It is a named, catchable failure now, on both backends, and the region's doubling no longer wraps size_t on its way to a request bigger than half the address space.

On Wasm the memory is a fixed 64MB and nothing grows it, so exhaustion arrives as an out-of-bounds trap. The host used to exit 1 in silence for every trap that was not fail; it now names that one out of memory and prints the engine's own words for anything else, so no trap is anonymous.

Gated two ways. test/parity/fail/uncaught_out_of_memory.mere holds all four backends to the same sentence on a request no allocator can satisfy — it is refused instantly, so it costs nothing. scripts/bigstr_check.sh is the 2.15GB measurement, deliberately not in the parity run: it costs 4.3GB of resident memory per backend, and a gate too expensive to run is one people stop running. The Wasm backend is not in it, because a 2GB string is not a value that backend can hold — asking it would measure the memory limit rather than the width.


v0.1.273 — 2026-08-16

_A gate that cached the thing it was testing._

scripts/selfhost_check.sh compiles a set of programs with both compilers -- the OCaml one and the self-hosted Mere-in-Mere one -- and diffs what the two binaries print. Run yesterday it reported 7 failures out of 7, every case, with the self-host side empty: the reading a person would take from that is that the self-hosted compiler is completely broken.

It was fine. The gate builds the self-hosted compiler into /tmp/selfmere.wasm only if that file does not already exist, and the copy sitting there was three days and one ABI change old -- built before str grew its length header. The gate was testing a compiler nobody had asked about.

CI never disagreed. A fresh runner starts with an empty /tmp, so CI always built the current compiler and always passed. The same commit was green on the machine nobody looks at and red on the machine someone is working on, and the red one was the wrong answer.

It builds every run now. The whole build is 260ms -- there was never enough here to cache. A sweep of the other gates for the same shape (reuse an artifact if present) finds none.

The failure report is also bounded now. When this failed it wrote 18MB of WAT into the terminal, and the part worth reading -- that one side was empty -- was the first line of it. Ten lines of each side and the paths, so the rest is one command away.


v0.1.272 — 2026-08-16

_Q-032 closed: the Wasm backend learned to unwind, and the last pin came down._

fail on this backend has nothing to unwind with. It sets a flag, returns a sentinel, and the try_or at the boundary reads the flag -- so the failure was caught, but everything between the fail and the catch still ran. A body that pushed three strings and failed after the second pushed the third here and nowhere else. That one line was the parity suite's last DIVERGE pin, standing since v0.1.246.

Unwinding, written out by hand: after a call, ask whether the callee failed and return at once if it did. The check goes in at emit_instr -- the one place every call passes through -- rather than at each emission site, which is how the sites that were forgotten in earlier sweeps would have been forgotten again. A return_call is exempt: it is a tail call, the frame is already gone. So is try_or's own call to the thunk, which is the frame that must not propagate.

The interesting half is the second question: which calls need to ask. A callee that cannot reach $__lang_fail cannot have set the flag, and the assembled module can prove that where the emitter could not -- so a pass over the finished module computes reachability and takes those checks back out. On a self-hosted compile that is 2,524 of 3,722 call sites:

modulevs no unwinding
no unwinding (before)241,915 B
a check after every call275,273 B+13.8%
dead checks pruned253,067 B+4.6%

Everything unknowable keeps its check: indirect calls (the callee is a table index), imported functions (the host can re-enter the module), and any callee without a definition in the module. Removing a needed check would be a silent wrong answer, so every doubt resolves toward keeping it.

scripts/parity.sh also learned to fail on a stale pin. A pinned divergence that starts matching used to pass quietly, because a matching case never reads its .expected file -- the declaration would have stayed on disk saying something that had stopped being true. That is the exact failure the pin mechanism exists to prevent, so the gate now names the file and asks for it to be deleted. It is how this slice found out it was done.


v0.1.271 — 2026-08-16

_The failure that had no name._

Recursion deeper than the stack is the most common way a Mere program actually dies -- three of this week's findings were exactly that -- and until this slice it was the one failure the language never said anything about. Four backends gave four answers, and the two most used gave none:

beforeafter
interpFatal error: exception Stack overflow, exit 2stack overflow (recursion too deep), exit 1
C(nothing), exit 139same sentence, exit 1
LLVM(nothing), exit 139same sentence, exit 1
Wasmnode's RangeError + hundreds of trace frames, exit 1same sentence, exit 1

The compiled backends carry a SIGSEGV/SIGBUS handler that runs on a stack of its own -- the stack that just overflowed has no room left to run a handler -- and it claims a stack overflow only when the faulting address is near the stack. Anything else keeps its own name: a segfault from some other cause is still reported as a segfault, because a diagnostic that guesses is worse than one that does not exist. The bounds are read from the thread rather than assumed, which is what makes the answer right for a program linked with a bigger stack -- a 512MB one, as the Ruby subset uses, overflows far below any 8MB guess and would otherwise have been misnamed.

The interpreter's limit is declared, not discovered. OCaml 5 grows the main fibre's stack by copying it, so finding the host's real ceiling costs 68 seconds and gigabytes of copying, and the depth it finds -- around forty million frames -- is two orders of magnitude past anything a compiled backend can reach. A program that recurses that far has already failed everywhere else. The interpreter now stops at 1,000,000 frames (MERE_MAX_DEPTH to move it) and says the same sentence in about a second. The count comes back down with the stack it was counting, so a program that catches a failure inside a loop does not drift upward into a depth it is not at.

test/parity/fail/uncaught_stack_overflow.mere is where the agreement is checked rather than claimed: 8 failing-program cases now, all four backends matching on exit status, prior output and message.

This also corrects v0.1.270's closing paragraph, which named the region as the cause of a crash nobody had measured. It was the stack.


v0.1.270 — 2026-08-16

_The other helper that could not survive a long list, and a sweep that says there is no third._

list_filter was fixed two slices ago for rebuilding its result through the return path. The obvious next question is whether it was the only one, and asking it the lazy way — grep the prelude for a self-call wrapped in a constructor — turns up exactly one more: `list_sort_insert`, which walks to the insertion point through Cons (h, list_sort_insert cmp t x) and so recurses once per element it passes. Inserting into a 50,000-long sorted list overflowed the stack on the compiled backends.

It accumulates and reverses now. The same sweep over the whole prelude afterwards finds no remaining wrapped self-call: list_map, take, concat, flat_map, zip, append, the merge-sort quartet and the utf8 helpers were all already in that shape, and the two that were not are both fixed.

Worth separating from the depth question: each list helper handles a 50,000-element list on its own, and a program that builds several such lists at once stops earlier on the compiled backends. That is the region running out, not the stack -- a different axis, and one this measurement deliberately does not mix in.

Corrected in v0.1.271. That last paragraph is a guess written as a finding: the crash was never measured, only named. It is the stack. The same program runs to completion under ulimit -s 65520, and under v0.1.271 it says so itself. The region was not involved.

v0.1.269 — 2026-08-16

_A match arm is a tail position too, which is where nearly every recursive list helper's tail call actually lives._

v0.1.267 gave the LLVM backend a notion of tail position and emitted musttail, and the measurement that immediately followed it — the collection value axis — said list_sum over a list still died at 100,000 there while C was fine at 200,000. The reason was narrow: only `If` had been made tail-aware. Every recursive helper in the prelude is written with match, so its tail call sat in an arm that branched to a join and phi'd — the one shape musttail cannot take.

An arm in tail position returns now, exactly as a tail-position If branch does. list_sum runs at 500,000 on LLVM, and a program that puts a 300,000-element list through every list helper — len, sum, rev, map, filter, fold — gives the same answers on the interpreter, C and LLVM.

The lesson is about where the measurement pointed. "Tail position" sounded like one concept and was implemented as one construct; the gate written an hour later found the other construct by asking a question that had nothing to do with tail calls.


v0.1.268 — 2026-08-16

_A missing map key is catchable everywhere, and list_filter survives a list longer than the stack._

The third value-axis parity case — after the integer widths and the string edges — asks about collections: the empty ones, the single-element ones, a key that is not there, and a list long enough to leave the small cases behind. It found two.

A missing map key was not catchable on any compiled backend. map_get wrote its own diagnostic and then abort()ed (C, LLVM) or executed unreachable (Wasm), so try_or (fn () -> map_get m k) d answered d on the interpreter and died with 134 or 1 everywhere else. It goes through the same failure path every other fail uses now: caught, it returns the default; uncaught, all four print the same sentence and exit 1.

`list_filter` recursed to the length of the list. Every other list helper in the prelude accumulates and reverses — list_map, list_take, list_concat, list_zip, list_append all do — and this one built Cons (h, list_filter t p), so filtering 100,000 elements overflowed the stack on a backend where mapping them did not. It accumulates now.

The case is sized at thirty thousand deliberately. Past that the axis stops being about values and becomes about stack depth, which differs per backend and per operation — list_sum alone survives 80,000 on LLVM and dies at 100,000 while C is fine at 200,000. A value gate that also measured the stack would report the wrong thing whenever either moved.


v0.1.267 — 2026-08-16

_The LLVM backend's tail calls are musttail, so a loop written as recursion runs in constant stack there too._

The C backend learned this in v0.1.230 — self tail calls became a goto — and the same measurement was left standing against LLVM: ten million iterations died with SIGSEGV at `-O0` while C ran them in constant space, and the emitted IR carried no tail marker at all. The note recorded it as measured rather than assumed, and said what the fix would cost: musttail is the marker LLVM guarantees regardless of optimisation level, but it requires the call to be immediately followed by a `ret` of its result, and this backend had no notion of tail position.

It has one now. emit_expr carries the same tail-position flag the C backend uses, cleared at entry and restored only for the sub-expression that stays in tail position. An If in tail position returns from each branch instead of joining through a phi, which is what puts a tail call next to its ret. A call in tail position whose prototype matches the enclosing function's is emitted as musttail.

Ten million iterations now run at -O0, and so does mutual recursion between two functions. MERE_NO_TAIL_CALL=1 turns the transform off, the same escape hatch MERE_NO_TAIL_LOOP gives the C side — "is it this change?" stays a one-variable question.


v0.1.266 — 2026-08-16

_The string values four backends were never asked about, and the two answers that were wrong._

A new parity case walks the axes the open question about builtin parity names next to the width one it already closed: the empty string, an index that is not inside the string, and a string big enough to leave the small cases behind. Every string probe in the suite until now used a literal a human types in the middle of the range, so a helper that divides by a length of zero, or reads one byte past the end, answered every question it was asked.

It found two, both on LLVM and both from this week's header migration:

one case that computes no length, and so the one the mechanical conversion missed. str_len of it read whatever preceded the allocation.

An interior pointer has no header of its own. This is the same bug the Wasm backend had in v0.1.262 — invisible on LLVM until today, because there was no header there to read wrongly.

The second one is the interesting one: the same defect existed in two backends, written years apart, and the gate that found the first could not see the second until the representation changed underneath it.


v0.1.265 — 2026-08-16

_What fail hands back on Wasm is an empty str, so the code it cannot unwind past survives to reach the try_or._

Wasm has no unwinding here: fail sets a flag and returns, and the callers check the flag on the way out. The value it returned was 0 — which is not an address. A consumer sitting between the fail and the try_or that treated it as a str read the length header at -4 and trapped, so try_or (fn () -> str_len (fail "b")) (-1) died with no output where the interpreter, C and LLVM all answered -1. Which of the two happened depended on what the consumer did with the value, which is the worst property a failure can have.

The sentinel is an interned empty str now. An empty str is a valid answer to every string operation, so the code between the fail and the catch survives, and the try_or default comes back on all four backends.

This does not make fail unwind, and the parity case still pins the difference that remains: statements after a fail in the same body still run, so a buffer that three pushes wrote to reads "onetwothree" there and "onetwo" everywhere else. That is the open half of the question, and it is where the pin now points.


v0.1.264 — 2026-08-16

_The LLVM backend's str carries its length, and the last pin comes off._

Its str was a bare pointer: str_len called strlen, == called strcmp, and literals were plain [N x i8] globals. So on that backend a zero byte was not a byte — str_len (chr 0) answered 0 against 1 everywhere else, and chr 0 == "" was true. The open question about it named the consumers: percent-decoding a URL and decoding Shift_JIS both produce arbitrary bytes, and LLVM alone gave a different answer for them.

It now lays a str out the way the other three do — [i64 len][bytes][NUL], value at byte0. Literals are { i64, [N x i8] } constants and the value is a constant getelementptr into the second field; every runtime helper that builds a string allocates through __lang_str_alloc; the ones that only learn their length at the end (replace, trim, unescape) call __lang_str_finish to write it. Strings that arrive from libc — asprintf for show, snprintf for floats — are copied into a header by __lang_str_of_cstr at the boundary. str_len, ==, str_compare and print all read the header, so a NUL is a byte on all four backends.

The trailing NUL stays. Everything else in this runtime still hands pointers to libc, and keeping the terminator is what let the migration be incremental rather than a rewrite.

Two things repeated from the Wasm slice a day earlier, which is worth writing down: the allocator had to be emitted unconditionally (it lived with the concat helper, so a program that only showed a value referred to a function that was not there), and every internal message global that gets concatenated — the fail prefix, the int_of_str message, the show constants — needed a header of its own.

nul_in_str has no pinned divergence left.


v0.1.263 — 2026-08-16

_The self-hosted Wasm backend carries the length header too, so the host can stop guessing._

The previous slice found that print on Wasm wrote up to the first NUL and could not do otherwise: the JS host runs modules from both compilers, and while the OCaml backend lays a str out as [i32 len][bytes][NUL], the self-hosted one still emitted the pre-header representation — a bare NUL-terminated buffer — while stamping the same ABI number as the compiler that carries a header. A number that says "you may read the length at ptr-4" is worth nothing if half the modules do not have one.

So the self-hosted backend lays strings out the same way now: literals carry a four-byte header in the data section, $__lang_strlen reads it instead of scanning, and every helper that builds a string — concat, substring, repeat, char_at, chr, strbuf_to_str, unescape, show_int/bool/str — allocates through one place that writes it. The digit buffer show_int fills right-to-left is copied into a str that has room for a header in front, the same answer the OCaml backend reached.

With both compilers agreeing, the host reads by length, and print on Wasm writes the whole value: the nul_in_str parity case needed a Wasm pin for exactly one slice. LLVM keeps its pin — its str is a bare pointer with no header anywhere.

Two things fell out of doing it. $__lang_str_alloc had to be emitted unconditionally rather than with the length helper: a module that showed a bool without measuring a string referred to a function it did not define. And the emitted module grew 17 bytes, which the size guard on the self-hosted codegen reports — four bytes per literal is the cost of a length that does not have to be searched for.


v0.1.262 — 2026-08-15

_An interior pointer has no header, and str_trim asked one for its length._

Chasing the Wasm half of the previous slice found something smaller and real. str_trim skips leading whitespace by walking a pointer INTO the string, and then called $__lang_strlen on that pointer to find out how much was left. The length of a Mere str lives in a header immediately before byte0 — so for an interior pointer that read takes the string's own bytes as a length. It is bounded by a NUL scan on the other backends, which is why only Wasm blew up on it, and why str_len (str_trim s) was the shape that showed it rather than print (str_trim s).

It takes the original length once now, uses it to bound the skip, and subtracts what the skip consumed. A NUL inside the value stays a byte through str_trim, on all four backends.

The Wasm host still writes up to the first NUL, and the reason turned out to be sharper than "something overstates its length": the self-hosted Wasm backend emits the pre-header string representation — its $__lang_strlen scans for a NUL and its strings carry no header — while stamping the same ABI number as the compiler that does. The host is shared between both kinds of module, so it cannot trust the header until the self-hosted backend carries one, or stops claiming the ABI that says it does. That is written down where the parity case pins it.


v0.1.261 — 2026-08-15

_print writes the string's length, and the two backends that cannot are pinned._

str became byte-safe in the v0.1.129 arc: the length lives in a header rather than in a terminator, and str_len (chr 0 ++ "X") has answered 2 ever since. print did not — it went out through puts, which stops at the first NUL — so the same value measured 2 and printed one character. Two answers about one value is a bug, not a choice, which is what the open question about this asked to settle.

The C backend writes by length now (print, print_err, print_no_nl), and matches the interpreter byte for byte.

The other two are pinned rather than fixed, each for its own reason, in a new parity case that prints NUL-carrying strings as well as measuring them:

str_len (chr 0) is 0 there against 1 everywhere else. The NUL is not truncated on output; it was never in the value.

finds up to the first NUL. Making the host trust the header instead surfaced a second thing: one str on the self-hosted compiler's path arrives with a header that overstates its content by half a megabyte of zeros. Until that is understood the host keeps scanning, and the pinned case is where it will be noticed.

Pinning is the point. Dropping U+0000 from the case — which is what happened the first time this came up — would have made the file agree by not asking.


v0.1.260 — 2026-08-15

_Exponent notation, because the ends of the double range could not be written down._

1.7976931348623157e308 lexed as the float 1.7976931348623157 applied to a variable named e308. A probe that needed the largest finite double, the smallest normal and the smallest subnormal had to build all three out of powers of two — scaling by halves until the value stopped changing — rather than write them.

Now a literal with an exponent is a float, with or without a decimal point: 1e3, 2.0e-3, 4E+5. A digit has to follow the e (after an optional sign), so 1.5 e is still a float applied to a variable called e, which is the only thing this could have taken away.

The formatter had to change with it. string_of_float keeps 12 significant digits, which was enough for every literal that could be written before and is not enough now: formatting 1.7976931348623157e308 would have written a different number back. It emits the shortest form that reads back as the same double.

And then the notation paid for itself immediately. A new parity case walks the float values four backends had never been asked about — the ends of the range, the subnormals below the smallest normal, both zeros, NaN — none of which could be reached before, because every float literal in the suite was one a human types. It found a real divergence on the first run: `nan < 0.0` was true on the interpreter and false on all three compiled backends. The interpreter routed every ordered comparison through OCaml's total compare, which sorts NaN below everything; the operator is IEEE, where every ordered comparison with NaN is false. Sorting still uses the total order — that part was deliberate — but < on two floats is now the float comparison, and the four backends agree.


v0.1.257 — 2026-08-14

_The tokenizer switches its own state after a start tag, which is a shortcut with a stated boundary rather than a guess._

t used to tokenize as a start tag, a bare t, and an end tag, because the content of a title is RCDATA and the standard has the tree builder decide that. A tokenizer handed a whole document cannot ask.

For HTML the decision is a pure function of the tag name, so the tokenizer makes it: title and textarea go to RCDATA, style and its relatives to RAWTEXT, script to script data, plaintext to PLAINTEXT. The exception is foreign content</code> inside SVG is an ordinary element — and until this backend knows about foreign content the two answers are the same. That is why this is a shortcut and not a mistake: the boundary is known and written down where it is taken.</p> <p>The conformance suite still passes 1,900 of 1,900, and the browser dogfood's tree construction went from 83 of 189 to 88.</p> <hr> <h2 id="v0-1-256-2026-08-14">v0.1.256 — 2026-08-14</h2> <p>_All 228 labels the Encoding Standard defines, generated, replacing a hand-written list of the four this directory can decode._</p> <p>The previous slice added <code>utf-16</code> to that list. This is the same gap at its full size: a page declaring <code>iso-8859-2</code> read as a page declaring <strong>nothing at all</strong>, because the list only had labels for encodings there is a decoder for.</p> <p><strong>Those are two different questions.</strong> <code>label_of</code> answers what a label names; whether this directory can decode the result is what <code>decode</code> answers, and it already answers <code>None</code>. Conflating them made the absence of a decoder look like the absence of a declaration, which the HTML standard treats differently — it is the difference between "use the default" and "use the encoding the page named".</p> <p>The table is generated from <code>encodings.json</code> and carried the way the Unicode tables and the HTML named references are (Q-028): fixed-width records in one sorted string, binary-searched by index arithmetic. 228 labels for 40 encodings, 8,208 characters.</p> <p>The comment above the old list had already written down what would happen — "it cannot discover a label we forgot to list, and that gap is real and is recorded rather than implied". It took a program that asks about labels rather than about decoding to walk into it.</p> <hr> <h2 id="v0-1-255-2026-08-14">v0.1.255 — 2026-08-14</h2> <p>_A label the table forgot, found by the program that asks about labels._</p> <p><code>label_of</code> had no <code>utf-16</code>, <code>utf-16le</code> or <code>utf-16be</code>. The comment above that table already said what would happen — "it cannot discover a label we forgot to list, and that gap is real and is recorded rather than implied" — and this is the gap, found by the browser dogfood asking a page what encoding it claims to be in.</p> <p>Listing them is right even though nothing here decodes UTF-16: <code>label_of</code> answers "what does this label name", which is a different question from "can this directory decode it", and <code>decode</code> already returns <code>None</code> for a name it does not implement. Leaving them out made <code><meta charset=utf-16></code> look like a page with <strong>no declaration at all</strong> — and the HTML standard answers that differently from a page declaring UTF-16, which cannot be true (the declaration is written in ASCII) and reads as UTF-8.</p> <hr> <h2 id="v0-1-254-2026-08-14">v0.1.254 — 2026-08-14</h2> <p>_1,900 of 1,900. The vendored tokenizer suite passes entirely, with no exemptions._</p> <p>Three fixes, and two of them were the same mistake in different clothes.</p> <p><strong>Form feed was not whitespace.</strong> <code>_is_space</code> compared against a <code>\012</code> escape the lexer did not read as U+000C — so the literal was four ordinary characters and form feed silently stopped being a space character everywhere the tokenizer looked for one. It is <code>chr 12</code> now: whether the language spells it <code>\f</code>, <code>\014</code> or <code>\x0c</code> is a question this file does not need an opinion about, and <strong>getting it wrong is silent</strong>. Twelve cases.</p> <p><strong>The bogus-doctype state was inventing quirks mode.</strong> It hardcoded the force-quirks flag rather than carrying the one the token already had, so <code><!DOCTYPE a PUBLIC''''</code> followed by junk — a complete, correct doctype with a parse error after it — was reported as a quirks-mode document. Reaching a recovery state does not by itself mean the thing recovered from was fatal. <strong>Sixty-two cases</strong>, and the same shape as the fix two slices ago: the recovery path was discarding what it had rather than keeping it.</p> <p><strong>And a NUL starting an attribute name</strong> went through unreplaced, because that one path appended the character without the substitution every other one had. Four cases.</p> <hr> <h2 id="v0-1-253-2026-08-14">v0.1.253 — 2026-08-14</h2> <p>_Character references, named and numeric: 1,807 of 1,900 becomes 1,822 — and the last exemption bucket comes out of the harness._</p> <p><strong>The 2,231 named references are one string of fixed-width records</strong>, generated from the standard's own <code>entities.json</code>, sorted, and binary-searched by index arithmetic. That is the shape Q-028 settled for the Unicode tables, applied again: 44 characters per record — the name space-padded to 32, then two code points as six hex digits each. Space pads rather than NUL because a Mere <code>str</code> cannot carry a NUL through the compiled backends, and because space sorts below every character a name uses, so the padded order is the plain order and a short name needs no special case. 98,164 characters, and it compiles and runs on the C backend as well as the interpreter.</p> <p><strong>Matching is longest-first</strong>, because <code>∉</code> is one reference and not <code>¬</code> followed by <code>in;</code> — a search that stopped at the first match would be a different tokenizer. Inside an attribute value a match that did not end in <code>;</code> is left alone when the next character is <code>=</code> or alphanumeric, because <code>?a¬=b</code> is a query string far more often than it is a negation sign.</p> <p><strong>And the harness lost its last exemption.</strong> It had a bucket that excused cases needing character references from the failure count, back when there were none. It came out the moment they were implemented: a bucket that exists because a feature is missing hides real failures as soon as the feature arrives. Every one of the 1,900 cases is now a pass or a failure, and the 78 remaining are named one by one.</p> <hr> <h2 id="v0-1-252-2026-08-14">v0.1.252 — 2026-08-14</h2> <p>_The tokenizer's other starting states: 1,598 of 1,704 becomes 1,807 of 1,900._</p> <p><strong>`Html.tokenize_in` takes the state to start in and the tag that opened the element.</strong> An element whose content model is text — <code>title</code>, <code>textarea</code>, <code>style</code>, <code>script</code> — puts the tokenizer in RCDATA, RAWTEXT or script data, and <strong>only the tag that opened it can end it</strong>. That is why the starting state is a parameter and not a guess: the tree builder is what knows which element it is inside, and a tokenizer that guessed would be wrong exactly where <code></</code> appears inside a script.</p> <p>The three text-like states share one shape and one implementation: character data until <code></</code> plus the opening tag's name plus a space, a slash or <code>></code>. PLAINTEXT is the degenerate case that never ends.</p> <p><strong>And the case count went up, which is the point.</strong> A case listed under several initial states is several cases — the suite writes it once and means it for each. Running only the first reported a number smaller than what was being checked, so the harness expands them: 1,704 entries are 1,900 cases.</p> <hr> <h2 id="v0-1-251-2026-08-14">v0.1.251 — 2026-08-14</h2> <p>_Three more of the tokenizer's rules: 1,505 of 1,704 becomes 1,598. Sixty-two of those came from one line._</p> <p><strong>Newline preprocessing</strong> — CRLF and a lone CR both become LF before the machine sees them. The standard puts this in a preprocessing step rather than in the states for the reason it shows here: otherwise every state has to say it.</p> <p><strong>U+0000 becomes U+FFFD inside markup</strong> — names, attribute values, comments, doctypes. It is a parse error and the character is replaced rather than dropped, because dropping it changes how many characters a later consumer counts.</p> <p><strong>And the one that was worth sixty-two cases: the bogus-doctype state was throwing away what had already been parsed.</strong> <code><!DOCTYPE a PUBLIC""</code> followed by junk has a public identifier that is <em>present and empty</em>; discarding the state on the way into the recovery path reported it as <em>missing</em>, which is a different token. Recovery paths keep what they have — that is what makes them recovery rather than restart.</p> <hr> <h2 id="v0-1-250-2026-08-14">v0.1.250 — 2026-08-14</h2> <p>_An HTML tokenizer, measured against the standard's own suite from the first run: 1,505 of 1,704._</p> <pre><code> contrib/html/tokenizer.mere the state machine, with the standard's state names test/data/html5lib/*.test vendored html5lib-tests (tokenizer) scripts/gen_html5lib_testdata.sh how they got here (maintenance, needs network) scripts/html_tokenizer_conformance.sh the gate </code></pre> <p><strong>Written as the standard writes it</strong>: one function per named state, so a line can be found in the specification by searching for its state. Not a regular expression or a lookahead scanner, because the recovery rules are what make HTML parseable at all and they are stated per state — <code><</code>, <code></</code>, <code><!</code> and <code><?</code> all have defined behaviour when what follows them is not what it looked like, and that is where the bugs are. Two of the first three the suite found were exactly that shape: a repeated attribute keeping the last instead of the first, and a comment ending in a lone dash keeping it instead of dropping it.</p> <p><strong>The pass count is pinned exactly, not as a floor.</strong> A floor lets a regression hide behind a new pass. The harness prints the first ten failures in full, so what is missing is in the output rather than only in a document — and what is not covered yet is counted by category rather than skipped silently: character references (53), non-Data initial states (111), U+0000 replacement (57).</p> <p>The suite is vendored rather than fetched by the gate, the same decision the UCD conformance files got: a gate that needs the network fails for reasons that have nothing to do with the code.</p> <p><strong>Placement</strong>: a conformant tokenizer is a library — the same shape as <code>contrib/url</code> and <code>contrib/encoding</code> — so it lives here. Tree construction is where a browser starts and is not.</p> <hr> <h2 id="v0-1-249-2026-08-14">v0.1.249 — 2026-08-14</h2> <p>_A window, its pixels, and its input — promoted from a probe to a capability. The interesting part is that it can be checked without anybody looking at a screen._</p> <pre><code> lib/codegen_c.ml the SDL2 runtime, emitted when a win_* extern is declared contrib/window/window.mere the typed side: window, event, show, capture, poll test/window/window_check.mere draw, show, read back, compare scripts/window_check.sh new gate: SDL's dummy driver, no display needed </code></pre> <p><strong>Six externs and no language feature.</strong> <code>win_open</code> / <code>win_size</code> / <code>win_blit</code> / <code>win_readback</code> / <code>win_poll</code> / <code>win_close</code>. Pixels cross the boundary as a flat arena offset and everything else is an int — the contract the socket family established — so the runtime is conditional C the way PortMidi's is, emitted only when a program declares one of these. Build with <code>sdl2-config --cflags --libs</code>.</p> <p><strong>`size` asks the renderer, not the window.</strong> They are different numbers on a HiDPI display: a 640×480 window has a 1280×960 renderer, and the pixels are in the second one. This was recorded as friction 3 when the capability was a probe — a readback comparison against the window's size compares against a number the pixels are not in — so the capability returns <code>SDL_GetRendererOutputSize</code> and nothing else.</p> <p><strong>`Window.show` composites a `canvas` with the same arithmetic as `Canvas.to_ppm`</strong>, so what a program puts on the screen and what it writes to a file are the same image by construction rather than by two similar loops.</p> <p><strong>The gate is a readback, and it needed one more thing to be evidence.</strong> Draw a known pattern with <code>contrib/raster</code>, show it, read the window's pixels back, compare — 3072 pixels, 0 mismatches, under SDL's <code>dummy</code> video driver, which has a software renderer and a real event queue but no display. That runs in CI and does not open a window on your desktop.</p> <p><strong>But `show` writes the image into the same arena block `capture` reads back into</strong>, so a readback that did nothing at all would hand back exactly what was written and every pixel would match — a gate that passes while testing nothing. <code>capture</code> poisons the block first. Checked by making the runtime's readback a no-op: 3072 of 3072 pixels then differ.</p> <p><strong>Not gated: that an event ever arrives.</strong> <code>poll</code> is checked only for answering <code>Nothing</code> on an empty queue. Delivering a real key or click needs either a display or a way to inject one, and a test-only extern that pushes events would be checking the scaffolding rather than the capability.</p> <hr> <h2 id="v0-1-248-2026-08-14">v0.1.248 — 2026-08-14</h2> <p>_Adding the last two math builtins turned up a silent wrong answer in every LLVM program that used floats: <code>f_pow 3.0 2.0</code> was <code>3.0</code>, because the prelude's integer <code>pow</code> had taken libm's symbol._</p> <pre><code> lib/codegen_llvm.ml Mere top-level names are prefixed `mu_`, as C's always were lib/codegen_c.ml exp / log beside sqrt lib/codegen_wasm.ml the same, as host imports scripts/run_wasm.js + 5 __lang_exp / __lang_log; and str_of_float follows C's %g test/parity/exp_log.mere new: identities within a tolerance, not digits </code></pre> <p><strong>`exp` and `log`</strong> were the last of the family that began with <code>floor</code> / <code>ceil</code> / <code>round</code> in v0.1.243: names in the typer's environment with a type, so a program using them type-checked everywhere and then <code>mere -c</code> emitted a call to a symbol the C compiler had never heard of, while LLVM and Wasm said "unbound variable". They are three lines on each backend. MISSING 8 → 6, nocompile 10 → 8.</p> <p><strong>Then the new test failed on LLVM, for a reason that had nothing to do with it: `f_pow 3.0 2.0` came back `3.0`.</strong> This backend emitted Mere top-level names into the IR's global namespace <strong>unprefixed</strong>, so when the prelude grew an integer <code>pow</code> in v0.1.245 that became <code>define @pow</code> — which is libm's symbol, and <code>@llvm.pow.f64</code> lowers to a call to <code>pow</code>. Every <code>f_pow</code> on this backend has been calling the integer power since. The C backend has prefixed with <code>mu_</code> since it was written; this one now does too.</p> <p>Neither <code>internal</code> linkage nor renaming the intrinsic helps — both were tried and measured. The collision is the name, and the name was in a namespace shared with the C library: <code>write</code>, <code>exit</code>, <code>time</code>, <code>free</code> and every other libc symbol were the same accident waiting for a program to name a function after one.</p> <p>The rename is the loud kind of change: a site missed by the prefix fails at link time with an undefined symbol rather than computing something else. Four such sites turned up and all four were tables <strong>keyed by the source name</strong> — the free-variable analysis, the lifting pass's host, the shadowing guard's position lookup, and the debug info's <code>DISubprogram(name:)</code>, which shows in a debugger and must stay what the program calls it. Emitted names are for the IR; source names are for everything that reasons about the program.</p> <p><strong>And the Wasm host printed floats by a different rule.</strong> <code>str_of_float</code> there emulated C's <code>%g</code> with JavaScript's <code>toPrecision</code>, which is not that rule: <code>%g</code> goes exponential when the decimal exponent is below -4 and <code>toPrecision</code> stays decimal down to 1e-7, so <code>exp -10</code> printed as <code>0.00004539992976248485</code> on Wasm and <code>4.5399929762484854e-05</code> everywhere else. The host implements the actual rule now, exponent padded to two digits as C does.</p> <p><strong>What the test asserts, and why it is not digits.</strong> A transcendental function is not required to be correctly rounded by anybody: <code>exp -10</code> differs in the last bit between libm and JavaScript, and a gate comparing the digits would be reporting the C library's build options. So <code>exp_log.mere</code> prints exact values only where the answer is exact in binary floating point and asserts everything else as an identity within a tolerance — <code>log (exp x) = x</code>, <code>exp (2 log 3) = f_pow 3 2</code>, <code>exp (0.5 log 2) = sqrt 2</code>. That still fails an <code>exp</code> that returns its argument or a <code>log</code> wired to log10, both checked by reverting the fix and watching the gate go red.</p> <hr> <h2 id="v0-1-247-2026-08-14">v0.1.247 — 2026-08-14</h2> <p>_<code>x / 0</code> was four different things, and three of them were not failures. The gate built last slice is what made fixing it a two-line test._</p> <pre><code> lib/codegen_c.ml __lang_idiv / __lang_imod: a checked divisor lib/codegen_llvm.ml the same, as IR functions lib/codegen_wasm.ml the same, with the message interned per program test/parity/fail/uncaught_div_zero new test/parity/fail/uncaught_mod_zero new scripts/parity.sh the message is the first line of stderr, not the last </code></pre> <p><strong>What it did before.</strong> The interpreter raised <code>division by zero</code>. The C backend emitted a bare <code>a / b</code>, which is <strong>undefined behaviour in C</strong>: this machine's arm64 quietly answered 0 and the program carried on printing, while an x86-64 build of the same source raises SIGFPE. LLVM emitted <code>sdiv</code>, undefined in IR and therefore something the optimizer may assume never happens. Wasm trapped — a defined failure, but a silent one, with no message at all. <strong>A wrong answer, a crash, or a silent death, depending on the backend and the CPU.</strong></p> <p>All four raise now, with the interpreter's messages (<code>division by zero</code> and <code>modulo by zero</code> — it distinguishes them, so the others do too), catchable with <code>try_or</code>. It costs a branch per division. <code>INT_MIN / -1</code> is the other undefined case in C and IR and wraps now, matching what the interpreter already produced.</p> <p><strong>The `-rv` backend is the exception, and it was measured rather than assumed</strong>: built for QEMU's <code>virt</code> board and run there, <code>17 / 0</code> is <code>-1</code> and <code>17 % 0</code> is <code>17</code>, the RISC-V specification's non-trapping answer. That backend targets bare metal, where there is no stream to write a diagnostic to and no process to exit, so the platform's answer is the answer. Making it raise would mean deciding what a machine-mode trap means for the kernel that runs on it, which is its own piece of work.</p> <p><strong>And the harness needed one more fix to see any of it.</strong> Its notion of "the message" was the <em>last</em> line of stderr, which works for a <code>fail</code> call — that carries no location and renders on one line — and not for a failure the interpreter can point at, which renders with <code>--> file:line</code> and the source under it. The message is the first line. A single-sink backend is the mirror image: there the diagnostic is the <em>last</em> line of the program's own output. Two ends, because the two files are different files.</p> <hr> <h2 id="v0-1-246-2026-08-14">v0.1.246 — 2026-08-14</h2> <p>_The parity harness compared stdout, so the failure surface of the language was the one part of it four independent implementations were never held to. No parity test used <code>fail</code> — and that was not an oversight: none could have passed._</p> <pre><code> scripts/parity.sh failing programs compared on exit + stdout + message; DIVERGE test/parity/fail/*.mere new: five uncaught failures, four backends test/parity/failure_caught.mere new: try_or over every failure kind lib/codegen_c.ml exit 1, not abort; the `fail: ` tag moves to the builtin lib/codegen_llvm.ml a stderr at last: diagnostics, print_err, print_no_nl lib/codegen_wasm.ml the tag; int_of_str names its input; print_err refuses </code></pre> <p><strong>What an uncaught failure did, before this slice.</strong> The same program exited <strong>1</strong> on two backends and <strong>134</strong> (SIGABRT) on two others. It wrote its diagnostic to <strong>stderr</strong> on two and <strong>stdout</strong> on two. It tagged the message <code>fail: </code> on three and not on the fourth. And <code>int_of_str "abc"</code> named the offending input on two backends and said only <code>int_of_str: not a valid int</code> on the other two — the same failing program telling you two different things, and the version that omits the input is the one you cannot debug from. Five differences, none of which any test could see.</p> <p>All four now write one line to <strong>stderr</strong> and exit <strong>1</strong>, with the message the program raised. LLVM's panic path had been using <code>puts</code> — a backend that <em>refused</em> <code>print_err</code> for having no stderr lowering was writing its own diagnostic to stdout. <code>write(2, ...)</code> was already declared for <code>print_bytes</code>; declaring it unconditionally made the panic path correct and made <code>print_err</code> and <code>print_no_nl</code> three lines each, so both stopped being refusals on that backend.</p> <p><strong>The `fail: ` tag belongs to the `fail` builtin, not to the printer.</strong> Tagging where the diagnostic is written tagged the backend's <em>own</em> failures too, which the interpreter does not: hence <code>fail: int_of_str: ...</code> against <code>int_of_str: ...</code>. Moving it to the builtin makes the message comparable <strong>verbatim</strong>, which matters more than it sounds — see below.</p> <p><strong>On Wasm, `print_err` now refuses.</strong> It wrote to the same host sink as <code>print</code>, so a diagnostic landed in the program's own output and nothing said so. The JS host ABI has one sink (<code>env.puts</code>); giving it a second one is a change to every host that instantiates a module, which is a deliberate change and not a side effect of a panic message. Nothing in the repo used <code>print_err</code>, so there was nothing to break — and a refusal names the missing thing where a silent stdout write named nothing.</p> <p><strong>`test/parity/fail/*.mere`</strong> is the gate: programs that are supposed to fail, compared on exit status, the stdout written <em>before</em> the failure, and the message. Five of them. The harness takes only the interpreter's envelope off the message (it names the source file it is running; a compiled binary has none) and compares the rest byte for byte.</p> <p><strong>The first version of that comparison stripped the `fail: ` tag as well</strong>, and it made the harness unable to see the difference this slice had just fixed: <code>boom</code> and <code>fail: boom</code> both normalized to <code>boom</code>, so removing the fix still passed. The control experiment caught it — revert a fix, confirm the gate goes red. <strong>A normalization is a place a gate stops looking</strong>, and the fix was to make the thing consistent by construction instead of normalizing it away.</p> <p><strong>And a DIVERGE state, because the caught-failure test found a live one.</strong> <code>fail</code> on Wasm sets a flag that callers check on the way out; it does not unwind, so statements after it in the same body still run. Inside a <code>try_or</code> thunk that is an observably different <em>result</em>, not just extra output — a buffer that reads <code>onetwo</code> on three backends and <code>onetwothree</code> on the fourth. A gate with no place to say "these two legitimately differ here" loses the first real divergence it finds, along with everything else that test was checking. So a divergence is declared by a file next to the case (<code>failure_caught.wasm.expected</code>) holding that backend's output <strong>exactly</strong>: the known difference is pinned rather than tolerated, any other change is still a failure, and the day that backend learns to unwind, the declaration breaks and says so.</p> <p><strong>Two smaller things found on the way.</strong> Passing a <code>test/parity/fail/</code> file as an explicit argument ran it as an ordinary test and reported it as one the interpreter could not run — arguments are partitioned by path now, the same way the defaults are. And <code>__lang_str_concat("fail: ", msg)</code> in the C backend hung the program instead of printing: this backend's strings carry a length header before byte 0, a raw C literal has none, so the concat read whatever preceded the constant as its length. Same trap that made <code>str_replace</code> return <code>""</code> in v0.1.233.</p> <hr> <h2 id="v0-1-245-2026-08-14">v0.1.245 — 2026-08-14</h2> <p>_Ten builtins that existed only on the interpreter became ten definitions in the language. Moving them exposed three bugs that had nothing to do with them, two of which were wrong answers from builtins every test called correct._</p> <pre><code> lib/prelude_stdlib.ml sign incr decr square cube sum_range pow lcm divmod assert lib/eval.ml -105 lines: the ten builtins those replace lib/codegen_llvm.ml string globals minted once; int_of_str returns i64 lib/codegen_c.ml gcd, random_int, file_size widened to long long test/parity/int_width.mere new: every int builtin, with arguments above 2^31 test/parity/show_json_same_program new: show and to_json in one program </code></pre> <p><strong>The ten.</strong> <code>sign</code> / <code>incr</code> / <code>decr</code> / <code>square</code> / <code>cube</code> / <code>sum_range</code> / <code>pow</code> / <code>lcm</code> / <code>divmod</code> / <code>assert</code> were in the typer's environment and in <code>eval.ml</code> and nowhere else: they type-checked on every backend and <code>mere -c</code> then emitted a reference to a name the C compiler had never heard of. Defining them as Mere source in the prelude gives all five backends the same one from one place, which is cheaper than five codegen cases and cannot drift between them. <code>int_max</code> / <code>int_min</code> are deliberately not among them — they cannot be a portable literal while int is 63-bit on interp and 64-bit elsewhere.</p> <p><strong>"The same as the builtin" was wrong four times out of ten,</strong> and only outside the range a small test looks at. <code>sum_range</code> and <code>pow</code> recursed where the builtin was closed-form and square-and-multiply, which is a stack overflow rather than a slow answer at a million terms. <code>lcm</code> multiplied before dividing, which overflows for operands whose lcm fits but whose product does not — on interp that made <code>lcm 3037000493 3037000493</code> answer <code>13</code>. And <code>divmod</code> left its documented zero failure to <code>/</code>, which is not one thing across the backends: <strong>bare `x / 0` raises on the interpreter and returns 0 on C and LLVM</strong>, so the failure would have depended on which backend you built with. It has its own check now; the divergence in <code>/</code> itself is still there and is not yet under any gate, because the parity harness compares stdout and does not compare failures at all.</p> <p><strong>The prelude definition shadows the builtin — measured, not assumed.</strong> Making <code>eval.ml</code>'s <code>sign</code> return 999 changed no answer, so the ten builtins were unreachable rather than merely redundant, and 105 lines came out. The typer declarations stay: that is the set <code>host_matrix.sh</code> generates its questions from, so deleting a declaration would have removed the question rather than answered it.</p> <p><strong>`@.s_true` was defined twice.</strong> The first symptom of any of this was <code>to_json_composite</code> failing to compile on LLVM with <code>redefinition of global '@.s_true'</code>. The show emitter and the to_json emitter register their string constants in separate blocks, and they want some of the same names — <code>s_true</code>, <code>s_false</code>, <code>s_lbracket</code>, <code>s_rbracket</code>. <strong>Any program using both `show` and `to_json` hit it</strong>, which no test did until a prelude helper's error path called <code>show</code> and gave every program a show emitter. Minting is idempotent by name now, and a second mint with different content fails the compile instead of resolving last-wins.</p> <p><strong>Then the value that found `lcm` found a real one: `gcd 3037000493 3037000493` was `1257966803` on the C backend.</strong> The generated runtime declared <code>static int __lang_gcd(int, int)</code> while int is 64-bit there, so both arguments were truncated to their low 32 bits. Nothing was missing and nothing failed to compile — the builtin was recorded as present and correct on all four backends, because every probe of it had used a one-digit literal. <strong>The gap was in the values, not in the list of names.</strong></p> <p><code>test/parity/int_width.mere</code> is that gate, and it found the second one while being written for the first: LLVM's <code>int_of_str</code> parsed with <code>strtoll</code> and truncated to <code>i32</code>, so <code>int_of_str "3037000493"</code> was <code>-1257966803</code> and the largest int was <code>-1</code> — the return width left behind when int widened to i64. <code>random_int</code> and <code>file_size</code> on C are the same shape and are widened too, though neither is observable from a parity test (one is random, the other needs a 2GB file). Sweeping both backends for the rest: every remaining narrow helper returns a status code or a count bounded by a string's length.</p> <p><strong>An intermediate has a width too.</strong> With those fixed, one line still diverged: <code>sum_range (0 - 3037000493) 0</code> has an answer both int widths hold and a naive Gauss intermediate only the 64-bit one does. Halving inside the product — exactly one of the two factors is even, since an odd count means the endpoints share parity — makes the function portable over the whole range its result can hold.</p> <hr> <h2 id="v0-1-244-2026-08-13">v0.1.244 — 2026-08-13</h2> <p>_The harness whose job is to ask which backend has which builtin was asking about a set somebody remembered. Now it asks for the set too — and the answer is 144 builtins, not 50._</p> <pre><code> bin/mere.ml --dump-builtins: every name in the typer's environment, with its type scripts/host_matrix.sh probes generated from those types; a new `nocompile` state docs/host-matrix.md 50 rows -> 144 </code></pre> <p><strong>`mere --dump-builtins`.</strong> One line per name in <code>Typer.initial_env</code>, <code>name<TAB>type</code>. The compiler is the authority on its own environment, which is the same argument <code>host_matrix.sh</code> already made for the <em>answers</em> — it just had not been applied to the <em>questions</em>.</p> <p><strong>Probes are synthesized from the type</strong>: one literal per argument for <code>int</code>, <code>float</code>, <code>str</code>, <code>bool</code> and <code>unit</code>, and a bare mention for a non-arrow. Anything needing a value a literal cannot make — a <code>File</code>, a <code>Vec</code>, a <code>Channel</code> — falls back to a hand-written override, of which there are 28. <strong>70 of the 214 names are not synthesizable and are counted and named</strong>, so the part of the environment this harness cannot see is a number rather than a silence. A synthesized probe that does not type-check is dropped and counted too.</p> <p><strong>And a new state, `nocompile`.</strong> <code>yes</code> used to mean "the backend emitted code", which is not the same as working: <code>floor</code>, <code>ceil</code> and <code>round</code> emitted fine and the C compiler then failed on an undeclared identifier. So for C the emitted source is now handed to a compiler, and the row says <code>nocompile</code> when that rejects it. That state is exactly the blind spot those three sat in for as long as they had been in the environment.</p> <p>The matrix went from <strong>50 builtins, 0 MISSING</strong> to <strong>144 builtins, 18 MISSING, 20 nocompile</strong>. The old number was not wrong — it was the answer to "is there a hole among the 50 somebody listed", which reads like the answer to a different question.</p> <p>Of the 20 <code>nocompile</code> rows, twelve are pure integer functions — <code>cube decr divmod incr int_max int_min lcm pow sign square sum_range assert</code> — which want defining in the prelude as Mere source, where all five backends get them at once. <code>exp</code> and <code>log</code> want libm cases beside <code>sqrt</code>. Neither is done here: this change is about being able to see them.</p> <p>Two rows that reported <code>error</code> were probe artifacts rather than defects — <code>map_new ()</code> alone leaves its key and value types unresolved, and codegen correctly refuses. Both now have overrides that pin the types, so the matrix reports 0 error.</p> <hr> <h2 id="v0-1-243-2026-08-13">v0.1.243 — 2026-08-13</h2> <p>_A rasterizer, and the three math builtins it turned out no compiled backend had._</p> <pre><code> contrib/raster/canvas.mere premultiplied pixels, one blend, rects and clips contrib/raster/path.mere antialiased polygon fill, curves, strokes lib/codegen_c.ml floor / ceil / round lib/codegen_llvm.ml floor / ceil / round lib/codegen_wasm.ml the same three, refused loudly </code></pre> <p><strong>`contrib/raster`.</strong> A pixel buffer, source-over compositing, and one antialiased polygon fill that rectangles, glyph outlines, borders and strokes are all expressed in terms of. Nothing here opens a window, and that is the point: turning a document into pixels is checkable by comparing pixels, which needs no display.</p> <p><strong>Premultiplied alpha</strong>, so source-over is <code>src + dst*(255-sa)/255</code> on every channel with no division by the result and no special case for a transparent destination. And <code>a*b/255</code> is exact at both ends — the obvious <code>(t + t/255)/255</code> returns <strong>256</strong> for <code>255*255</code>, which overflows the byte into the next channel of the packed colour. The first smoke test drew an opaque black canvas instead of a white one, which is a good way for that to be found.</p> <p><strong>Coverage is separate from alpha and multiplies it</strong>, and the parity test asserts it: coverage 128 with a solid colour and coverage 255 with a half-alpha colour must land on the same pixel.</p> <p><strong>Coverage, not sampling.</strong> Each pixel row is cut into slices; per slice the edges are intersected, the crossings sorted, and the spans added to a per-pixel accumulator <strong>exactly in x</strong> — an edge at x = 3.25 puts 75% into pixel 3. Only y is quantized. Geometry is float and coverage is integer, both measured rather than assumed: doubles print identically across backends, and an integer accumulator cannot drift.</p> <p><strong>And then: `floor`, `ceil` and `round` did not exist on any compiled backend.</strong> Emission <em>succeeded</em> and the C compiler then failed on an undeclared <code>mu_floor</code>, so nothing short of a program that used them could notice — and nothing did, for as long as they had been in <code>Typer.initial_env</code>. <code>sqrt</code>, <code>sin</code>, <code>cos</code> and <code>tan</code> were all handled; these three were simply never added. Now on C and LLVM, verified identical to the interpreter including the negative half-way cases (<code>round (-2.5)</code> is -3 on all three).</p> <p>The Wasm backend <strong>refuses all three, deliberately.</strong> <code>f64.floor</code> and <code>f64.ceil</code> are instructions and looked like a five-line addition — but putting the names in that backend's eta-expansion list sent it into an infinite expansion, and a bare <code>floor</code> call never finished emitting. <code>round</code> is worse than absent there: <code>f64.nearest</code> rounds half to even where C and the interpreter round half away from zero, and doing it properly needs a scratch <code>f64</code> local, which that backend declares per function. A backend that says "no" is one a caller can work around; one that hangs, or that quietly rounds differently, is not.</p> <p><strong>A survey, since one missing builtin implies others.</strong> Fifteen names in <code>Typer.initial_env</code> emit as undeclared identifiers on the C backend: <code>ceil cube decr exp floor id incr int_max lcm log pow round sign square sum_range</code>. Three are fixed here. The rest are recorded rather than fixed, because <code>scripts/host_matrix.sh</code> — the harness whose entire job is to ask which backend has which builtin — has a <strong>hand-written</strong> case list and covers none of them. Generating that list from the typer's environment is the actual fix and is its own change.</p> <p><code>contrib/raster</code> runs on interp and C. The framebuffer is a <code>ByteBuf</code>, which the LLVM and Wasm backends do not have, so they refuse at emit time and the harness records <code>UNSUP</code> rather than a failure — C is what a native renderer targets and the interpreter is an independent second implementation, so the gate still compares two.</p> <hr> <h2 id="v0-1-242-2026-08-13">v0.1.242 — 2026-08-13</h2> <p>_A bug both hosts had, in a place the gate was only compile-checking. Backend parity could not see it, because being wrong the same way twice looks like agreement._</p> <pre><code> lib/codegen_c.ml mem_get_u32be: long long, unsigned scripts/pg_env.js getUint32, not getInt32 lib/eval.ml the byte arena, so the interpreter can run these programs scripts/ctest.sh compare when both sides can run, compile-check when they cannot </code></pre> <p><strong>`mem_get_u32be` sign-extended.</strong> It returned a C <code>int</code>, which widens into Mere's 64-bit int with the sign — so <code>0xFF008080</code> came back as <code>-16777088</code>. Every opaque pixel has alpha <code>0xFF</code>, so anything touching pixels hit it. It was also undefined behaviour rather than merely wrong: <code>q[0] << 24</code> on an <code>int</code> promoted from <code>unsigned char</code> overflows a signed int once <code>q[0]</code> reaches <code>0x80</code>. Now <code>long long</code>, computed unsigned.</p> <p><strong>The JS host had the identical bug</strong> — <code>getInt32</code> where <code>getUint32</code> was meant. Which is the interesting part: <strong>backend parity could not catch this, because both hosts were wrong the same way.</strong> That is the same shape as an exhaustive test file derived from the rules it tests, and it is worth naming: agreement is only evidence when the things agreeing are independent.</p> <p><strong>So what did find it?</strong> A probe that opened a window, wrote a known pattern, blitted it and read it back — when a pixel it wrote did not compare equal to the pixel it read. And what let it hide was <code>scripts/ctest.sh</code>: a program containing an FFI declaration was compile-checked only, on the grounds that a bare extern has no linkable symbol. True of an arbitrary name, false of the native FFI set — <code>mem_*</code>, <code>tcp_*</code>, <code>str_ptr</code> and the rest get <code>static</code> definitions emitted, so those programs link and run. Their <strong>answers were never compared</strong>.</p> <p>The rule is now: <strong>compare when both sides can run, compile-check when they cannot.</strong> Requiring the interpreter to run it too is what keeps this from firing on a program that would open a socket — an extern the interpreter mocks is an extern somebody thought about.</p> <p><strong>Which needed the interpreter to have the arena at all</strong>, and now it does: the same bump allocator over a fixed buffer, the same capacity, the same first offset, so the two agree on arithmetic as well as on values. That also makes <code>contrib/db/pg.mere</code> runnable on the interpreter, and it is a prerequisite for the raster work: a framebuffer is an arena.</p> <p><code>test/ctests/mem_arena_u32.mere</code> covers the round trip at <code>0x7FFFFFFF</code>, <code>0x80000000</code>, <code>0xFF008080</code>, <code>0xFFFFFFFF</code> and the byte order, on both sides. Reverting the fix turns it red.</p> <hr> <h2 id="v0-1-241-2026-08-13">v0.1.241 — 2026-08-13</h2> <p>_The one algorithm here with both kinds of gate pointed at it — which is what makes the difference between them concrete rather than theoretical._</p> <pre><code> contrib/unicode/normalize.mere NFC and NFD contrib/unicode/nfc_table.mere generated: ccc, decompositions, and the derived inverse scripts/gen_normalize_tables.sh derives all three scripts/normalize_conformance.sh 20,034 UCD cases x 6 assertions scripts/unicode_parity.sh + 8,755 inputs vs node's String.prototype.normalize </code></pre> <p><strong>`Normalize.nfc` and `Normalize.nfd`.</strong> <code>é</code> can be one code point or two, and the two spellings are the same text. Anything that compares text — an origin check, a cache key, a search — has to pick one, and a renderer that draws both spellings differently is drawing the same text two ways.</p> <p>Three things carry the weight and only the first is obvious. <strong>Canonical ordering</strong> sorts each run of combining marks by class, <em>stably</em>, because marks of equal class must keep the order they were typed in. <strong>A decomposition is not automatically a composition</strong> — four kinds of mapping are excluded from the inverse, and the generator applies and <strong>counts</strong> all four rather than assuming them: 1,035 singletons, 4 non-starter decompositions, 81 script-specific exclusions, and 3,833 compatibility mappings that are not canonical at all. 2,081 canonical mappings in, <strong>961 primary composites</strong> out. And <strong>blocking</strong>: a mark reaches the last starter only if nothing between them blocks it, which is why <code>q</code> + dot-below + dot-above composes nothing while <code>d</code> + dot-below + dot-above composes only the first.</p> <p>Hangul is arithmetic rather than table lookup — 11,172 syllables that would otherwise be entries. Canonical mappings are stored <strong>pairwise</strong> and applied recursively, because the longest one in the UCD is two code points and a pre-expanded table would need variable-length values to buy a recursion a few levels deep.</p> <p><strong>Both gates, and the reason neither substitutes for the other.</strong> The UCD's conformance file is exhaustive in ways no independent implementation is sampled for — canonical-order permutations, PRI #29's chained composites, the closure of every composite — but it is derived from the same rules this code reads, so a shared misreading would agree with itself. node's <code>normalize</code> is independent but not exhaustive. So: <strong>20,034 cases × 6 assertions</strong> against the file, and <strong>8,755 inputs against node</strong>, the latter derived from the generated tables so a row nobody thought to test still gets one.</p> <p>Six assertions per conformance line rather than two, because <code>NFC(c1) == c2</code> alone would pass an implementation that is wrong about already-normalized input — which is the common case in real text.</p> <p>Both green on the first run, which for once is worth saying: the previous slice's gate found three defects, and the difference is that this algorithm's hard parts (ordering, blocking) were measured against node before the gate existed rather than reasoned about.</p> <p><strong>Also</strong>: the claim in <code>contrib/unicode/README.md</code> that a layout engine wants East Asian Width "for advance widths" was wrong and is corrected. A renderer with a font takes advances from the font's metrics. EAW is for terminal-style layout and for a fallback when there are no metrics, which moves it down the list rather than up it.</p> <hr> <h2 id="v0-1-240-2026-08-13">v0.1.240 — 2026-08-13</h2> <p>_The first gate here that is not an independent implementation — and it caught three defects a careful reading of the rules had not._</p> <pre><code> contrib/unicode/linebreak.mere UAX #14, forty-four rules in the standard's order contrib/unicode/lb_table.mere generated: 2,175 ranges, four UCD properties scripts/gen_linebreak_table.sh derives the table scripts/gen_linebreak_testdata.sh vendors the conformance suite scripts/linebreak_conformance.sh 19,338 cases, all agreeing </code></pre> <p><strong>`LineBreak.opportunities`</strong> says where a line is <em>allowed</em> to end. Not where it should — that is the layout engine's decision, made with widths — but where the text permits one.</p> <p>Three things make UAX #14 long, and none of them are the rules themselves. <strong>LB9 and LB10 are a preprocessing step</strong>: a combining mark takes the class of the character before it, so the unit the rules see is a base plus its trailing <code>CM</code>/<code>ZWJ</code> run, except after a hard break or a space where LB10 makes the leftover an <code>AL</code>. <strong>"even after spaces" appears in six rules</strong>, each needing the last non-space class as well as the immediately preceding one, all six sitting before the rule that breaks after a space. And <strong>some rules look further than one character either way</strong> — LB25 needs a number state and two of lookahead, five rules need what came before the previous character, LB30a needs a count.</p> <p><strong>Four UCD properties in the table</strong>, because several rules are written in terms of things other than <code>Line_Break</code>: LB15a/15b test <code>General_Category</code> Pi and Pf, LB19a and LB30 test <code>East_Asian_Width</code>, LB30b tests an unassigned <code>Extended_Pictographic</code>. LB1's resolution happens in the generator, which lets the rules read the way the standard writes them.</p> <p><strong>The gate is a different kind, and the difference cuts both ways.</strong> UAX #14 has no oracle in node: <code>Intl.Segmenter</code> has no <code>line</code> granularity and <code>Intl.v8BreakIterator</code> is gone. So this runs the Unicode Consortium's own conformance file instead — <strong>weaker</strong>, because it is derived from the same rules the implementation reads and a shared misreading of the prose would agree with itself; <strong>stronger</strong>, because it is exhaustive over the pair table, every class against every class with and without an intervening combining mark and space, which no hand-written corpus would reach. It is vendored under <code>test/data</code> so it runs offline and cannot drift from the table's version.</p> <p><strong>It earned its keep immediately.</strong> Three defects survived a careful reading of the rules:</p> <p><em> positions were reported </em><em>per unit rather than per code point</em><em>, so every case containing a combining mark was one short — 48% passing, and the pattern was uniform enough to name the cause before reading a second failure; </em> <strong>LB8a was called unreachable in a comment, and is not.</strong> A ZWJ at the start of text has nothing to fold into, so LB10 turns it into an <code>AL</code> — but LB8a comes <em>before</em> LB10, so <code>ZWJ ×</code> still applies. Then the fix needed a second correction: the flag means "this unit's <strong>last</strong> code point is a ZWJ", so folding a combining mark on top of one clears it. The suite distinguishes those two readings in 24 cases; <em> </em><em>LB19a's last line tests the character before the quotation mark</em><em>, not the mark itself. Three cases, all of them CJK text with curly quotes.</em></p> <p>None of those would have been found by a corpus somebody wrote by hand, which is the argument for the exhaustive-but-not-independent gate rather than against it.</p> <hr> <h2 id="v0-1-239-2026-08-13">v0.1.239 — 2026-08-13</h2> <p>_A grapheme cluster is what a reader calls a character, and it is what a renderer has to advance by. Four of UAX #29's rules are not local, and those four are the whole difficulty._</p> <pre><code> contrib/unicode/grapheme.mere UAX #29 extended grapheme clusters contrib/unicode/gcb_table.mere generated: 1,631 ranges, three UCD properties folded into one scripts/gen_unicode_tables.sh derives the table from the UCD scripts/unicode_parity.sh 8,509 inputs against node's Intl.Segmenter </code></pre> <p><strong>`Grapheme.clusters`.</strong> <code>á</code> is one cluster, <code>👩‍👩‍👦</code> is one, <code>🇯🇵</code> is one, <code>\r\n</code> is one. Code points are not the unit and neither are bytes — cursor movement, selection and glyph advance all break visibly when the wrong one is used.</p> <p>Most of UAX #29 is local: read the class of the code points on either side of a position and decide. <strong>Four rules are not, and the walk carries exactly four pieces of state, one per rule.</strong> GB12/13 needs how many regional indicators precede rather than whether one does (<code>🇯🇵</code> is one cluster, <code>🇯🇵🇯</code> is two). GB11 needs whether the ZWJ was itself preceded by <code>ExtPict Extend*</code> — the ZWJ alone does not say. GB9c needs whether a Linker appeared between two Consonants, which is why the InCB property is in the table at all. And GB9b is decided by the <strong>left</strong> character, the only rule that looks that way. <code>breaks_between</code> is written in the standard's own order so it can be checked against it line by line.</p> <p><strong>Three UCD properties folded into one class per code point</strong>, from three different files because that is how the UCD is arranged — and the folding is only sound because of three facts the generator <strong>asserts</strong> rather than trusts: every <code>Extended_Pictographic</code> code point has <code>Grapheme_Cluster_Break=Other</code> (all 2,848), <code>InCB=Consonant</code> is disjoint from the non-Other breaks, and <code>InCB=Linker</code>/<code>Extend</code> live inside <code>Extend</code> or <code>ZWJ</code>.</p> <p><strong>The table shape carried over unchanged from the JIS slice</strong>, which is the point of having settled it: 1,631 ranges as a fixed-width hexadecimal literal, fourteen characters each, with <code>Other</code> as the unstored default. A lookup is a binary search rather than an index this time, and nothing else about the decision changed — including the reason for hex, which is still that a <code>str</code> is <code>strlen</code>-based on the LLVM backend.</p> <p><strong>The Unicode version is pinned to the oracle's, before being bitten rather than after.</strong> <code>Intl.Segmenter</code> follows whatever node's ICU implements; a table of a different vintage would differ from it for reasons that are neither a bug nor interesting. Both the generator and the harness assert <code>process.versions.unicode</code>, so a node upgrade fails with one line instead of a page of diffs. That is the node 22/24 lesson from <code>url_parity</code> applied in advance.</p> <p><strong>The oracle here is worth more than the others in this repository.</strong> It is not a second reading of a specification this code also reads — it is ICU, which is what browsers ship. 8,509 inputs, none hand-picked: every ordered pair from 22 class representatives, every triple from 8, every quadruple from 7 (which is what reaches <code>ExtPict Extend ZWJ ExtPict</code>), runs of 1..8 regional indicators and 1..5 of each repeating shape, and <strong>both ends of every one of the 1,631 ranges in the generated table</strong> — so a shifted range shows up as a segmentation difference rather than waiting for a character nobody tested. All agreeing.</p> <p>The corpus is generated once and written to a file both sides read, because writing the same list of code points twice in two languages is how the two lists come to disagree.</p> <hr> <h2 id="v0-1-238-2026-08-13">v0.1.238 — 2026-08-13</h2> <p>_The first table in this project too large to write as code — so it is generated, and the question of what a 35KB literal does to five backends is answered by measuring it._</p> <pre><code> scripts/gen_jis_index.sh derives both JIS indexes from the Standard's own files contrib/encoding/jis_index.mere generated: 2 x 8,836 slots as fixed-width hex contrib/encoding/jis.mere Shift_JIS and EUC-JP scripts/encoding_parity.sh + 196,608 sequences, compared a different way </code></pre> <p><strong>The table question, settled by measurement.</strong> A 35,344-character string literal compiles and runs <strong>identically on interp, C, LLVM and Wasm</strong>, and <code>mere -rv</code> emits a 37,633-byte RV32I image from it — so the worry about what a large literal does to a backend's rodata is answered rather than assumed. (RV32I verified at emit; running it needs an emulator that lives elsewhere.) No startup expansion, no data file, no compression: a slot is four O(1) <code>char_at</code> reads.</p> <p><strong>The encoding of the table is decided by the LLVM backend's `str`, not by size.</strong> Raw 16-bit values would be half as long, and are unusable: a <code>str</code> there is <code>strlen</code>-based and a raw table is full of 0x00 bytes (U+00A2 is <code>00 A2</code>). Fixed-width hexadecimal is NUL-free by construction, and <code>0000</code> doubles as the hole sentinel because U+0000 is not a mapping either table produces. Two open questions meeting in one design decision is worth writing down.</p> <p><strong>`contrib/encoding/jis.mere`.</strong> Shift_JIS and EUC-JP. Four details that are easy to get plausibly wrong: Shift_JIS's lead offset is <strong>two</strong> numbers (0x81 below 0xA0, 0xC1 above, because the single-byte katakana range sits in the middle of what would otherwise be one contiguous lead range); there is a <strong>private-use window past the end of the table</strong> (pointers 8836..10715 are U+E000 onwards, the vendor extensions the encoding grew); an unmapped pair <strong>puts an ASCII trail byte back</strong> (<code>82 40</code> is U+FFFD then <code>@</code>, the same rule as UTF-8's); and EUC-JP has <strong>two tables and a three-byte form</strong>, a 0x8F lead selecting JIS X 0212 for the pair that follows it.</p> <p><strong>The tables are derived from the Standard, not from node — and that is a change of oracle with a measured reason.</strong> node's <code>shift_jis</code> is ICU's CP932. Its <code>index jis0208</code> is <strong>identical</strong> to the Standard's in all 8,836 slots, but its <code>index jis0212</code> maps <strong>21</strong> pointers the Standard does not (from pointer 7708, the small Roman numerals), it remaps three single bytes in a cycle (0x1A→U+001C→U+007F→U+001A), it treats 0x80 as an error where the Standard returns U+0080, and its error recovery consumes a malformed sequence whole instead of putting an ASCII trail back. A browser implements the Standard, and accepting 21 code points the Standard does not is the same failure mode <code>contrib/url</code> guards against — agreeing with an implementation instead of a specification, in the permissive direction.</p> <p>So the generator reads the Standard's published index files and <strong>pins each file's `Identifier:` hash</strong>, which is the oracle-version lesson applied to a data file that states its own version.</p> <p><strong>The gate keeps node, and asserts something exact rather than something weaker.</strong> Strict equality would be asserting ICU. Instead: <strong>the two implementations never disagree about which character a byte sequence is</strong> — 75,547 inputs that both call characters, all agreeing — and every remaining difference must be either error handling (U+FFFD on at least one side) or the named three-cycle. Anything else is a table or pointer bug and fails. The sweep is exhaustive over all three two-byte spaces (196,608 more sequences, 268,032 in total).</p> <p>That framing was not chosen up front. The first run reported thousands of differences; each class was then measured and named, and what fell out was that the disagreements are entirely about error handling and never about identity. A gate that says that is more useful than one that says "equal".</p> <hr> <h2 id="v0-1-237-2026-08-13">v0.1.237 — 2026-08-13</h2> <p>_A decoder's interesting behaviour is all in its error cases, and the Encoding Standard specifies how many U+FFFD a malformed sequence produces — which is not one per byte._</p> <pre><code> contrib/encoding/decode.mere UTF-8, windows-1252, and the label table scripts/encoding_parity.sh 71,424 sequences swept against node's TextDecoder </code></pre> <p><strong>`contrib/encoding/decode.mere`.</strong> Bytes off a wire into a <code>str</code>. Decoding never fails — every malformed sequence becomes U+FFFD — so <code>decode_utf8</code> returns a <code>str</code> rather than an <code>?str</code>. The only thing that can fail is recognising a label, and <code>decode</code> returns <code>?str</code> for a <em>different</em> reason: the label named a real encoding that is not implemented yet, so a caller can tell "unknown encoding" from "known but unsupported" and say so instead of guessing.</p> <p><strong>Two facts here are counter-intuitive enough to be worth stating outright.</strong></p> <p><code>ascii</code> <strong>means windows-1252</strong>. So do <code>latin1</code>, <code>iso-8859-1</code>, <code>us-ascii</code> and <code>ansi_x3.4-1968</code>. The Standard folds them into one encoding on purpose, because that is what the deployed web already did — so a page that calls itself <code>ascii</code> and contains byte 0x80 has a euro sign in it, and an implementation that "helpfully" treats <code>ascii</code> as 7-bit produces U+FFFD where every browser produces <code>€</code>.</p> <p>And <strong>the replacement count is specified</strong>: <code>F1 80 80 41</code> is one U+FFFD then <code>A</code>, because those three bytes were a valid <em>prefix</em> and are one error together, while <code>E0 80 80</code> is three, because <code>E0</code> requires its first continuation in <code>A0..BF</code> — so <code>80</code> is not part of the sequence at all, and each remaining byte is then reconsidered on its own and fails on its own. Getting this wrong changes how many characters a page has, which changes every offset after it, and is invisible on valid input.</p> <p>That same bound mechanism does all the other rejecting with no separate checks afterwards: <code>ED</code> requires <code>80..9F</code>, which is exactly what keeps the surrogates unrepresentable, and <code>F0</code> requires <code>90..BF</code> while <code>F4</code> requires <code>80..8F</code>, bounding the range at both ends.</p> <p><strong>The gate sweeps rather than samples</strong>, because the error cases are invisible on valid input: every single byte (256), <strong>every two-byte sequence (65,536)</strong>, every three-byte lead × first continuation (4,096), every four-byte lead × first continuation (1,280), and every byte through windows-1252 (256) — 71,424 comparisons against node's <code>TextDecoder</code>. The two-byte sweep is exhaustive; the three- and four-byte sweeps are exhaustive in the dimension that carries the logic, with the rest held valid. Neither side builds an input as a string literal — both loop over the byte — so there is no escaping layer to get wrong. Labels are checked rather than derived (40 of them, including three that must <em>not</em> resolve), and that gap is printed as a SKIP: the harness cannot discover a label nobody listed.</p> <p><strong>A new, measured consequence of the LLVM backend's `str`.</strong> A decoded 0x00 is U+0000, and because that backend's <code>str</code> is <code>strlen</code>-based, <code>str_of_codepoint 0</code> yields the <strong>empty</strong> string — the character does not truncate the text, it <strong>vanishes</strong>, and every offset after it shifts by one. <code>41 00 42</code> decodes to length 3 on interp, C and Wasm, and to length 2 on LLVM. Silent loss is worse than truncation for anything that then indexes the result, so this is documented as 0x01..0xFF-safe there for now, and <code>test/parity/encoding_decode.mere</code> omits 0x00 with the reason written at the top rather than asserting the bug.</p> <p><strong>Shift_JIS and EUC-JP resolve as labels but have no decoder yet.</strong> They need the JIS X 0208 index — 6,879 code points — which is the first table in this project too large to write as code, and that question deserves settling once rather than per encoding. <code>meta charset</code> sniffing is likewise absent on purpose: it is a scan for tags and belongs with an HTML tokenizer, and doing it in two places is how the two come to disagree.</p> <hr> <h2 id="v0-1-236-2026-08-13">v0.1.236 — 2026-08-13</h2> <p>_An IPv6 address has many spellings and exactly one canonical form, so the serialiser is as much of the answer as the parser._</p> <pre><code> contrib/url/ipv6.mere eight 16-bit pieces, in and back out scripts/url_parity.sh + 53 IPv6 literals </code></pre> <p><strong>`contrib/url/ipv6.mere`.</strong> <code>[0:0:0:0:0:0:0:1]</code> and <code>[::1]</code> are the same host, and until now the host field carried whichever one was typed — a comparison on the text would have called them different origins. Both now parse to the same eight pieces and serialise to <code>[::1]</code>.</p> <p>The serialiser's rules are narrow enough to get plausibly wrong while still looking right on <code>[::1]</code>, which is what the corpus is weighted towards: the <strong>longest</strong> run of zero pieces is compressed, ties go to the <strong>first</strong> run (<code>[1:0:0:1:0:0:1:1]</code> is <code>[1::1:0:0:1:1]</code>), and a run of exactly <strong>one</strong> zero is left alone (<code>[1:0:2:3:4:5:6:7]</code> keeps it). <code>::</code> may stand for a single piece on the way in — <code>[1:2:3:4:5:6:7::]</code> is <code>1:2:3:4:5:6:7:0</code>, which then serialises without any <code>::</code> at all.</p> <p>A trailing dotted quad occupies the last two pieces and is <strong>strict</strong> dotted decimal: four parts, no leading zeros, no hex. That is deliberately <em>not</em> the multi-base parser a bare host uses, so <code>[::0x7f.1]</code> and <code>[::01.2.3.4]</code> are not addresses while <code>http://0x7f.1</code> is <code>127.0.0.1</code>. Two IPv4 parsers in one file looks like duplication and is not — they are answering different questions, and the comment says so at both.</p> <p><strong>Implementation note worth keeping.</strong> The Standard's IPv6 parser is a pointer-walking state machine over a mutable eight-slot array. This is the same algorithm expressed by splitting: once on <code>::</code> (three parts means two of them, which is one too many), then each side on <code>:</code>, with the zero fill computed from the two lengths. A trailing quad is folded into two hex groups <em>before</em> the split, so nothing downstream has to know about dots — and anything else containing one fails on its own, because <code>.</code> is not a hex digit. No mutation, and the failure cases fall out rather than being enumerated.</p> <hr> <h2 id="v0-1-235-2026-08-13">v0.1.235 — 2026-08-13</h2> <p>_Resolution against a base, hosts checked one byte at a time — and the first place the oracle turned out to be wrong, which the harness now says out loud._</p> <pre><code> contrib/url/host.mere resolve; forbidden host code points; domains decoded contrib/url/percent.mere decode_strict scripts/url_parity.sh + 272 resolve pairs, + 190 single-byte hosts, + DIVERGE </code></pre> <p><strong>`Url.resolve base input`.</strong> What every link in a page needs: the reference wins wherever it says anything and the base fills in the rest. A relative path replaces the base's last segment and is then normalised, so <code>../d</code> against <code>/a/b/c</code> is <code>/a/d</code>; a reference that says anything at all about the path drops the base's query, and one that says nothing keeps it.</p> <p>The rule worth stating separately: <strong>a reference carrying a special scheme that names the base's own scheme is relative, not absolute.</strong> <code>http:d</code> against <code>http://h/a/b/c</code> is <code>http://h/a/b/d</code>; <code>https:d</code> against the same base is <code>https://d/</code>. Getting that backwards turns a same-origin relative link into a request to a host the page named.</p> <p><strong>Forbidden host code points, and domains are decoded.</strong> A domain is percent-decoded before anything else looks at it, so <code>%41</code> is <code>a</code> and <code>%2e</code> is a <strong>label separator</strong> — <code>http://0x7f%2e1</code> is <code>127.0.0.1</code>. An allowlist that inspects the host before decoding sees an opaque name where there is an address. A malformed escape means it is not a domain (<code>http://a%b/</code> is not a URL, hence <code>Percent.decode_strict</code>), and the forbidden code points are checked <strong>after</strong> decoding, so <code>http://a%2fb</code> has a <code>/</code> in its host and is rejected. An opaque host is neither decoded nor folded, but the forbidden points still apply to it.</p> <p><strong>Two new derived gates.</strong> Resolution is checked as a <strong>cross product</strong> — 8 bases × 34 references = 272 pairs, each compared as <code>href</code> — because the interesting cases are combinations, and picking pairs by hand is picking the ones already thought of. Hosts are checked <strong>one byte at a time</strong>: <code>http://aXb/</code> and <code>foo://aXb/</code> for every X in 0x20..0x7E, 190 comparisons, with neither side building the string as a literal (both loop over the byte, so there is no escaping layer to get wrong). That is the component where being too permissive is worst, and it caught the missing forbidden-code-point check and the missing domain decode together.</p> <p><strong>And the oracle was wrong once.</strong> The Standard's <em>no scheme state</em> admits a reference against an opaque base only when the reference's <strong>first</strong> code point is <code>#</code>. node v24 accepts any reference that merely contains one — <code>new URL("?q#f", "mailto:x@y")</code> is <code>mailto:x@y?q#f</code>, and <code>new URL("e?q2#f2", "mailto:x@y")</code> invents a path segment and gives <code>mailto:x@y/e?q2#f2</code>. We follow the Standard, and the harness prints these as <code>DIVERGE</code> lines with the count and both answers rather than dropping them from the corpus.</p> <p>Overruling the oracle here and not elsewhere is a judgement, so the reasoning is recorded next to it: the spec text is explicit (unlike the <code>^</code> in the path set, where the prose did not name it and the implementation did), and the divergence is in the <strong>permissive</strong> direction — accepting more than you should is the failure mode this gate exists to find. A gate needs somewhere to say "the oracle is wrong here" or the first time it happens the answer is to quietly delete the test.</p> <hr> <h2 id="v0-1-234-2026-08-13">v0.1.234 — 2026-08-13</h2> <p>_The rest of the URL, and two places where the previous slice's simplifications turned out to be wrong about where a delimiter stops mattering._</p> <pre><code> contrib/url/path.mere dot segments, query / fragment split, per-piece encoding contrib/url/host.mere the authority rules corrected; href scripts/url_parity.sh 95 inputs vs node, nine fields each </code></pre> <p><strong>`contrib/url/path.mere`.</strong> Splitting <code>rest</code> into path / query / fragment, resolving <code>.</code> and <code>..</code>, and encoding each piece with its own set. The fragment starts at the <strong>first</strong> <code>#</code> and the query at the first <code>?</code> before it; neither delimiter is special inside the fragment, so <code>#a?b</code> is a fragment of <code>a?b</code>.</p> <p>A dot segment is matched on the <strong>whole</strong> segment and includes its percent-encoded spellings case-insensitively — <code>.</code>, <code>%2e</code>, <code>..</code>, <code>.%2e</code>, <code>%2e.</code>, <code>%2e%2e</code>. So <code>/a/%2E/b</code> is <code>/a/b</code>, but <code>/a/..%2f</code> is left exactly as it is: <code>..%2f</code> is a segment that begins with two dots, not a dot segment. And a <strong>trailing</strong> dot segment leaves an empty segment behind, which is what keeps the trailing slash: <code>/a/b/..</code> is <code>/a/</code>, not <code>/a</code>.</p> <p>The path is never decoded. <code>%41</code> stays <code>%41</code> and <code>%2f</code> stays <code>%2f</code> in whatever case it arrived in — decoding an escaped slash into a separator is a path-traversal bug with a long history.</p> <p>An <strong>opaque path</strong> — no authority <em>and</em> no leading <code>/</code> — is not segmented and gets only the <code>c0_control</code> set, which is why <code>mailto:a b</code> keeps its space. <code>foo:/a/../b</code> is <code>foo:/b</code>, but <code>foo:a/../b</code> keeps its dots.</p> <p><strong>Two things the previous slice had wrong, both found by widening the oracle corpus.</strong></p> <p><em> </em><em>A special scheme's authority needs no slashes, or any number of them.</em><em> `http:h`, `http:/h`, `http:\\h` and `http:///h` all have host `h`. The old code required `"//"` and rejected the rest, so it saw a relative reference where there was in fact a host — which is the shape of a real allowlist bypass. A backslash also </em>ends<em> an authority: `http://u\p@h/` has host `u`, because the `\` comes before the `@` ever does. </em> <strong>Backslash folding belongs to the path, not the whole URL.</strong> The old code folded <code>\</code> to <code>/</code> across everything after the scheme. <code>http://h/a\b?c\d#e\f</code> has path <code>/a/b</code> but query <code>c\d</code> and fragment <code>e\f</code>, both keeping the backslash and both leaving it unencoded.</p> <p><strong>`href`, and three booleans.</strong> <code>has_authority</code>, <code>has_query</code> and <code>has_fragment</code> are fields rather than being folded into the strings, because presence and content are separate state: <code>foo://</code> and <code>foo:</code> have the same empty host but only one has an authority, and <code>http://h/?</code> has an empty query that still serialises its <code>?</code>. <code>href</code> is what needs them, and round-tripping is the honest test of a parse — a dropped field or an invented delimiter shows up there and nowhere else.</p> <p><strong>The gate is now one section of nine fields over 95 inputs</strong> (<code>scheme|user|pass|host|port|path|search|hash|href</code>), up from five fields over 24. <code>search</code> and <code>hash</code> are derived from the two bools to match node's shape, where the delimiter is carried when the component is non-empty and dropped when it is present but empty.</p> <p><code>test/parity/url_host.mere</code> grew to match, and <code>examples/url_parse_demo.mere</code> prints every field of a parse. Both hold all four backends to one output.</p> <p>One find worth writing down for anyone else generating Mere source from a shell: a <code>{</code> in a string literal opens interpolation, so a corpus entry like <code>mailto:a{b</code> has to be escaped as <code>\{</code>. The compiler's error message says so exactly, which is the only reason this cost one run instead of an afternoon.</p> <hr> <h2 id="v0-1-233-2026-08-13">v0.1.233 — 2026-08-13</h2> <p>_Two gates, two classes of bug: the oracle found three places the parser was too permissive, and the backend-parity test found that <code>str_replace</code> had never worked when compiled to C._</p> <pre><code> contrib/url/host.mere scheme / userinfo / host / port, WHATWG scripts/url_parity.sh + authority section, 24 inputs vs node, per field lib/codegen_c.ml str_replace: allocate a str, not a raw buffer test/parity/string_ops.mere str_replace across four backends, with lengths </code></pre> <p><strong>`contrib/url/host.mere`.</strong> The scheme and authority half of a WHATWG URL: cleaning, the scheme, userinfo / host / port, and <code>origin</code>. <code>rest</code> hands the path, query and fragment over untouched for the next slice. It returns <code>?url_parts</code> rather than failing, because rejecting input is half of what a URL parser does — and on the wasm backend <code>fail</code> sets a flag and returns a sentinel instead of unwinding, so a caller sitting between the failure and its <code>try_or</code> still runs. A rejection has to be a value.</p> <p>Two behaviours in here are the kind that become security bugs when an implementation guesses. A host that parses as a number is an <strong>address</strong>, in any base the Standard allows: <code>http://0x7f.1</code> is <code>127.0.0.1</code>, and an allowlist that only understands dotted decimal passes it through as a hostname and then resolves it to localhost. And a domain is lowercased while an opaque host is not — case folding belongs to the special schemes, not to hosts.</p> <p><strong>The authority gate: 24 inputs against node, compared field by field.</strong> One line per input as <code>scheme|user|pass|host|port</code>, so a mismatch names the field rather than just the URL. Inputs node rejects must come back <code>None</code> from us too, because a parser that accepts <em>more</em> than the oracle is the failure mode that matters for anything that then makes a request. It found three:</p> <p><em> `http://256.1.1.1` and `http://1.2.3.4.5` were accepted as hostnames. Conflating "the IPv4 parse failed" with "so it must be a domain" is exactly the hole above, from the other side. Fixed by asking first whether the host </em>ends in a number<em>: if it does the host is an address and a bad one is invalid, and if it does not IPv4 never applies. </em> <code>http://a@b@h/</code> produced an unencoded username. The userinfo set includes <code>@</code>, so it would re-split differently on the way out. Now <code>Percent.encode Percent.userinfo</code>.</p> <p><strong>`str_replace` was returning a buffer with no length header on the C backend.</strong> A Mere <code>str</code> in C carries its length in a <code>size_t</code> at <code>[-1]</code>, written by <code>__lang_str_alloc</code>. <code>__lang_str_replace</code> allocated with the raw <code>__lang_region_alloc</code> instead, so <code>__lang_str_size</code> of its result read whatever bytes happened to precede the buffer in the region. Those bytes were zero often enough that <strong>every replacement came back empty</strong> — which is how this surfaced: <code>Url.clean</code> removes tabs with <code>str_replace</code>, so on the C backend it returned <code>""</code> and every valid URL was rejected, while node and the interpreter agreed with each other the whole time. The cap is a worst case, so the header is also corrected down to what was actually written. The empty-needle guard now tests the length rather than <code>old[0]</code>, so a needle that <em>is</em> a NUL byte is replaceable like any other.</p> <p>A sweep for the same shape found no other instance: <code>__lang_str_alloc</code> is the only other function that region-allocates directly, which is its job.</p> <p><code>str_replace</code> had no cross-backend coverage at all — only an interpreter test — which is why this survived. <code>test/parity/string_ops.mere</code> now exercises eleven cases (shorter, longer, equal, absent, whole-string, empty subject, empty needle, overlapping, UTF-8, grow-from-one, then-concatenated) and <strong>prints the length of every result as well as the text</strong>. The length is the point: the text alone would not have caught a garbage header on every input. Reverting the fix turns that test red.</p> <hr> <h2 id="v0-1-232-2026-08-13">v0.1.232 — 2026-08-13</h2> <p>_A percent-encode set is a list of bytes somebody transcribed, so it was checked against somebody else's implementation instead._</p> <pre><code> contrib/url/percent.mere 7 sets, encode / decode scripts/url_parity.sh derives each set from node's URL, byte by byte </code></pre> <p><strong>`contrib/url/percent.mere`.</strong> The URL Standard has no single escaping rule; it has a stack of percent-encode sets, and which one applies depends on the component being written. Getting the component wrong is not cosmetic — a <code>#</code> left unencoded in a path ends the path. So <code>encode</code> takes the set as a predicate on a byte and the named sets are supersets of one another: <code>c0_control ⊂ fragment</code>, and <code>c0_control ⊂ query ⊂ special_query</code>, and <code>query ⊂ path ⊂ userinfo ⊂ component</code>.</p> <p>Encoding walks bytes, not codepoints, which is what the Standard says and is the reason a <code>str</code> being a byte buffer is the right shape here. Decoding leaves a <code>%</code> that is not followed by two hex digits exactly as it found it: refusing would make a literal percent sign unrepresentable, and decoding it as zero would invent a NUL.</p> <p><strong>`scripts/url_parity.sh` derives the sets rather than asserting them.</strong> For each byte in 0x20..0x7E it puts that byte alone into a component of an http URL, reads node's serialisation back, and records whether it came out as <code>%XX</code>. That is node's set for the component; ours is diffed against it. A fixture file would only have covered the bytes somebody thought to write down — and this found two wrong sets on the first run: <code>fragment</code> was missing <code></code> <code> </code><code> and </code>path<code> was missing </code>^<code>.</code></p> <p><code>^</code> in the path set is in there because the oracle encodes it, which is not the same thing as the prose naming it. That is recorded at the definition, so the next person sees a citation rather than a magic number.</p> <p>Four bytes per component cannot be probed this way and the harness prints them as SKIP rather than passing them silently: a byte that delimits the component under test ends it instead of being escaped in it, a lone space is stripped by URL parsing before escaping happens, <code>.</code> in a path is resolved away, and <code>\</code> is normalised to <code>/</code> for the special schemes.</p> <p><code>test/parity/url_percent.mere</code> holds all four backends to the same output. <code>scripts/url_parity.sh</code> skips when node is absent, like <code>qemu_virt.sh</code> does.</p> <p><strong>`contrib/http/query.mere`'s `url_encode` / `url_decode` are deliberately untouched.</strong> They implement the query-string convention a server wants — an allowlist of <code>alnum -_.~</code> — which is not any of the Standard's sets. Changing them would change every existing caller's behaviour, so the two live side by side with the difference written down.</p> <p>Not here yet: the parser itself, punycode, and <code>decode</code> into <code>bytes</code>. That last one is blocked on the llvm <code>str</code> being <code>strlen</code>-based, so <code>Percent.decode "%00"</code> differs by backend; until that changes, callers that can receive <code>%00</code> should treat this as ASCII-safe.</p> <hr> <h2 id="v0-1-231-2026-08-13">v0.1.231 — 2026-08-13</h2> <p>_The codepoint pair, written in the prelude rather than five times._</p> <pre><code> str_of_codepoint : int -> str codepoint_of : str -> int codepoint_at : str -> int -> int </code></pre> <p><strong>Q-014's last deferred piece.</strong> The Unicode question was settled in v0.1.38/45 — a <code>str</code> is a UTF-8 byte buffer, with a codepoint view composed above it — and one sliver was left open on purpose: the integer form of a codepoint, to be added "when an external program asks for it." Percent-encoding and punycode ask for it.</p> <p>It went in as <strong>prelude source, not builtins</strong>. The first attempt added them to the typer, the interpreter and the C backend, and stalled at the point of hand-writing the decoder in LLVM IR and WAT — at which point it was obvious the whole thing composes out of <code>chr</code>, <code>ord</code> and <code>char_at</code>, which every backend already has. The codepoint layer has been prelude-composed since v0.1.38 for the same reason. Six declarations, no codegen touched, and all four backends agree because there is only one definition.</p> <p><code>chr</code> and <code>ord</code> stay byte-shaped: the redis, pg and http drivers build 0..255 bytes with them. And the divergence <code>chr</code> carries — out-of-range raises on the interpreter and masks on wasm and llvm — is not repeated: the new pair fails on all four. A scalar value is 0..0x10FFFF less the surrogates, and <strong>overlong encodings are refused</strong>, because two spellings of one character is how a filter gets walked past.</p> <p><code>test/parity/codepoint_pair.mere</code> round-trips both sides of every length boundary (127/128, 2047/2048, 65535/65536, 1114111) and refuses ten ill-formed inputs: negative, above max, both surrogate ends, empty, two codepoints, truncated, <code>C0 80</code>, a bare continuation byte, and a lead byte with nothing after it.</p> <p><strong>Getting that file green on four backends turned up three older holes</strong>, none of them this change's:</p> <ul> <li><strong>The llvm `str` is not byte-safe.</strong> The v0.1.129 work reached C and wasm; llvm still</li> </ul> <p> implements <code>str_len</code> as <code>call @strlen</code> and has no <code>__lang_str_size</code> anywhere, so a <code>str</code> holding a NUL cannot exist there. <code>str_len (chr 0)</code> is 1 on interp, C and wasm, and 0 on llvm, whose <code>chr</code> returns a raw pointer into a 256-entry table. U+0000 is left out of the parity file for this reason and checked in the unit tests instead.</p> <ul> <li><strong>`fail` does not unwind on wasm.</strong> It sets a global and returns a sentinel, so whatever</li> </ul> <p> sits between the <code>fail</code> and the <code>try_or</code> still runs. <code>1 + (fail "b")</code> survives, because an integer tolerates the sentinel; <code>str_len (fail "b")</code> traps the module and the program dies without even the <code>try_or</code> default. Which failure you get depends on what the consumer does with the value.</p> <ul> <li><strong>`print` stops at an interior NUL.</strong> <code>str_len</code> says 2 and <code>print</code> emits one character,</li> </ul> <p> on the same value. <code>print_bytes</code> (v0.1.219) is the byte-safe writer, but <code>print</code> is silently lossy rather than either correct or refusing.</p> <p>Also: <code>codepoint_at</code> is <code>let rec</code> for a reason that has nothing to do with recursion. The decl loop is duplicated in test helpers that bind <code>Top_let_rec</code> and <code>Top_let</code> separately, so a plain <code>let</code> here reaching back to <code>let rec utf8_at</code> is unbound in those copies. Same duplication that made the quadratic-inference fix land in six places.</p> <hr> <h2 id="v0-1-230-2026-08-13">v0.1.230 — 2026-08-13</h2> <p>_A loop was a function calling itself, and whether it survived was up to clang._</p> <pre><code> while, 200 000 iterations before after clang -O0 SIGSEGV ok (10 000 000 ok) clang -O1 and up ok ok </code></pre> <p><strong>`while` never reached a backend as a loop.</strong> The parser desugars <code>while cond do body</code> into a tail-recursive <code>let rec</code>, so by the time codegen sees it there is nothing left that says "iteration" — the C backend emitted a self-recursive function and left the tail call to the optimizer. That made the iteration bound a property of the build: <code>-O0</code> died with no output at somewhere between 100 000 and 200 000, <code>-O1</code> did not.</p> <p>The build that dies is the one <code>scripts/debug_info.sh</code> prescribes: <code>clang -g -w -O0</code>. So the debugger work of v0.1.212-215 — a compiled program you can step through against its Mere source — and a program with a loop in it were mutually exclusive, and nothing said so. A 100 KB input walked one byte at a time is 100 000 iterations.</p> <p><code>for i in a..b do body</code> was worse, not better: <code>range</code> materialises the whole range as a list and <code>list_iter</code> then recurses down it. There was no safe way to iterate.</p> <p><strong>A self tail call now lowers to a goto.</strong> Tail position is tracked the way <code>codegen_wasm</code> already tracked it — taken on entry to <code>emit_expr</code>, handed on only by the cases where a subexpression really is in tail position (<code>Annot</code>, both arms of <code>If</code>, the body of <code>Let</code> in each of its four pattern forms, the body of <code>Let_rec</code>, and match arms; deliberately not <code>Region_block</code> or <code>With</code>, which have reclamation left to do after the value). The call becomes argument temporaries, assignments to the parameters, and a jump:</p> <pre><code class="language-c"> int f(vec* i, long long n, int u) { int __mere_ret; __mere_tail: ; return (cond ? ({ __auto_type __mt0 = i; ...; i = __mt0; ...; goto __mere_tail; __mere_ret; }) : 0); } </code></pre> <p><code>__mere_ret</code> is never evaluated; it is there to give the statement expression the function's return type, which is what lets a <code>goto</code> sit in expression position at all — the C backend is expression-oriented and cannot restructure a body into a statement loop. Verified by hand first, with a struct return, before any of this was written.</p> <p><strong>Two things the parity suite caught, both of which had built and linked cleanly.</strong></p> <p><code>schema_reflect</code> printed nothing. Rewriting <code>If</code> to name its operands had reversed the order they were emitted in: OCaml evaluates the right argument of <code>^</code> first, so the original chain emitted else, then, cond — and <code>emit_expr</code> interns strings and numbers closures as it goes, so the order is part of the output. Restored, with a comment, since nothing in the expression makes it look load-bearing.</p> <p>Then it looped forever. <code>*(&p)</code> rather than <code>p</code> is why the assignments go through aliases taken at function entry: <code>Json.parse_object</code> binds <code>let j = skip_ws s j</code> five times, so by the tail call the name <code>j</code> is a local shadowing the parameter. Assigning by name wrote the shadow and jumped with the parameter unchanged. The address is taken before the body runs, so it names the parameter however the body rebinds that name.</p> <p>Neither showed up in <code>dune runtest</code> or <code>ctest.sh</code> — the first needs a program whose output depends on interning order, the second a function that shadows its own parameter and tail-calls itself. <code>MERE_NO_TAIL_LOOP=1</code> disables the rewrite, which is what made "is it this change?" a one-variable question; <code>MERE_TAIL_ONLY=<substring></code> narrows it to matching callees.</p> <p>The prologue is only emitted for functions that actually produced a jump, so a function with no self tail call is emitted exactly as before. That is per function, not per program: the prelude has tail-recursive functions of its own, so <strong>all 84 parity programs contain a rewritten function</strong>, which is what makes 85/0 there worth something rather than an accident of coverage. <code>examples/bst.mere</code> has one of its own (<code>lookup</code>), and its output is byte-identical before and after, at <code>-O0</code> and <code>-O2</code>, and against the interpreter.</p> <p><code>__attribute__((noinline))</code> on the <code>show_*</code> helpers has a comment from v0.1.31 saying it exists because an inlined helper's escaped local defeated clang's sibling-call optimisation and deep loops overflowed. That was this bug, seen once from the other end and worked around locally.</p> <p><strong>The LLVM backend has the same defect</strong> — <code>-O0</code> dies at 10 000 000, and the emitted IR carries no <code>tail</code> marker anywhere. The fix there is <code>musttail</code>, which LLVM honours regardless of optimisation level, but it requires the call to be immediately followed by the <code>ret</code> of its result, and this backend has no tail-position notion and returns from many places. Measured and left open rather than guessed at.</p> <p><strong>`show` on a float gave four different answers.</strong> The interpreter formatted it, C printed <code><unsupported></code>, LLVM printed <code>()</code> — a number rendered as unit — and Wasm printed <code><?show_float?></code>. Each backend's <code>show</code> generator simply had no float case and fell through to a different placeholder. <code>parity.sh</code> never caught it because no program in the suite showed a float; the probe that found it could not print the time it had just measured. The formatter was already there and already shared, so this only wires it up in the three generators. All four now agree, including <code>0.1 + 0.2</code> at 17 digits and the trailing <code>.0</code> on whole values.</p> <hr> <h2 id="v0-1-229-2026-08-13">v0.1.229 — 2026-08-13</h2> <p>_Formatting a long function was quadratic in two places, neither of them arithmetic._</p> <pre><code> 16 000 nested lets 22 810-line file fmt 1.30s -> 0.05s 1.23s -> 0.67s </code></pre> <p><strong>A run of `let ... in` is now written out as a run.</strong> The formatter recursed into the body and concatenated what came back, so every level copied the whole remainder of the function into a new string. All 87 profile samples were inside <code>Stdlib.(^)</code>. Each binding in a run sits at the same indent as the one before it, which is what makes the run flattenable at all.</p> <p><strong>`rename_free_vars` carried its shadow set as a list</strong>, extended with <code>@</code>. That copies the whole list at every binding, and each <code>Var</code> scanned it linearly — so a pass whose entire job is renaming names cost O(depth²). It is a set now, which shares structure. This one is not the formatter's: <code>mere -t</code> on the same input went 2.60s to 1.34s, because every path that parses pays for it.</p> <p>The formatter's output is unchanged: 193 files — every example, every <code>contrib</code> source, every parity program, and a 22 810-line one — format byte-identically before and after. That is the only acceptable evidence for a change to a formatter.</p> <p><strong>What is left, measured rather than assumed</strong>: <code>mere -t</code> on that 16 000-deep chain is still quadratic (0.10 / 0.36 / 1.34 at 4k / 8k / 16k), because the type environment is an association list and looking up a variable bound at the top of a function walks every binding since. The deepest run of consecutive <code>let</code>s in this repository's own sources is <strong>281</strong>, and those files type-check in 0.02s. So it is real, it is not biting, and rewriting the environment on the strength of a synthetic chain is the kind of thing Q-023 exists to say no to. Recorded as Q-026.</p> <h2 id="v0-1-228-2026-08-13">v0.1.228 — 2026-08-13</h2> <p>_The support matrix is asked for rather than remembered, and it found three holes._</p> <p>There have been three hand-written versions of "which backend has which host builtin": a table in the design notes, and a list in each of <code>codegen_llvm.ml</code> and <code>codegen_wasm.ml</code> naming the builtins with no lowering. All three had gone stale in the same direction — <code>print_int</code> gained real lowerings in v0.1.190 and <code>file_pwrite_bytes</code> in v0.1.222, and both were still listed as missing, in code that is inert rather than wrong. A table nobody can trust is worse than no table.</p> <p><code>scripts/host_matrix.sh</code> produces one instead: fifty one-line programs, each using a host builtin, emitted for each backend, and the outcome recorded as <code>yes</code>, <code>refused</code> (the backend says so itself), <code>MISSING</code> (<code>unbound variable</code> — the compiler blaming the user for a backend hole), or <code>error</code>. The result is <code>docs/host-matrix.md</code>, checked in and diffed on every run.</p> <p>The first run found three <code>MISSING</code>, all of them the failure this project's loud-failure rule exists to prevent:</p> <ul> <li><strong>`read_bytes` and `write_bytes` on LLVM and Wasm.</strong> They arrived with the <code>bytes</code></li> </ul> <p> type in v0.1.216, <em>after</em> the per-backend lists were written, and fell straight through to <code>unbound variable</code>. The hole those lists exist to close, reopened by a later feature — which is exactly what a generated matrix is for.</p> <ul> <li><strong>`par_map` on Wasm</strong>, which said <code>unbound variable: __pm_f1</code>: a name the user never</li> </ul> <p> wrote, about a function they do not know exists. <code>par_map</code> is desugared at parse time into spawn + channel + list_map, and Wasm cannot resolve the captured function from inside the nesting that produces. The limitation stands; it now names <code>par_map</code>.</p> <p>The matrix is now 50 builtins, <strong>0 MISSING</strong>.</p> <p>What it cannot see is a builtin that compiles and then does nothing — <code>tcp_set_timeout</code> on Wasm in v0.1.227 returned success and hung. Only running a program catches that, which is <code>scripts/parity.sh</code> and <code>scripts/socket_parity.sh</code>. The two kinds of check answer different questions and neither replaces the other.</p> <h2 id="v0-1-227-2026-08-13">v0.1.227 — 2026-08-13</h2> <p>_A capability that quietly did nothing now refuses._</p> <p><code>tcp_set_timeout</code> had a Wasm helper that ignored both arguments and returned <code>0</code> — the same value the C version returns on success. So a program set a deadline, was told it had one, and blocked forever on the next read. Measured at ten minutes before it was killed.</p> <p>That is worse than not having the capability at all: a missing feature is a compile error, a silent no-op is a hang. It is refused at the call site now, with the reason, until WASI's poll is wired up. A socket program that never sets a deadline is unaffected.</p> <p><strong>`scripts/socket_parity.sh`</strong> is what found it, and is why this was possible to find at all. <code>parity.sh</code> runs eighty-odd programs across four backends and <strong>none of them opens a socket</strong>, because a socket program needs two endpoints and a host willing to grant a network. So the whole socket family — <code>tcp_listen</code> through <code>tcp_close</code>, all of it implemented against p2 <code>wasi:sockets</code> — had never been run. It works: the same round trip prints the same four lines natively and under <code>wasmtime -S inherit-network=y</code>.</p> <p>Two things stay unequal and are recorded rather than fixed:</p> <ul> <li><strong>A failed read is `0` on Wasm</strong>, the same value C uses for a clean end of stream.</li> </ul> <p> Distinguishing them means decoding WASI's <code>stream-error</code> variant instead of its is-error bit. The check above deliberately does not assert on it.</p> <ul> <li><strong>`sleep_ms` is interp + C only</strong>, which is why the parity program contains no</li> </ul> <p> clock at all.</p> <p>The general shape is the one the host-builtin registry work named: a capability with a per-backend implementation and no single place that says which backends really have it. This is the second silent gap found by running one, after <code>print_int</code> in v0.1.190.</p> <h2 id="v0-1-226-2026-08-13">v0.1.226 — 2026-08-13</h2> <p>_A failed read says which failure it was._</p> <pre><code> > 0 bytes read 0 the peer closed cleanly — end of stream, not an error -1 nothing arrived before the deadline -2 the connection is gone -3 anything else </code></pre> <p><code>tcp_read</code> returned read(2)'s <code>-1</code> for everything. With <code>SO_RCVTIMEO</code> set that covers two opposite events: the deadline passed with nothing arriving, and the connection broke. One means wait again, the other means reconnect. The mraft dogfood told them apart by timing the call and asking whether it had failed slowly enough to have been a timeout — inferring a cause from a duration, and recorded as that repository's P4.</p> <p>The codes stay negative, so every existing <code>< 0</code> check is unaffected.</p> <p><code>scripts/tcp_read_codes.sh</code> <strong>produces</strong> all three rather than describing them: a socket nobody writes to, a peer that closes cleanly, and a peer that aborts with data still unread in its own receive queue — which is what makes <code>close()</code> send RST instead of FIN, and the only reliable way to get <code>ECONNRESET</code> without <code>setsockopt(SO_LINGER)</code>.</p> <p>Two things this turned up:</p> <ul> <li><strong>The socket externs were documented nowhere.</strong> Same as the positioned-IO family in</li> </ul> <p> v0.1.222: real, on every network program, and absent from the stdlib reference. Both are now in it.</p> <ul> <li><strong>Wasm disagrees about `0`.</strong> Its <code>tcp_read</code> goes through WASI <code>sock_sread</code> and</li> </ul> <p> returns <code>0</code> on error, where C returns <code>0</code> only at end of stream. So a program that treats <code>0</code> as "the peer closed" is wrong there. No parity test covers sockets, which is why nobody had noticed; recorded rather than fixed, because fixing it means mapping WASI's error set and there is no Wasm program that opens a socket yet.</p> <p>This answers the concrete case. The general question mraft's P4 asks — how a capability reports <em>why</em> it failed, rather than only that it did, across an FFI boundary that is C-shaped — is not answered by a convention about negative integers, and is still open.</p> <h2 id="v0-1-225-2026-08-13">v0.1.225 — 2026-08-13</h2> <p>_The editor stopped accepting names that no longer exist._</p> <p>Rename a constructor and keep using the old name:</p> <pre><code class="language-mere"> type t = Gamma | Delta; let x = Alpha in print "b" // Alpha was renamed away </code></pre> <p>The compiler says <code>unknown constructor: Alpha</code>. The language server said nothing — clean file, no diagnostic, until the build failed. Three versions of one document, driven through <code>mere lsp</code>:</p> <pre><code> v1 type t = Alpha | Beta; clean v2 type t = Gamma | Delta; ... Alpha clean <- wrong v3 type t = Gamma | Delta; ... Gamma clean </code></pre> <p>The parser has had <code>reset_decl_state</code> since a <code>type</code> in one program could shadow one in the next. The <strong>typer's</strong> registries — constructors, types, records, views, drop / sync / local types, record aliases — were never cleared. A compiler process checks one program, so nothing ever noticed; a language server checks one document per keystroke. The same leak made a <code>view Pair</code> and a later <code>type Pair</code> collide, which is how this was found: a test declaring a record hit "view Pair must be constructed inside a region block".</p> <p>Two changes, and the second is the one worth reading:</p> <ul> <li><code>Typer.reset_type_registries ()</code> runs next to the parser's reset. It restores a</li> </ul> <p> snapshot rather than emptying the tables, because the built-in capability records (<code>Logger</code>, <code>Metrics</code>) are registered at module load and a plain reset deletes them permanently. The snapshot is taken on the first call — which comes before any program's declarations are processed — so it does not depend on where in the file it is written.</p> <ul> <li><strong>What types exist is now established by `parse_program`</strong>, not only by whichever</li> </ul> <p> later walk happens to visit the declarations. <code>Typer.infer</code> on a desugared program never registered anything, so a caller that skipped <code>process_decls</code> type-checked against whatever the <em>previous</em> program in this process had declared. Dozens of tests did exactly that and passed, because nothing was ever cleared: resetting without this made <code>Nil</code> unknown. Registering is idempotent, so the later walks are unaffected.</p> <p>That second point is the real defect. The first is what exposed it.</p> <h2 id="v0-1-224-2026-08-13">v0.1.224 — 2026-08-13</h2> <p>_A name bound by a constructor pattern can cross a thread boundary._</p> <pre><code class="language-mere"> match ports with | Cons (p, rest) -> let _ = spawn (fn () -> sender p out) in ... </code></pre> <p>That was refused with <code>cannot capture \</code>p\<code> of unknown type across a thread boundary</code>, with <code>ports : int list</code> written down two lines above. The capture check's pattern binder handled <code>P_var</code> and a tuple pattern over a tuple type, and bound everything else — constructor patterns, record patterns — with an unknown type, which makes those names unusable across <code>spawn</code>. The mraft dogfood hit it on every peer it spawned a thread for, and the workaround (<code>let q = (p : int) in</code>, capture <code>q</code>) reads like superstition because it is: an ascription that tells the program nothing it did not already know.</p> <p>The declared payload type is enough to do better, with no unification and nothing this pass may mutate: the constructor registry knows the type parameters and the payload type, and the scrutinee's own arguments say what to substitute for them. Record patterns get the same treatment, field by field.</p> <p><strong>The diagnosis in the dogfood's PAIN.md was wrong</strong>, and worth recording as such. It said the Send check ran before inference had propagated the annotation — a plausible story about pass ordering, told without looking. The types were fully resolved; the binder simply never looked at that shape of pattern. A payload whose type genuinely is a variable is still refused, and now says so accurately (<code>of polymorphic type</code> rather than <code>of unknown type</code>).</p> <h2 id="v0-1-223-2026-08-12">v0.1.223 — 2026-08-12</h2> <p>_The reserved-name warning now looks at type names, which is where it was needed._</p> <pre><code> line 1, col 6: warning: type name `wait` collides with a C type, keyword or libc symbol — this will be a compile error at codegen, from the C compiler rather than from here. </code></pre> <p>A Mere <code>type</code> lowers to <code>typedef struct <name> <name>;</code>, which claims both C's tag namespace and its ordinary one. <code>type wait = ...</code> therefore collides with <code>union wait</code> in <code><sys/wait.h></code> <strong>and</strong> with the <code>wait()</code> declared beside it, and the failure arrived from clang:</p> <pre><code> error: use of 'wait' with tag type that does not match previous declaration </code></pre> <p>The compiler has had a list of libc and C-keyword names since v0.1.55 and warns when a top-level <code>let</code> collides with one. It never ran for <code>type</code> declarations. The mraft dogfood named a type <code>wait</code> on its first day and got the collision from the C compiler — the same "documented thing failing at the wrong layer" shape as the <code>print_int</code> bug in v0.1.190.</p> <p>The function list applies to type names unchanged (a typedef is an ordinary identifier), plus a new list of the struct tags the emitted headers bring in: <code>wait</code>, <code>tm</code>, <code>timeval</code>, <code>timespec</code>, <code>stat</code>, <code>dirent</code>, <code>sockaddr</code>, <code>addrinfo</code>, <code>hostent</code>, <code>termios</code>, <code>winsize</code>, <code>sigaction</code>, <code>iovec</code>, <code>fd_set</code>, <code>div_t</code> and the rest of that family.</p> <p>Two things about the implementation are worth recording:</p> <ul> <li><strong>`Top_type` carries no position</strong>, and adding one touches ten files. So the</li> </ul> <p> parser records <code>(name, loc)</code> for each type it declares — the same shape as the per-program tables it already keeps for constructors and records — and Pipeline reads that. A warning an editor cannot place is a warning nobody sees.</p> <ul> <li>The warning is raised on <strong>both</strong> paths: <code>process_decls</code>, which the compiler</li> </ul> <p> takes, and <code>infer_program</code>, which the editor takes. It went in on the first one only, and the test that checks it through <code>Pipeline.diagnostics</code> failed — which is exactly the test being worth writing.</p> <p>Nothing in <code>examples/</code> or <code>contrib/</code> trips it.</p> <h2 id="v0-1-222-2026-08-12">v0.1.222 — 2026-08-12</h2> <p>_A positioned write that takes the byte type the language grew afterwards._</p> <pre><code class="language-mere"> let n = file_pwrite_bytes h off (bytes_of_str line) </code></pre> <p><code>file_pwrite</code> was added for the mbtree dogfood in v0.1.115 and takes <code>Vec[int]</code>. The <code>bytes</code> type arrived in v0.1.216 and got its I/O boundary in v0.1.219 — so the language had a byte string, and the one API that writes at an offset could not take it. The mraft dogfood's write-ahead log had to explode every record into a Vec with <strong>one boxed int per byte</strong> before writing it.</p> <p><code>file_pwrite_bytes : File -> int -> bytes -> int</code> is the same operation over <code>bytes</code>. Both remain: mbtree builds its pages as Vecs and has no reason to change.</p> <p>All four backends, <code>test/parity/file_pwrite_bytes.mere</code>. Two of them cost almost nothing:</p> <ul> <li><strong>Wasm</strong>: the host import already took a bytes pointer — the Vec version converts</li> </ul> <p> first and then calls it. The new path is the same call with nothing in between.</p> <ul> <li><strong>LLVM</strong>: one <code>fwrite</code> instead of a loop, since <code>bytes</code> is <code>{ i64 len, i8 data[] }</code></li> </ul> <p> and <code>bytes_len</code> already loads from that layout.</p> <ul> <li><strong>C</strong>: the runtime function had to be defined next to the bytes runtime rather</li> </ul> <p> than with the other <code>file_*</code> ones, because those are emitted before <code>struct mere_bytes</code> has a body — the same ordering the ByteBuf freeze hit in v0.1.218.</p> <p>The LLVM path also registers the <code>Vec[int]</code> instance even though it uses no Vec: the positioned-IO runtime is emitted as one block and <code>file_pread</code>'s body calls the Vec accessors regardless. A program that only writes bytes carries a few unused functions, which is cheaper than splitting the block into per-function flags.</p> <h2 id="v0-1-221-2026-08-12">v0.1.221 — 2026-08-12</h2> <p>_A program's output no longer depends on whether someone redirected it._</p> <p><code>print</code> lowered to <code>puts</code>, and nothing in the runtime ever called <code>fflush</code>. C line-buffers a terminal and fully buffers a pipe, so a program whose output was redirected to a file printed nothing until it exited or accumulated 4KB.</p> <p>Every dogfood until now was a batch program — it printed and exited, and exiting flushed. The mraft dogfood is the first Mere program meant to be <em>watched while running</em>, and it logged nothing at all: a server started with <code>> log 2>&1</code> looked identical to one that had hung.</p> <p>The C backend now emits <code>setvbuf(stdout, NULL, _IOLBF, 0)</code> in <code>main</code>; the LLVM backend flushes after each <code>print</code> (<code>fflush(NULL)</code>, which needs no platform-specific <code>stdout</code> global — glibc and macOS name it differently). Both give piped output the same behaviour a terminal already had.</p> <p>The LLVM change also removed a duplicate <code>declare i32 @fflush(ptr)</code>: the file positioned-IO runtime had its own, and the second declaration is an error rather than a redefinition LLVM tolerates. <code>scripts/parity.sh</code> caught it — <code>file_pio</code> was the only program that emitted both.</p> <h2 id="v0-1-220-2026-08-12">v0.1.220 — 2026-08-12</h2> <p>_Type inference was quadratic in the number of bindings. It is linear now._</p> <pre><code> inference (mere -t) LSP, per keystroke 4 000 bindings 0.16s -> 0.02s 524ms -> 32ms 8 000 bindings 0.50s -> 0.04s 1834ms -> 65ms 16 000 bindings 1.72s -> 0.09s 7741ms -> 135ms 22 466 lines (real) 5261ms -> 1251ms </code></pre> <p><code>generalize</code> decided which type variables to quantify by collecting the free variables of <strong>every scheme in the environment</strong> and quantifying what was not among them. That is the textbook definition, and it costs O(environment) per binding — so checking N bindings cost O(N²).</p> <p>Nobody noticed while the compiler only ran once per file. The LSP shipped in v0.1.207 re-checks the whole document on every keystroke, which turned a cost nobody paid into 5.3 seconds of latency per character on a 22k-line file. The profile named one function.</p> <p>The fix is levels (Rémy's ranks): each type variable records how many generalizable bindings it was created inside, unification lowers that number when a variable escapes into an outer type, and generalization quantifies exactly the variables still deeper than the binding — no environment scan at all. The <code>level</code> field on <code>Ast.tyvar</code> is the whole representational cost.</p> <p><strong>The old definition is kept as an oracle.</strong> <code>MERE_LEVEL_CHECK=1</code> computes both answers at every generalization and reports any disagreement (add <code>MERE_LEVEL_CHECK_TRACE=1</code> for a call stack). They agree on all 2421 tests and on 390 further files — contrib, examples and the dogfood repositories. Since the quantified set is the <em>only</em> thing this change can affect, that comparison is the correctness argument, and the switch stays so the next change to the level discipline is checked against the definition it replaced.</p> <p>It found three real defects while being written, none of which any test caught:</p> <ul> <li><strong>`check_pattern` ran outside the binding's level</strong>, so the fresh variables it</li> </ul> <p> makes for a tuple pattern's components dragged the value's variables out with them: <code>let (f, g) = (fn x -> x, fn x -> x + 1) in f</code> stopped being polymorphic. <code>pp_ty</code> prints <code>('a -> 'a)</code> either way, which is why the existing test passed.</p> <ul> <li><strong>`trait_elab` has its own copy of the declaration loop</strong> and did not get the</li> </ul> <p> level discipline, which cost polymorphism for every binding in a program using traits.</p> <ul> <li><strong>The value restriction's monomorphic path</strong> left variables looking local to</li> </ul> <p> the binding they had just escaped, so the next binding out would quantify them — the one direction of this change that would have been unsound.</p> <p>The declaration loop now exists in six copies (three in <code>pipeline</code>, one in <code>trait_elab</code>, two in the tests). Each had to be found and fixed by hand here.</p> <h2 id="v0-1-219-2026-08-12">v0.1.219 — 2026-08-12</h2> <p>_<code>print_bytes</code> on Wasm, which makes it all four backends._</p> <pre><code> interp: 41004228290a C: 41004228290a LLVM: 41004228290a Wasm: 41004228290a </code></pre> <p>Wasm was the one backend that had to refuse this, and the reason was the host boundary rather than codegen: its printing goes through an <code>env.print_no_nl(ptr)</code> import that reads a <strong>NUL-terminated</strong> string out of linear memory, which is exactly what a byte sequence cannot be. So it needed a new import taking a pointer <em>and a length</em> — <code>env.print_bytes(ptr, len)</code> — and the host side in <code>scripts/run_wasm.js</code> to write that many bytes from memory.</p> <p><strong>Gated on use</strong>, like the imports around it: a program that does not call <code>print_bytes</code> declares nothing new and runs on an older host unchanged. That mattered here, since the playground ships prebuilt <code>.wasm</code> files.</p> <p>_The test names both halves: the import must appear when the builtin is used, and must not appear when it is not._</p> <hr> <h2 id="v0-1-218-2026-08-12">v0.1.218 — 2026-08-12</h2> <p>_<code>ByteBuf[R]</code>: the mutable byte buffer that was missing._</p> <p>_v0.1.216 gave <code>bytes</code> a way out of the program. What it still had no answer for was building or editing one: <code>bytes</code> is immutable, <code>StrBuf</code> appends only and is text, and <code>Vec[R, int]</code> does the job at <strong>eight bytes per byte</strong>. The thing that asked for it was reconstructing a PNG scanline, which reads the row above it — already reconstructed — and writes the row it is on. Random access both ways, and bytes._</p> <pre><code> bytebuf_new : int -> ByteBuf[R] n zeroed bytes bytebuf_len : ByteBuf[R] -> int bytebuf_get : ByteBuf[R] -> int -> int bytebuf_set : ByteBuf[R] -> int -> int -> unit bytebuf_push : ByteBuf[R] -> int -> unit appends, growing bytes_of_bytebuf : ByteBuf[R] -> bytes freeze a copy bytebuf_of_bytes : bytes -> ByteBuf[R] </code></pre> <p>Region-bound like <code>StrBuf</code>, and for the same reason: the bytes live in a region and the region is tracked by a pointer inside the struct rather than by the marker. Freezing copies into the current region, so a <code>bytes</code> frozen inside <code>region R { }</code> can be returned out of it — the mistake <code>strbuf_to_str</code> had to fix once already.</p> <p><strong>interp + C</strong>, which is where byte I/O lives.</p> <p>_Measured on the dogfood, decoding a 736×724 RGBA PNG: peak RSS <strong>164MB → 117MB</strong>, with byte-identical output. The reconstructed image is 2.1MB of bytes, which was 17MB of <code>int</code>s._</p> <p>_Adding the type re-found v0.1.217's P5 immediately, and twice — in both directions. Once because <code>ByteBuf</code> was missing from the list of region-parameterised constructors, so the marker erased to <code>int</code>. Then again with the names the other way round, which produced the better fix: <strong>a type whose C representation does not depend on its region should not carry the region in its tag at all.</strong> <code>StrBuf</code> and <code>ByteBuf</code> both lower to one C type each, with the region tracked by a pointer inside the struct, so <code>StrBuf___heap</code> was never carrying information — only an opportunity to disagree. Both now tag as their bare name, which removes the class rather than the instance, and gives <code>StrBuf</code> the fix for a bug it had never happened to trip._</p> <hr> <h2 id="v0-1-217-2026-08-12">v0.1.217 — 2026-08-12</h2> <p>_Two C-backend bugs the mpng dogfood found, both of which emitted C that a C compiler rejects._</p> <p><strong>A `let rec`'s names leaked into every later function.</strong> The inner-fn lifting pass keeps a set of names that are <em>not</em> captures — top-level names, builtins, externs, and the siblings of a <code>let rec</code>. The sibling names were added to that set and never removed, so a <strong>later</strong> top-level function whose parameter had the same name had it taken for one of them: not recorded as a capture, and the lifted body then referred to an identifier nothing declared.</p> <p>The line that was supposed to put the set back read <code>let _ = known_before in</code>, which does nothing. <code>known_before</code> was already being taken two lines above.</p> <p>_What it took to see it: <code>png.mere</code> has an inner <code>let rec row</code>, <code>encode.mere</code> has a parameter called <code>row</code>, and the second one lost it. Neither file alone reproduces, which is why this survived — the failure needs two files and a name in common._</p> <p><strong>An unresolved region marker tagged as `int`.</strong> <code>ty_tag</code> erases a type variable that survives to codegen (a dead result, an unconstrained value) to <code>int</code>, on the reasoning that no operation ever inspects such a value. That is true of values and false of a <strong>region marker</strong>: the marker sits in a type's first slot, the typedef for the same type was emitted from a copy where it had resolved to <code>__heap</code>, and the two spellings — <code>Vec_int_int</code> and <code>Vec___heap_int</code> — are a prototype for a type that does not exist. An unresolved marker now tags as the default region.</p> <p>_Both were found by compiling the program and running the result, which is what <code>CC_CHECK=1</code> does in the dogfood and what <code>scripts/ctest.sh</code> does here. Neither is visible on the interpreter; neither is visible from reading the emitted C without a compiler. Both have twelve-line repros in the dogfood's PAIN.md, and regression tests here that name the wrong spelling as well as the right one._</p> <hr> <h2 id="v0-1-216-2026-08-12">v0.1.216 — 2026-08-12</h2> <p>_<code>bytes</code> gets its I/O boundary: <code>read_bytes</code>, <code>write_bytes</code>, <code>print_bytes</code>._</p> <p>_The <code>bytes</code> type has existed for a while — <code>bytes_len</code> / <code>get</code> / <code>slice</code> / <code>concat</code> / <code>of_hex</code> / <code>of_str</code> / <code>of_vec</code> and their inverses, across interp, C, LLVM and Wasm. What it had no way to do was <strong>leave the program</strong>. Reading, writing and printing a byte sequence all went through <code>str</code> or <code>Vec[int]</code>._</p> <p>_Through <code>str</code> does not work, and the <a href="https://github.com/284km/mpng">mpng</a> dogfood found out the hard way:_</p> <pre><code class="language-mere"> let _ = print_no_nl (chr 65); let _ = print_no_nl (chr 0); let _ = print_no_nl (chr 66); </code></pre> <p>_The interpreter writes <code>41 00 42</code>. Compiled with <code>-c</code>, the same program writes <code>41 42</code> — a <code>str</code> is a NUL-terminated C string there, so <code>"\0"</code> and <code>""</code> are the same value and nothing downstream can tell them apart. A PNG decoder writing a PPM produced a file seven bytes short of correct, and only on some backends._</p> <p><strong>The fix is not in printing.</strong> A <code>bytes</code> carries its length in every backend (<code>{ len; data[] }</code> in C and LLVM, an OCaml string in the interpreter), which is what makes these three correct where <code>print_no_nl</code> cannot be:</p> <table> <thead><tr></tr></thead> <tbody><tr><td><code>read_bytes : str -> bytes</code></td><td>interp + C</td></tr> <tr><td><code>write_bytes : str -> bytes -> unit</code></td><td>interp + C</td></tr> <tr><td><code>print_bytes : bytes -> unit</code></td><td>interp + C + LLVM</td></tr> </tbody></table> <p>_LLVM's writes through <code>write(1, …)</code> rather than <code>fwrite</code> to stdout: reaching <code>stdout</code> from IR means naming a symbol that differs between platforms (<code>__stdoutp</code>, <code>stdout</code>), and a file descriptor is the same everywhere — and unbuffered, which is what the name promises._</p> <p>_Wasm refuses <code>print_bytes</code> at compile time: its printing goes through a host import that takes a NUL-terminated pointer, so this needs a host-side change rather than a codegen one. Loud, not silent._</p> <p>_The dogfood now reads and writes through <code>bytes</code> end to end, which is the check that the design is the right one — 28 cases, on the interpreter and compiled, producing identical files._</p> <p>_Also noticed while testing, unrelated and unfixed: <code>exit 0</code> as a program's trailing expression breaks the LLVM backend (<code>unsupported LLVM codegen type element: 'a</code>, since <code>exit : int -> 'a</code>)._</p> <hr> <h2 id="v0-1-215-2026-08-12">v0.1.215 — 2026-08-12</h2> <p>_<code>mere -ll -g</code>: the LLVM backend, too. Every backend can now be debugged as Mere._</p> <pre><code class="language-sh"> mere -ll -g app.mere > app.ll && clang -g app.ll -o app lldb app -o "b twice" # Breakpoint 2: where = app`twice at app.mere:2:1 </code></pre> <p>The same destination as the C backend's <code>#line</code> — a DWARF line table naming the <code>.mere</code> — reached the most <strong>directly</strong> of the three, because LLVM IR carries debug information itself. There is nobody to divide the work with: a <code>DISubprogram</code> per function, a <code>DILocation</code>, and a <code>!dbg</code> on the instructions.</p> <p>On <em>every</em> instruction, which is the constraint that shapes this. A function with a subprogram whose calls have no location is something the verifier objects to, so the location is attached in <code>emit_instr</code> — the one choke point every instruction already goes through — rather than at chosen points. All of a function's instructions share one location, the line its body began on, which is the same granularity the C backend arrives at for an entirely different reason.</p> <p><code>Debug Info Version</code> in the module flags is not optional: without it the metadata is stripped as being from an older LLVM and the debugger shows nothing, with no error anywhere to explain why. It has a test of its own for that reason.</p> <p><strong>`sh scripts/debug_info.sh`</strong> compiles a program through both backends and asks <code>lldb</code> where each function is, because a breakpoint resolving to <code>app.mere:8</code> is evidence and emitted text looking right is not:</p> <pre><code> ok C both resolves to app.mere:8 ok LLVM both resolves to app.mere:8 </code></pre> <p>_That closes Q-021, and with it every backend: C <code>#line</code>, LLVM <code>!dbg</code>, Wasm a source map, RV32I its own debug map — each reaching the same place by whatever route its output allows. The interpreter needs none, being where the source already is._</p> <hr> <h2 id="v0-1-214-2026-08-12">v0.1.214 — 2026-08-12</h2> <p>_<code>mere -wg</code>, and a source map: the browser's debugger shows Mere source._</p> <pre><code class="language-sh"> mere -w app.mere > app.wat mere -wg app.mere > app.map.txt wat2wasm --enable-tail-call --debug-names app.wat -o app.wasm node scripts/wasm_sourcemap.js app.wasm app.map.txt app.mere </code></pre> <p>Writes <code>app.wasm.map</code> and appends a <code>sourceMappingURL</code> custom section, which is what Chrome and Firefox look for. The playground runs on this backend, so this is the debugger a Mere program in a browser has been missing.</p> <p><strong>The compiler cannot produce the map, and that is not a limitation but a fact about the format.</strong> A Wasm source map addresses <em>byte offsets in the assembled binary</em>; this backend emits text for <code>wat2wasm</code> to assemble. So the work splits the way the RV32I debug map splits: <code>mere -wg</code> says which function came from which line, the binary says where each function ended up (its name section), and a script joins them by name. Whoever knows the addresses is not whoever knows the source.</p> <p><strong>The check is the interesting part.</strong> A source map is easy to produce and hard to trust — the segments are VLQ deltas, so an error in one shifts every mapping after it, and the result still looks like a source map. So <code>scripts/wasm_sourcemap.sh</code> decodes the map back and compares it against <code>wasm-objdump</code>:</p> <pre><code> ok 0x001550 is <both>, and the map says line 8 ok 0x00155c is <thrice>, and the map says line 5 ok 0x001564 is <twice>, and the map says line 2 </code></pre> <p>_Prelude functions are absent from the table, by the rule v0.1.212 established: a position that names a file did not come from the source being compiled. And the binary still validates after the section is appended, which the check also confirms — appending to a Wasm file is only harmless when it is done right._</p> <hr> <h2 id="v0-1-213-2026-08-12">v0.1.213 — 2026-08-12</h2> <p>_Find references, and rename._</p> <p>_The same question — where else is <strong>this</strong> binding — and the difficulty in both is shadowing: two <code>x</code>es in one file may be two different things, and treating them as one is a rename that breaks the program._</p> <pre><code class="language-mere"> let x = 1; // renaming this one touches let f = fn (n: int) -> let x = n + 1 in // ... not this one, nor x + x; // ... these let _ = print_int (f x + x); // ... but these two </code></pre> <p>So the walk resolves <strong>every occurrence to the binding it refers to</strong>, and the answer is the occurrences that resolved to the same one — the reverse of what go-to-definition does, and the one shape <code>Query</code> was missing. Binder positions are included, so the cursor may be on the definition rather than on a use.</p> <p><strong>Rename refuses what the file does not own.</strong> A prelude name or a builtin has its definition somewhere the edit cannot reach, and renaming the uses while leaving the definition is worse than refusing. The refusal is returned from <code>prepareRename</code>, which is where an editor asks before offering a box to type in — so it arrives as a message rather than as a broken file.</p> <p>_That is the LSP's list done: diagnostics, hover, definition, completion, outline, formatting, semantic tokens, references, rename. What is left is deliberate — incremental sync (nothing to gain yet), and the twenty typer <code>raise</code> sites whose worst case is one error per declaration rather than all of them._</p> <hr> <h2 id="v0-1-212-2026-08-12">v0.1.212 — 2026-08-12</h2> <p>_<code>mere -c -g</code>: a debugger on the compiled program shows the Mere source._</p> <pre><code class="language-sh"> mere -c -g app.mere > app.c && clang -g app.c -o app lldb app -o "b mu_twice" # Breakpoint 1: where = app`mu_twice + 8 at app.mere:2:40 </code></pre> <p>_Verified with <code>lldb</code> and <code>dwarfdump</code>, not by reading the emitted text: the line table names <code>app.mere</code>, and a breakpoint on a function resolves to the line it was written on._</p> <p><strong>This was reported as "not mechanical after all" in the notes for v0.1.202</strong>, and the reason given was that <code>codegen_c</code> is expression-oriented and does not know which output line it is on, so <code>#line</code> — which applies to the <em>next</em> line — cannot be placed. That turned out to be looking at the wrong thing. A function's whole body is emitted as <strong>one C line</strong>, so a directive per function is not a coarse approximation but the finest granularity the output has; put it inside the braces and the body lands on exactly the line the programmer wrote. No line-tracking writer, no second pass.</p> <p>The other half of the problem is what to say about the code that has no Mere source — the runtime, and the prelude. Each user function is followed by a directive naming a file that does not exist (<code><mere runtime></code>), so a debugger shows <em>no</em> source for those frames, which is the truth, rather than an arbitrary line of the user's file.</p> <p><strong>The rule for "is this the user's code" is one line</strong>, and it is the one v0.1.210 made possible: <em>a position that names a file did not come from the source being compiled.</em> Imports were already stamped; the prelude is now tokenised as <code><prelude></code>, so neither can be claimed. An earlier attempt counted prelude declarations instead and was wrong — <code>Trait_elab</code> reorders the list, so the prelude's are not the first N by the time codegen sees them.</p> <p>_Off by default: without <code>-g</code> the emitted C is byte-identical to what it always was, which the suite checks. LLVM (<code>!dbg</code>) and Wasm (source maps) remain unanswered; the RV32I backend has had its own since v0.1.200._</p> <hr> <h2 id="v0-1-211-2026-08-12">v0.1.211 — 2026-08-12</h2> <p>_Formatting, an outline, and colour that is not guessing._</p> <p><strong>`textDocument/formatting`</strong> runs the function <code>mere fmt</code> runs. That is the whole point of it living in <code>Pipeline</code> rather than in the CLI: format-on-save and the command line cannot come to different conclusions about what formatted means. It declines twice, deliberately — a file that does not parse is left alone, because replacing a buffer with the best guess of a parser that failed is how somebody loses work, and an already-formatted file produces no edit rather than an edit that changes nothing. It also re-adds the trailing newline the CLI's <code>print_endline</code> supplies, without which format-on-save would strip it from every file, every time.</p> <p><strong>`textDocument/documentSymbol`</strong> lists the file's value declarations for the outline, telling a function from a value by its type. <code>type</code> declarations are absent and honestly so: <code>Top_type</code> carries a name and its variants and no position, so it cannot be pointed at without guessing.</p> <p><strong>`textDocument/semanticTokens/full`</strong> is the compiler saying which names are parameters, which are functions, which are constructors. Syntax highlighting is normally regular expressions guessing at a language; this one does not have to guess. The editor's grammar keeps what it is good at — keywords, strings, numbers — and the distinction it <em>cannot</em> make, a parameter from a global, comes from the tree.</p> <p>_The encoding is five integers per token and every one is relative to the token before it, which is compact and unforgiving: wrong deltas paint the file at an offset. The test decodes the stream back into positions and names rather than asserting on the numbers._</p> <p>_The VS Code extension needed no change for any of this — it asks the server what it can do during <code>initialize</code>, so three new capabilities simply started working. That is the argument for keeping the two apart, arriving on schedule._</p> <hr> <h2 id="v0-1-210-2026-08-12">v0.1.210 — 2026-08-12</h2> <p>_Positions know which file they came from, and the typer reports more than one problem per declaration._</p> <p><strong>A position carries its file.</strong> <code>Loc.t</code> gained <code>file : string option</code>, and since the lexer is the only thing in the compiler that builds a position, stamping the tokens of an <code>import</code>ed file was a one-line change that everything downstream inherits: an error raised deep in the typer, about a node that came from another file, now knows which file it is about without anybody having threaded that through. The CLI renders the snippet from that file; the language server publishes against that file's URI. Before this, only <em>syntax</em> errors could say where they came from — by the time the typer runs, imported declarations have been merged into one program.</p> <p><strong>The typer collects.</strong> The two sites that account for nearly every real type error — a mismatch in <code>unify</code>, and an unknown name — report and carry on instead of raising, when a sink is installed. So one declaration can report four problems rather than the first one.</p> <p>What it carries on <em>with</em> is the interesting choice: a <strong>fresh type variable</strong>, which unifies with anything, so it neither invents a second error nor silences a real one further along. A distinguished error type would be the textbook answer and would have to be taught to every match on <code>ty</code> in five backends. On a mismatch the two types are left unlinked, since neither is more right than the other and forcing one on the other is how one mistake becomes five.</p> <p>The other twenty <code>raise</code> sites are unchanged: they end that declaration's check, and v0.1.209's recovery picks up at the next one. The compiler's path is untouched — no sink, same first-error-raises behaviour — and the sink is installed under <code>Fun.protect</code>, because one left behind would make the compiler collect errors instead of stopping. There is a cap of a hundred, because a pathological file can produce errors without end once inference is allowed past them and nobody is reading the hundred and first.</p> <hr> <h2 id="v0-1-209-2026-08-12">v0.1.209 — 2026-08-12</h2> <p>_More than one type error at a time._</p> <p>_A file with three broken functions reported one of them: fix, recheck, learn about the next. The check now recovers at <strong>declaration</strong> boundaries — the same boundary the parser recovers at — so it reports one error per broken declaration, in the editor and in the terminal._</p> <pre><code> type error: expected `int`, got `str` --> app.mere:1:28 type error: expected `int`, got `str` --> app.mere:2:24 2 errors </code></pre> <p><strong>A declaration that failed still binds its names</strong>, to a fresh type variable that unifies with anything. Otherwise every later use of the name is a second error about the same mistake and the real ones are buried — the test for that uses a broken function twice and expects exactly one error.</p> <p><strong>The compiler's path is unchanged.</strong> Recovery is opt-in (<code>infer_program ?on_error</code>): without it the first error is raised exactly as before, because a compiler that carries on past a type error has nothing useful to emit. One code path, two behaviours, rather than a second implementation to keep in step.</p> <p>_The typer still stops at the first problem <strong>within</strong> a declaration. Making that collect means teaching every one of its 22 <code>raise</code> sites to produce a value and carry on — a different and much larger change, and one that needs an error type that unifies silently, or every recovery invents cascades of its own._</p> <p>_One wrinkle worth recording: the pass over the desugared program re-visits every declaration's body, so it re-raises the error the declaration loop already reported. Diagnostics are de-duplicated, which is what makes that harmless — and is the same fix the duplicated exhaustiveness warnings needed in v0.1.208._</p> <hr> <h2 id="v0-1-208-2026-08-12">v0.1.208 — 2026-08-12</h2> <p>_Diagnostics become data: which file a position belongs to, and warnings too._</p> <p><strong>A syntax error inside an `import` is reported against the file it is in.</strong> Its line numbers describe <em>that</em> file, so reporting it against the importing one was underlining an innocent line. <code>Parse_error_in_file</code> carries the path from the import that raised it, the CLI renders the snippet from that file, and the language server publishes against that file's URI — remembering which other files it has spoken about so it can clear them when the import is fixed. A diagnostic stays on an editor's screen until the server says otherwise, and "never mind" is exactly the message nobody thinks to send.</p> <p><strong>Warnings are diagnostics now</strong> (severity 2 in the protocol): a non-exhaustive <code>match</code>, a top-level name that collides with a C keyword. They were printed to stderr from inside the pipeline, which is fine for a terminal and useless to anything else — an editor cannot underline a line written to a stream it is not reading. The pipeline collects them; the CLI prints them, which is where the decision about how a warning looks belongs.</p> <p>Two things that fixing this exposed. The non-exhaustive-match warnings arrived <strong>twice</strong>, because type inference visits a declaration's body once as a declaration and again as part of the desugared program — de-duplicated now. And the messages carried their own <code>line L, col C:</code> prefix, which read as <code>warning: line 3, col 29: warning: …</code> once a caller with the position added its own; the position is data and the text no longer repeats it.</p> <p>_Still not carried: <strong>type</strong> errors from an imported file. By the time the typer runs, the imported declarations have been merged into one program and nothing records which file each came from._</p> <hr> <h2 id="v0-1-207-2026-08-12">v0.1.207 — 2026-08-12</h2> <p>_Completion — the third of the three questions that are really one question._</p> <p>_Fifth slice of the language-server arc, and the one that needed no new machinery: <code>Query.scope_at</code> already knew what is visible at a position, so this is that list, de-duplicated by name and dressed for the protocol._</p> <p>Every name visible at the cursor, innermost first, one entry per name — an inner binding shadows an outer one, and offering both would offer a name that cannot be reached. Each carries its inferred type as the <code>detail</code> line and a kind, so an editor draws a function icon for a function.</p> <p>Two judgements about what <em>not</em> to offer: the prelude's internal helpers (the ones it names with a leading underscore) are left out, and <code>_</code> is not a name anybody wants back. Prelude names themselves are offered — <code>str_len</code> is exactly what you want in the list — with <code>sortText</code> putting them after the file's own names.</p> <p>_That completes hover / definition / completion. All three are <code>Query.node_at</code> and <code>Query.scope_at</code> with a different answer attached, which is what moving the check into the library bought: the editor's three questions turned out to be one question the compiler could already answer._</p> <hr> <h2 id="v0-1-206-2026-08-12">v0.1.206 — 2026-08-12</h2> <p>_Go to definition._</p> <p>_Fourth slice of the language-server arc, and the first that needs <strong>scope</strong>: not just what is under the cursor, but what is bound there and where each name came from._</p> <p><strong>Scope is recomputed, not indexed.</strong> <code>Query.scope_at</code> walks down to the position and collects the binders on the way. The walk descends one path rather than the whole tree, it cannot go stale, and there is no invalidation to get wrong — the same reason hover reads the typer's annotations instead of building a table beside them.</p> <p>A binder covers the parts of itself where it is really visible: a <code>let</code> binds its body but not its own value expression, a <code>fn</code> binds its body, a <code>let rec</code> binds both, a match arm's pattern binds that arm. Each of those is a test, because getting one wrong is how a server sends you to the wrong <code>x</code>.</p> <p><strong>Two answers it declines to give</strong>, both because the honest answer is nothing:</p> <ul> <li>A <strong>prelude name</strong> (<code>print_int</code>) is genuinely in scope, but its position is a</li> </ul> <p> line in the prelude's own text — jumping there would send the editor to an arbitrary line of the user's file. Prelude bindings are therefore <em>marked</em> rather than dropped (completion will want them), which needed the pipeline to record how many declarations the prelude contributed.</p> <ul> <li>A <strong>parameter</strong> resolves to the <code>fn</code> that introduced it rather than to the</li> </ul> <p> parameter name, since <code>Fun</code> carries the name but not the name's own position.</p> <hr> <h2 id="v0-1-205-2026-08-12">v0.1.205 — 2026-08-12</h2> <p>_Hover: the type inference gave whatever is under the cursor._</p> <p>_Third slice of the language-server arc. Point at a name and the editor shows <code>twice : (int -> int)</code>; point at a literal and it shows <code>int</code>._</p> <p><strong>There is no second inference pass and no index.</strong> The typer already writes the type it found onto every node it visits (<code>e.ty <- Some t</code>), so the check that produced the diagnostics leaves behind a tree that knows the answer. <code>Pipeline.check</code> now hands that tree back instead of dropping it, and the server keeps it per open document.</p> <p><strong>What "the node at this position" means here</strong>, since it is not obvious: a <code>Loc.t</code> in this compiler is a line, a column and a <strong>width</strong> — the token a node was built from, not a span over its subtree. So the node at a position is the <em>narrowest</em> node whose own token contains the cursor. That is what makes hovering inside a call answer about the piece under the cursor rather than about the whole application. <code>Query.node_at</code> is that search, and <code>Ast.children</code> is the one generic child walk it needed — written once, because every position question (this one, go-to-definition, completion) needs it and three hand-written 26-case matches would drift apart.</p> <p><strong>The last tree that type-checked is kept.</strong> While a line is half typed the file does not check, and an answer from a moment ago beats no answer at all — so hover keeps working through an edit and catches up when the file is valid again. The test for this edits a good file into a broken one and asserts the tree survived.</p> <hr> <h2 id="v0-1-204-2026-08-12">v0.1.204 — 2026-08-12</h2> <p>_<code>mere lsp</code> — a language server. Diagnostics in the editor, from the check the compiler runs._</p> <p>_The second slice of the language-server arc (the first was recovering from syntax errors, so there is more than one to show). What it does is diagnostics: every syntax error in the buffer, republished on each keystroke, and the first type error once the file parses. Hover and completion want a position resolved against a typed tree, which is the next slice._</p> <pre><code class="language-sh"> mere lsp # LSP over stdin/stdout; see docs/lsp.md for editor setup </code></pre> <p><strong>The check is the compiler's check.</strong> <code>infer_program</code> — parse, elaborate, type, plus the borrow/move/Send analyses — moved out of the CLI into <code>Pipeline</code>, where the server calls the same function the four backends start from. A language server that agrees with the compiler on good days is worse than none: it teaches you to distrust the underline. <code>Pipeline.diagnostics</code> is the one entry point that answers "what is wrong with this text", as data rather than as an exception.</p> <p><strong>Everything the server decides is a function.</strong> <code>Lsp.handle : state -> message -> state * message list * bool</code> — so the protocol is tested in the suite without a socket, a subprocess or an editor, and the only untested part is three lines of IO in <code>Lsp.serve</code>. <code>scripts/lsp_smoke.sh</code> covers the process end to end by piping a canned editor session through the real wire format.</p> <p><strong>Also new: `Json`</strong> — a JSON value, parser and writer (~230 lines), because this project has two dependencies and reading a protocol this small is not worth a third. It decodes <code>\uXXXX</code> into UTF-8 including surrogate pairs, and prints integers without a decimal point, since an editor reading <code>"line": 3.0</code> strictly is entitled to object.</p> <p>_Known gaps, all written down in <code>docs/lsp.md</code>: one type error at a time (the typer still raises on the first), positions inside imported files are reported against the importing file, and sync is full-text rather than incremental._</p> <hr> <h2 id="v0-1-203-2026-08-12">v0.1.203 — 2026-08-12</h2> <p>_The parser no longer stops at the first syntax error._</p> <p>_A file with three broken functions told you about one of them, three times in a row: fix, recompile, learn about the next one. <code>mere <file></code> now reports all of them, in source order, with a count at the end._</p> <pre><code> parse error: expected literal, identifier, or '(' --> app.mere:3:20 parse error: expected type --> app.mere:7:16 parse error: expected 'ident = expr' after 'with' --> app.mere:12:15 3 syntax errors </code></pre> <p>_This is the first slice of a language-server arc, and it is the one that pays off on its own: an editor cannot underline three mistakes if the compiler only knows about one, and neither can a person._</p> <p><strong>How.</strong> <code>Parser.parse_program_recover</code> parses, and on an error <strong>deletes the declaration that contains it</strong> and parses the whole file again, collecting errors until it succeeds (or hits 20). Re-parsing rather than resuming is deliberate: the parser is functional over an immutable token list, so there is no cursor to reset and no half-built state to unwind — deleting a span and starting over is exact, and it needs no changes to the 130-odd places that raise. It costs one pass per error, which for an editor re-parsing on every keystroke is not the expensive part. <code>parse_program</code> itself is untouched, so nothing on the good path changed.</p> <p><strong>Where a declaration ends</strong> is the interesting part. <code>;</code> at bracket depth zero is the language's real boundary — but a declaration with an unbalanced <code>(</code> never returns to depth zero, so a depth-only rule deletes the rest of the file and hides every later error, which is the exact failure being fixed. So a <strong>declaration keyword in column 1</strong> is accepted as a boundary too: every top-level declaration in this language's sources starts flush left (the formatter emits nothing else), so an indented <code>let</code> is a local binding and one in column 1 is a new declaration. It is a heuristic, and it is consulted only about where to resume after an error.</p> <p>_Errors from an <strong>imported</strong> file are not recovered from — their positions belong to another file's token list, so there is nothing in this one to delete. The first is reported and the walk stops._</p> <hr> <h2 id="v0-1-202-2026-08-12">v0.1.202 — 2026-08-12</h2> <p>_Two loose ends from the bare-metal work: diagnostics that report the line you wrote, and the differential test the QEMU port was aiming at._</p> <p><strong>The line you wrote.</strong> The <code>-rv</code> path compiles a <em>concatenation</em> — a Mere-source runtime prelude, then the user's file — so every position it produced was counted from the top of that text, and a type error in a three-line file was reported at "line 133", against a snippet from an unrelated line or none at all. The debug map already subtracted the prelude; the diagnostics did not.</p> <p>One function now answers "where is this really?" for both, so they cannot drift apart. A position that lands <em>inside</em> the prelude is deliberately <strong>not</strong> remapped into the user's file — there is no honest line there to point at — and is shown against the prelude's own text under the name <code><rv-prelude></code>, which also makes a prelude bug legible as one.</p> <p><strong>Two independent machines, same bytes.</strong> v0.1.201 booted a bare program and a trap handler on QEMU's <code>virt</code> board. Two additions finish the thought:</p> <ul> <li><code>examples/riscv_virt_sched.mere</code> — the <strong>context switch</strong> on virt. This is the</li> </ul> <p> case most worth an outsider's opinion: the trampoline saves 31 registers to a known place and the emulator restores them, so if the two agreed on a wrong order, order-dependent corruption would be invisible to every test we own. It also checks the rule that was hardest to arrive at (<code>gp</code> switches with the rest, because each task has a heap of its own) against a machine with no stake in it.</p> <ul> <li><code>scripts/qemu_virt.sh</code> now takes <code>MEMU=<memu checkout></code> and runs each image on</li> </ul> <p> <strong>both</strong> machines — QEMU and the Mere-written emulator — diffing the two. All three examples are byte-identical on both.</p> <p>For that diff to mean anything the output has to be a function of the program rather than of the clock, so the scheduler prints one letter per <strong>switch</strong> rather than one per N iterations: virt gives each task 20ms of real time, our emulator counts instructions, and both print <code>ABABABA</code>.</p> <p>_The emulator side of this is in the memu project: <code>./rvrun 8 virt</code> places RAM at 2GB and moves the CLINT to virt's addresses. What had to change there was the decode <em>order</em> — it asked "is this a device?" first, which is only right while the devices are above RAM._</p> <p>_Still not on virt: the shell (its input would have to be piped in to be diffable) and the user-process pair (a second image needs <code>-device loader</code> rather than <code>-kernel</code>)._</p> <hr> <h2 id="v0-1-201-2026-08-11">v0.1.201 — 2026-08-11</h2> <p>_The backend's output boots on a machine nobody here wrote: QEMU's <code>virt</code> board._</p> <p>_Every layer of the bare-metal work is self-written — the compiler, the fifth backend, the kernel, and the RV32I emulator it runs on. So when something misbehaves, "is the binary wrong or is the emulator wrong?" has no answer inside the stack; agreeing with yourself is not evidence. QEMU is an independent implementation of the same specification, which is what the Klaus and Blargg suites are for the 6502 and Game Boy emulators in the sibling project._</p> <pre><code class="language-sh"> mere -rv --bare --load-base 0x80000000 --ram 8 examples/riscv_virt_hello.mere > virt.bin qemu-system-riscv32 -M virt -bios none -nographic -kernel virt.bin </code></pre> <p>_Two programs boot: <code>riscv_virt_hello.mere</code> (UART, a run-time-allocated string, recursion, the CLINT read back) and <code>riscv_virt_timer.mere</code> (a registered Mere closure servicing a real timer interrupt). <code>sh scripts/qemu_virt.sh</code> builds both, runs them and diffs the output; it skips cleanly when QEMU is absent, so this is an optional check rather than a dependency._</p> <p>_What QEMU checks that our own emulator cannot: instruction encodings against a decoder nobody here wrote, the layout <code>_start</code> builds at a load base above 2GB, the 16550 protocol against a real device model, and — the one most worth an outside opinion — the trap contract: <code>mtvec</code>, <code>mstatus.MIE</code>, <code>mie.MTIE</code>, the CLINT's compare register, and the PC a handler returns for <code>mepc</code>._</p> <p>_The <strong>one</strong> thing that had to change in codegen: the machine window's length is now a <code>max</code> rather than a sum. It was <code>mmio_base + mmio_len</code>, which is right only while the devices are above RAM — the arrangement the default base 0 forces. virt inverts it: DRAM at <code>0x80000000</code> with every device beneath it, so a program handed <code>[0, 0x10010000)</code> could not name its own RAM. The bounds checks were already unsigned, so a length past 2GB is not a negative number to them._</p> <p>_Also: the <code>-rv</code> family's flags (<code>--bare</code>, <code>--ram</code>, <code>--load-base</code>) are now parsed in any order rather than matched as literal argument lists, which is why the combination this needed — all three at once — did not exist before. That removes eight arms whose only distinction was which combinations somebody had happened to want._</p> <p>_Not yet on virt: the scheduler, shell and user-process examples (they name our CLINT addresses; the shell wants a receive side; a second image needs <code>-device loader</code>), and running a virt image on our own emulator, which would make the same bytes runnable on both. Both are address swaps rather than redesigns — see <code>docs/bare-metal.md</code>._</p> <hr> <h2 id="v0-1-200-2026-08-11">v0.1.200 — 2026-08-11</h2> <p>_<code>mere -rvg</code>: a debug map, so a program compiled to machine code can be debugged at the source lines it was written as._</p> <p>_Nothing in any backend emitted debug information — no DWARF, no source maps, no line directives — so "which line is this?" was unanswerable everywhere. On the RV32I backend that showed up as a working method: every hard bug in the bare-metal arc was found by instrumenting the emulator <strong>by hand</strong>, a ring buffer of program counters here, a store watchpoint on a save area there, a register dump at trap entry. Ten instruments, written and thrown away, and once by patching codegen to print two registers from inside <code>__oom</code>._</p> <p>_The map is a text sidecar, because the binary has no header to hold anything — this backend emits code and nothing else. One record per line, addresses ascending:_</p> <pre><code> S <addr> <name> every label F <addr> <name> fsz= ra= fp= params= line= a function and its frame L <addr> <line> <col> the statement starting here </code></pre> <p>_Two properties it was worth designing for. It is emitted from <strong>the same item list the assembler consumes</strong>, via a zero-width <code>Meta</code> item that the assembler and the listing both ignore — so <code>-rv</code> and <code>-rvg</code> agree by construction, there is no separate debug build, and the map describes the bytes that actually ran. And the line numbers are the ones <strong>the programmer wrote</strong>: source positions arrive counted from the top of the prelude-plus-source text the driver builds, and the map subtracts the prelude, so an address whose line lands inside the prelude gets no record — the honest answer for code nobody wrote. (That offset is the same one that makes <code>-rv</code> diagnostics report line 133 for a three-line file; fixing the diagnostics is a separate change.)_</p> <p>_Frame layout is uniform on this backend, so <code>fsz</code> / <code>ra</code> / <code>fp</code> describe it completely and a backtrace is two loads per frame with no guessing._</p> <p>_The reader lives in the memu project as <code>riscv-dbg</code>: breakpoints on source lines, a backtrace, and <strong>reverse stepping</strong> through an undo log — one fixed-size record per instruction, so going back applies the inverse rather than replaying from a snapshot. It is exact, and tested as such (N instructions forward and N back restore every register and a checksum of the heap), and it crosses traps: <code>S</code> walks out of an interrupt handler and onto the line the timer interrupted. Which is the shape of the thing this arc kept needing and building by hand._</p> <p>_Three tests on the map. <code>dune runtest</code> 2339/0, ctest 13/13._</p> <hr> <h2 id="v0-1-199-2026-08-11">v0.1.199 — 2026-08-11</h2> <p>_Documentation for the bare-metal work, which existed only as twelve changelog entries and seven example headers._</p> <p>_The arc built a fifth backend, an operating system on it and a user process on that, and none of it was discoverable: the README did not mention RV32I at all, <code>codegen.md</code> documented three backends, and the nine new examples were missing from the category index. Someone arriving at the repository could not find the tower, let alone the rules for using it._</p> <p>_<a href="bare-metal.html">docs/bare-metal.md</a> is now the one place: flags, the memory map, raw memory as a window capability and the three ways out that are closed, CSRs and why they are deliberately <strong>not</strong> a capability, the trap trampoline and its two non-obvious properties, tasks, and <strong>when to switch `gp`</strong> — the rule that took the arc's hardest bug to find. It ends with what is deferred on purpose (the fantasy console's ambient framebuffer, a QEMU boot for external verification, nested traps, and the absence of an MMU) so the gaps are recorded rather than implied._</p> <p>_Also stated plainly there, because it would otherwise be easy to overclaim: what isolates the user process is <strong>the type system, not the hardware</strong>. Everything runs in machine mode; the process is contained because without <code>--bare</code> it cannot obtain a <code>Raw</code> at all, not because an MMU would stop it._</p> <p>_The examples index gains a section in the same shape as the browser apps — each example beside the thing it forced — and a broken link found on the way (<code>contrib/json/writer.mere</code>, merged into <code>json.mere</code> in 31b4c45) is fixed where this file referenced it._</p> <hr> <h2 id="v0-1-198-2026-08-11">v0.1.198 — 2026-08-11</h2> <p>_The allocating-handler corruption, solved. The mechanism was none of the three suspects — it was <code>region</code>, and the "fixed" shell had been quietly broken all along._</p> <p>_The tell was in the emulator's register log: task0's <code>gp</code> moved <strong>backwards</strong> while it ran — the shell's per-command <code>region R { ... }</code> rollback. The rest follows. The region parked the bump pointer, a timer switch let the background task allocate its loop closure above the mark, and the rollback freed it — live — for the next command to overwrite. The task then resumed with <code>a0</code> pointing into reused memory and jumped through whatever now sat at its closure's first word._</p> <p>_<strong>Sharing the bump pointer between tasks was the bug.</strong> v0.1.192's rule ("gp is machine state; never switch it") missed the other direction: with a shared heap, anything that rolls the pointer back frees what the other context allocated meanwhile. The rule that survives contact is the user-process one, applied inside a single program: <strong>contexts share `gp` only if they genuinely share a heap, and a context that uses regions must not.</strong> The scheduler and shell examples now give each task an arena carved from <code>machine_scratch</code> — heap up from the bottom, stack down from the top, so the out-of-memory check guards each task for free — and switch every register. <code>raw_len</code> (the partner of <code>raw_base</code>) went in so a kernel can partition a window it was handed without hardcoding the runtime's geometry._</p> <p>_<strong>And v0.1.193's fix had fixed nothing.</strong> Making the handler allocation-free moved the corruption out of sight, not out of existence: in the shipped shell the background task's counter froze a few commands in and never advanced again — the task was dead, resuming into reused memory every slice, and the fault-stepping handler swallowed the evidence. The falsification test was the counter, probed between heavy commands: 864, 864, 864. With per-task heaps it climbs monotonically, <strong>and the original repro passes with the handler allocating</strong> — the rule "a trap handler must not allocate" is back to being good practice rather than load-bearing._</p> <p>_Two adjacent holes found on the way, both real:_</p> <ul> <li>_The dedicated trap stack (v0.1.197) sat <strong>below</strong> <code>machine_scratch</code>, so a</li> </ul> <p> handler allocating while an arena task was interrupted compared a high <code>gp</code> against a low <code>sp</code> and declared the heap exhausted, spuriously. The stack now sits above the arenas, where the same check instead <strong>protects</strong> it: an arena that grows into the trap stack is refused._</p> <ul> <li>_The runtime's abort paths (<code>__oom</code>, <code>__raw_fault</code>, <code>__pat_fail</code>) report and</li> </ul> <p> exit via <code>ecall</code> — which, with a kernel installed, vectored to the program's own handler; a handler that steps over faults swallowed both ecalls and execution fell off the end of the helper into whatever was emitted next. They now take the machine back (<code>csrrw x0, mtvec, x0</code>) before reporting: a dying runtime owes the program nothing, but it owes the person at the terminal a message._</p> <p>_All six bare-metal examples verified, the background counter climbing, the self-hosted compiler still byte-identical under its kernel. <code>dune runtest</code> 2336/0, ctest 13/13._</p> <hr> <h2 id="v0-1-197-2026-08-11">v0.1.197 — 2026-08-11</h2> <p>_A third hypothesis for the allocating-handler corruption, also disproved. The change it prompted is worth keeping anyway: the trap handler gets a stack of its own._</p> <p>_Until now the handler ran on whichever task's stack it interrupted. That is a design smell independent of any bug — it makes the handler's frame size a constraint on every task's stack, and it means a task with a nearly-full stack turns any trap into a memory-corrupting event. The trampoline now switches <code>sp</code> to a dedicated 8KB stack in the reserved region once every register is safely saved, which is what a kernel does and for these reasons. <code>machine_scratch</code> starts above it, so task stacks are unaffected except for being 8KB smaller._</p> <p>_It does not fix the corruption. Three mechanisms are now ruled out — the header-before-bump window (v0.1.193), trampoline reentrancy (v0.1.195), and the handler's stack placement (here) — against a signature that is precise:_</p> <ul> <li>_a task resumes with <code>a0</code> holding a pointer to the <strong>trap save area's window</strong></li> </ul> <p> block<strong> — a two-word `Raw` value that only the handler ever constructs;_</strong></p> <ul> <li>_the next closure tail call reads word 0 of it as a code pointer, which is the</li> </ul> <p> save area's base, and jumps there;_</p> <ul> <li>_from then on the machine executes its own saved registers as instructions._</li> </ul> <p>_Every write to that <code>a0</code> slot comes from the trampoline's own save instruction, so the value was in <code>a0</code> at trap entry, meaning the interrupted code held it — and the only code that holds it is the handler. Which would be reentrancy, which the depth check says is not happening. One of those two statements is wrong and finding out which is the next probe: log the first forty writes to the slot rather than the last, and see the value's first appearance instead of its aftermath._</p> <p>_Recorded rather than guessed at. The rule stands and every example keeps it: a trap handler must not allocate. All six bare-metal examples verified (uart, timer, sched, shell, user, selfhost — the last still byte-identical to the interpreter), <code>dune runtest</code> 2336/0, ctest 13/13._</p> <hr> <h2 id="v0-1-196-2026-08-11">v0.1.196 — 2026-08-11</h2> <p>_The self-hosted Mere compiler, running as a user process on a Mere kernel, on a CPU written in Mere._</p> <pre><code> kernel: running the self-hosted Mere compiler as a user process (module ... 5,224 more lines of WAT ... kernel: user process exited after 3 syscalls and 3727 ticks </code></pre> <p>_The WAT is byte-identical to what the native interpreter emits for the same input. It reached the UART through kernel write syscalls, from a process the timer preempted 3,727 times along the way._</p> <p>_Nothing new was needed. The compiler image is <a href="../examples/riscv_user_selfhost.mere">examples/riscv_user_selfhost.mere</a> — the contrib self-hosted compiler asked to compile <code>let x = 10 in x * x + 1</code> and print the result, with no idea it is a user process. The kernel is <a href="../examples/riscv_bare_selfhost.mere">examples/riscv_bare_selfhost.mere</a>, which is v0.1.194's kernel with a bigger tenant: 24MB for the image, because the compiler's heap peaks between 14 and 18MB and this backend's allocator never frees. That measurement, made back in v0.1.186, is why <code>--ram</code> exists._</p> <p>_The tower, bottom to top: a language; a backend of its own that emits RV32IM; a CPU written in that language to run it; a kernel written in it too, with traps, a timer, a scheduler and a syscall boundary; and the language's own compiler running as a process on that kernel._</p> <p>_v0.1.147 reached the third floor of that and called it the north star. This is the fifth._</p> <hr> <h2 id="v0-1-195-2026-08-11">v0.1.195 — 2026-08-11</h2> <p>_Chasing the allocating-handler corruption from v0.1.193. Two hypotheses tested and disproved, one narrowed to a single instruction, and a permanent diagnostic for the class._</p> <p>_The repro is deterministic: the shell with its register-copy loops moved back <strong>inside</strong> the handler (where they are closures, and a closure is an allocation), driven by a 33-command session. It stops at exactly the same byte every time._</p> <p>_Working backwards with the emulator, which is where this backend's debugging lives:_</p> <ul> <li>_the guest ends up executing the <strong>trap save area</strong> as if it were code — <code>mcause</code></li> </ul> <p> 2 (illegal instruction), the PC marching forward four bytes per trap because the fault path returns <code>mepc + 4</code>;_</p> <ul> <li>_it got there from <code>jalr zero, 0(t1)</code> — the tail call this backend emits for a</li> </ul> <p> closure — with <strong>`t1` holding the save area's base address</strong>;_</p> <ul> <li>_<code>t1</code> was loaded two instructions earlier by <code>lw t1, 0(a0)</code>, the code-pointer</li> </ul> <p> fetch. So <code>a0</code> was pointing at the two-word block a <code>Raw</code> window is, not at a closure: word 0 of that block is the window's base, and the window in question is the save area._</p> <p>_A pointer to the save-area window is a value only the <strong>handler</strong> ever holds. So some register belonging to the handler ends up restored into a task. That reads like a reentrancy failure — the save area is one global buffer, so a trap taken while the handler runs would overwrite the interrupted context with the handler's own. The trampoline now <strong>counts trap depth</strong> and refuses to nest, printing what happened and stopping, because with one save area there is nothing left to resume. It is placed after the register-save loop, since checking any earlier would clobber a register before saving it — which is the exact bug it exists to catch._</p> <p>_<strong>And it does not fire on the repro.</strong> So the corruption is not a nested trap either. That is worth as much as a positive result: two mechanisms are now ruled out (the header-before-bump window, fixed in v0.1.193 and not the cause; and reentrancy, ruled out here), and the failure is pinned to one instruction with a known-wrong register. What remains unexplained is how a handler-local pointer reaches a task's register file at all._</p> <p>_The rule stands and is now enforced by construction in every example: <strong>a trap handler must not allocate.</strong> The nested-trap check stays regardless — it turns a whole class of silent corruption into a sentence, which is this project's usual trade._</p> <p>_All five bare-metal examples verified after the trampoline change (uart, timer, sched, shell, user), <code>dune runtest</code> 2336/0, ctest 13/13._</p> <hr> <h2 id="v0-1-194-2026-08-11">v0.1.194 — 2026-08-11</h2> <p>_A user process. A separately compiled, ordinary Mere program running under a Mere kernel, printing through kernel syscalls, preempted by the timer, and unaware that any of that is happening._</p> <pre><code> kernel: starting a user process at 8MB user: hello from a user process user: I do not know a kernel exists user: fib 20 = 6765 user: exiting kernel: user process exited after 9 syscalls and 21 ticks </code></pre> <p>_The user program in <a href="../examples/riscv_user_prog.mere">examples/riscv_user_prog.mere</a> is not <code>--bare</code>, holds no capability, names no device and touches no CSR. It calls <code>print</code>. That lowers to the same <code>ecall</code> every hosted Mere program on this emulator has always used — what changed is <strong>who answers</strong>: with mtvec set, an environment call traps (cause 11) and the kernel in <a href="../examples/riscv_bare_user.mere">examples/riscv_bare_user.mere</a> reads fd, buffer and length out of the register save area and writes the bytes to the UART. Neither the emulator nor the user program is in that conversation. That is the whole idea of a syscall boundary, and it is why the user program needs no cooperation: it asks the machine, and the machine now has a kernel._</p> <p>_<strong>`--load-base <addr>`</strong> is what makes two programs fit in one address space. Everything PC-relative in the emitted binary never cared where it lived; the absolute parts did, and they all went through three places — the globals region, the stack top, and the assembler's resolution of <code>la</code> for string literals and lambda entries. They now shift together. The kernel says where its user process goes; the process does not know._</p> <p>_One thing this got wrong first, and the mistake is the interesting part. v0.1.192 found that <code>gp</code> — the heap's bump pointer — must <strong>not</strong> be switched between tasks, because two tasks in one program share a heap and switching it makes them allocate over each other. Carrying that rule over to a user process broke immediately: the kernel resumed with the user's <code>gp</code>, its next allocation landed at the user's heap top, and the out-of-memory check compared that against the kernel's much lower stack and correctly declared the heap exhausted._</p> <p>_So the rule is not about tasks at all. <strong>Switch `gp` exactly when the two contexts do not share a heap.</strong> Two tasks in one program share one; two programs built with different <code>--load-base</code> do not. Both examples now say so, and say why._</p> <p>_On the emulator side (memu): <code>ecall</code> traps to <code>mtvec</code> when a kernel is installed and falls back to the host ABI when none is, and a second image <code>user.bin</code> loads at 8MB if the file is there — a bootloader's job, done by the bootloader, since a kernel with no filesystem has to get its first process from somewhere. The instruction fetch is guarded now too, which is what made the previous slice's debugging possible at all._</p> <p>_A consequence worth noting: any <code>--bare</code> program that installs a trap vector must clear it before returning, or <code>_start</code>'s exit <code>ecall</code> vectors into its own handler instead of halting. The timer and shell examples now do._</p> <p>_Two new tests. <code>dune runtest</code> 2336/0, ctest 13/13, parity 84/84._</p> <hr> <h2 id="v0-1-193-2026-08-11">v0.1.193 — 2026-08-11</h2> <p>_A shell, on a machine with no operating system. And a rule the previous slice got wrong._</p> <p>_<a href="../examples/riscv_bare_shell.mere">examples/riscv_bare_shell.mere</a> reads a line from the UART, dispatches a handful of commands, and reports on the machine it is running on: <code>bg</code> twice shows a counter that climbed while the shell sat waiting for a keystroke — nothing yielded to it, the timer took the CPU away and the handler gave it to the other task. <code>fault</code> reads an address the machine does not have, and the same handler that schedules fields the access fault, counts it, and steps over the faulting instruction, so a bad command does not take the machine down. Each command runs inside <code>region R { ... }</code>, which is what keeps the heap flat across a session on a backend whose allocator never frees._</p> <p>_It needed no new language features. That is the point of the slice, and it is also the signal that this dogfood has stopped generating pressure._</p> <p>_<strong>The correction.</strong> v0.1.191 said the trampoline's save-and-restore of <code>gp</code> gives "a region per trap, for free". That is wrong, and this shell is what proved it: a trap handler that allocates corrupts the program it interrupted. Reproducibly — three sessions, three corruptions — and cleanly fixed by moving the handler's two register-copy loops to top level, where they are functions rather than closures and so allocate nothing._</p> <p>_Two candidate mechanisms were investigated and neither fully explains it. The runtime's variable-size allocators did claim a block and write its header <strong>before</strong> advancing <code>gp</code>, which leaves a window where an allocating trap lands on a block that already has contents; those are reordered here to bump first and fill after (<code>__str_concat</code>, <code>__str_of_int</code>, <code>__substring</code>, <code>__strbuf_new</code> — the vec allocators were already in the right order). That is a real latent bug and worth fixing on its own, but it was not the whole story: with it fixed the corruption still reproduced. Removing the handler's allocation fixes it; so does not rolling <code>gp</code> back. The honest state is that the interaction between a bump allocator and an involuntary interruption is not yet understood, and the working rule — <strong>do not allocate in an interrupt handler</strong> — is one real kernels keep anyway. It is now what the example does and why._</p> <p>_<code>dune runtest</code> 2334/0, ctest 13/13, parity 84/84; the interpreter-vs-emulator sweep is unchanged at 39 / 38 / 8._</p> <hr> <h2 id="v0-1-192-2026-08-11">v0.1.192 — 2026-08-11</h2> <p>_Preemptive multitasking. Two Mere tasks, neither yielding, on a machine with no operating system._</p> <p>_A context switch turned out to need no new mechanism. The trampoline already saves the interrupted register set to a known place and restores from that same place before <code>mret</code>, so switching tasks is a memory copy in the middle of a handler: the save area into the outgoing task's TCB, the incoming task's TCB back into the save area, and its PC as the handler's result. The scheduler in <a href="../examples/riscv_bare_sched.mere">examples/riscv_bare_sched.mere</a> is thirty lines of ordinary Mere._</p> <p>_What a bare program could not do was <strong>name</strong> any of it, so five primitives went in — all narrowing from the machine capability, so none of them hands out authority the program did not already have. Only coordinates:_</p> <ul> <li>_<code>trap_save mach</code> — the save area, so a handler can reach both sides of a switch_</li> <li>_<code>machine_scratch mach</code> — reserved RAM the runtime is not using, which is where</li> </ul> <p> a task stack comes from. A bare program owns no fixed address of its own: the heap grows up from 2MB and the stack down from the top, so any address it picks is one the compiler is already using. The timer example's first draft learned this by sharing a word with a top-level binding._</p> <ul> <li>_<code>raw_base w</code> — a window's base as a number, because a stack pointer is an</li> </ul> <p> address and the hardware wants the number. Not authority: touching anything still needs a window._</p> <ul> <li>_<code>closure_code f</code> / <code>closure_env f</code> — a task IS a closure, so starting one means</li> </ul> <p> building a context whose PC is its code and whose a0 is its environment. ABI knowledge, which a kernel legitimately has._</p> <p>_One word must <strong>not</strong> be switched, and finding out why is the interesting part: <code>gp</code>, the heap's bump pointer. Save and restore it per task and two tasks allocate over each other, each rolling the pointer back to where it stood when it last ran. The heap is machine state, not task state. That is a real collision between this language's memory model and concurrency, and the answer here — share the bump pointer, leave the word alone across a switch — is the simplest one that is correct. A per-task heap would be the other, and it is not needed yet._</p> <p>_Also: the RV32I backend had <code>print_int</code> but not <code>print_bool</code>, which the parity file added in v0.1.190 immediately caught by being the one file <code>-rv</code> refused. It lowers the way LLVM and Wasm do it, as <code>print</code> of a literal._</p> <p>_Two new tests. parity 84/84, ctest 13/13, <code>dune runtest</code> 2334/0. The interpreter-vs-emulator sweep of <code>test/parity</code> is 39 identical / 38 refused / 8 mismatching — one better than last slice, since print_int_bool now runs there too._</p> <hr> <h2 id="v0-1-191-2026-08-11">v0.1.191 — 2026-08-11</h2> <p>_The machine takes traps, and a Mere closure services them. A timer interrupt arrives while the program is doing something else._</p> <p>_A trap handler cannot be an ordinary function: it is entered with every register live and it leaves with <code>mret</code>, not <code>ret</code>. The tempting move is to give the language a <code>naked fn</code> or an interrupt attribute. That is not needed — codegen already emits <code>_start</code>, so it can emit the trampoline too, and the user writes plain Mere._</p> <p>_The harder question was how a handler gets the machine capability. It needs one for anything useful (a context switch is a memory copy), and an interrupt has no caller to hand it anything. So the handler is <strong>registered rather than named</strong>: <code>set_trap_handler (fn cause -> ...)</code> takes a closure, which captures whatever it needs. The trampoline stores it, points mtvec at itself, and calls it with mcause; the result is the PC to resume at. Everything else the handler might want is a <code>csr_read</code> away — mepc, mtval — so nothing has to be packed into a tuple, which would mean allocating inside a trap._</p> <p>_<code>mscratch</code> holds the save area's address, because at trap entry there is no free register to build one in — which is what that CSR exists for. <code>gp</code>, the bump pointer, is saved and restored with the rest, so whatever the handler allocated is reclaimed when it returns: a region per trap, for free. The corollary, worth stating, is that a handler must not leave an allocated value somewhere that outlives it._</p> <p>_<strong>(v0.1.193: that last paragraph is wrong. An allocating handler corrupts the program it interrupted, reproducibly. The rule is that a trap handler must not allocate at all — see v0.1.193.)</strong>_</p> <p>_On the emulator side traps vector to mtvec with mepc / mcause / mtval set and MIE moved into MPIE. Three causes so far: an unimplemented instruction (2), a load or store past the end of RAM (5 / 7), and the timer (0x80000007). The access faults are an improvement in their own right — an address past RAM used to take the <strong>emulator</strong> down with a Mere "index out of bounds", reporting the host's problem instead of the guest's. A guest with mtvec still zero halts, as before, rather than jumping to address 0._</p> <p>_The timer is a CLINT with mtime and mtimecmp in the MMIO region. mtime advances once per instruction: a clock in units of work done, which is what a deterministic emulator can honestly offer and enough for a scheduler tick. The interrupt is taken between instructions._</p> <p>_<a href="../examples/riscv_bare_timer.mere">examples/riscv_bare_timer.mere</a> arms it and spins. The ticks arrive anyway, which is the mechanism preemption is made of: once a handler runs without the interrupted code's cooperation, a scheduler is a matter of what that handler chooses to return._</p> <p>_The example's first draft kept its tick counter at <code>0x200000</code> and quietly shared a word with a top-level binding — that address is where globals live. A bare program owns no fixed RAM: the heap grows up from 2MB and the stack down from the top. The handler's state is an ordinary Mere cell it captures instead, which is both correct and the better demonstration._</p> <p>_Three new tests (twenty-one on this backend). parity 84/84, ctest 13/13, <code>dune runtest</code> 2332/0._</p> <hr> <h2 id="v0-1-190-2026-08-11">v0.1.190 — 2026-08-11</h2> <p>_<code>print_int</code> and <code>print_bool</code>, which the reference has claimed for every backend since Phase 22 and which only the interpreter had._</p> <p>_Found while smoke-testing the RV32I work: <code>let f = fn n -> let _ = print_int n in n;</code> would not compile on the <strong>C</strong> backend. Not a shadowing or value-position subtlety — codegen_c had no arm for the name at all, so it fell through to the closure path and emitted a call to an undefined <code>mu_print_int</code>. The refusal then arrived from clang, as "use of undeclared identifier", about a symbol the user never wrote. LLVM and Wasm at least said what they meant ("no lowering yet"), though LLVM's message named its scope as "interp + C", which was not true either._</p> <p>_So a documented builtin worked in one of four places, and the one that failed worst failed at the wrong layer. All four now agree. C prints through <code>printf</code> (<code>%lld</code>, matching its int width) and <code>puts</code> for the bool. LLVM and Wasm route through the <code>str_of_int</code> they already had, so neither needed a new runtime call or host import — <code>print_int x</code> becomes <code>print (str_of_int x)</code> at emit time. That also needed <code>print_int</code> added to each one's use-scan, since the helper <code>@show_int</code> / <code>$show_int</code> is only defined when something registers it: exactly the v0.1.42 gap, one level further in._</p> <p>_<code>test/parity/print_int_bool.mere</code> keeps the four in step over zero, negatives and a bool from a comparison. parity 84/84, ctest 13/13, <code>dune runtest</code> 2329/0._</p> <p>_The reference line that claimed all this now says what actually happened._</p> <hr> <h2 id="v0-1-189-2026-08-11">v0.1.189 — 2026-08-11</h2> <p>_Machine CSRs, and a non-blocking byte of stdin so a UART can have a receive side._</p> <p>_<strong>CSRs.</strong> <code>csr_read</code> / <code>csr_write</code> lower to CSRRS-from-x0 and CSRRW-to-x0, and the register number has to be a literal — it is a 12-bit field of the instruction, so a computed one has nowhere to go, and saying so beats emitting something that reads a register nobody asked for. They are <code>--bare</code> only: a trap vector means nothing under a host. Unlike raw memory they are <strong>not</strong> behind a capability, and that is a decision rather than an oversight — a CSR has no base and length to narrow, and the hardware already has machine / supervisor / user mode to separate a kernel from a user process. Duplicating that in the type system before there is a user mode to protect would be speculative._</p> <p>_The disassembler had been rendering every CSR instruction as <code>ebreak</code>, because it only knew <code>inst = 0x73</code>. It now decodes the six CSR forms plus <code>mret</code> and <code>wfi</code>, which matters more than it sounds: this backend's debugging story is reading its own listings._</p> <p>_<strong>A receive side.</strong> The plan for the UART's input half was "the emulator reads host stdin with <code>read_key</code>", and that does not work: <code>read_key</code> blocks. A CPU polling a line-status register cannot stop the machine to find out whether a byte is waiting — and once there are timer interrupts it will have other things to do while nothing is arriving. So the dogfood forced a new builtin instead: <strong>`stdin_byte : unit -> int`</strong>, one byte or -1 when nothing is ready, on the interpreter and the C native backend (select with a zero timeout, so it leaves stdin's flags alone and composes with <code>tty_raw</code> and with a plain pipe alike)._</p> <p>_With that, <a href="../examples/riscv_bare_echo.mere">examples/riscv_bare_echo.mere</a> echoes what you type through the UART and stops at <code>q</code> — polling the status register, reading the data register, all through the window capability <code>main</code> was handed, with no host syscall anywhere in it. That is the last piece the shell needs._</p> <p>_Four new tests (eighteen on this backend). parity 83/83, ctest 13/13, <code>dune runtest</code> 2329/0._</p> <p>_Recorded while here, unrelated and pre-existing: on the <strong>C</strong> backend <code>let f = fn n -> let _ = print_int n in n;</code> does not compile — the builtin ends up in value position and the emitted C calls an undefined <code>mu_print_int</code>. It reproduces on the pre-session compiler, so it is not from this arc; <code>print</code> on a str refuses at codegen instead. Worth its own slice._</p> <hr> <h2 id="v0-1-188-2026-08-11">v0.1.188 — 2026-08-11</h2> <p>_Bitwise operators on the RV32I backend, and the INT_MIN bug they found._</p> <p>_The UART example from the previous slice had to read a status bit with <code>/ 32 % 2</code>, because this backend had no <code>bit_and</code>. A device driver is mostly masks and shifts, so that was the next thing in the way. All six lower now: AND / OR / XOR as R-type, or the I-type form when the operand is a small literal, <code>bit_not</code> as <code>xori -1</code>, and <code>bit_shl</code> / <code>bit_shr</code> as SLL / <strong>SRA</strong> — arithmetic, because <code>bit_shr</code> is documented as floor division by 2^n on every backend._</p> <p>_Shift counts of 32 or more needed a decision. RV32 shifts use only the low five bits of the count, so a bare SLL would quietly make <code>bit_shl x 33</code> mean <code>x << 1</code>. What the other backends produce, read back as 32 bits, is zero for a left shift and the sign bit for a right shift, so that is what this emits: folded when the count is constant, and three extra instructions (or a branch) when it is not. A silently different answer was not on the table._</p> <p>_Then <code>bit_shl 1 31</code> printed <code>-./,),(-*,(</code>._</p> <p>_Not a shift bug — <code>str_of_int</code> and <code>print_int</code> both began by negating a negative value to make it positive, and 0x80000000 negated is still 0x80000000. Every remainder after that came out negative and <code>'0' + negative</code> is punctuation. Both helpers now go the other way: make the value <strong>negative</strong> (negating a positive is always safe) and take each digit as <code>-(x % 10)</code>. So INT_MIN prints. This had been latent since the backend's first slice; nothing before now had produced 0x80000000, because nothing before now could shift._</p> <p>_The bitwise builtins take three files off the interpreter-vs-emulator sweep's "refused" list. One joins the matching set; the other two ask for integers wider than 32 bits (<code>int64_bitwise</code> shifts 1 by 40, and <code>riscv_core</code> is the RV32I emulator itself, which needs headroom above 32 bits before masking), so they cannot match on a 32-bit target and are recorded as such. The sweep now reads 38 identical / 38 refused / 8 mismatching._</p> <p>_Three new tests. parity 83/83, ctest 13/13, <code>dune runtest</code> 2325/0. The UART example reads its status bit with <code>bit_and</code> now._</p> <hr> <h2 id="v0-1-187-2026-08-11">v0.1.187 — 2026-08-11</h2> <p>_Raw memory, as a capability rather than an ambient builtin. <code>mere -rv --bare</code> hands a program the machine and it writes to a UART._</p> <p>_A kernel needs to reach hardware, and this backend's answer so far was one builtin per device with the address baked into codegen: <code>fb_set</code> stores to the framebuffer, <code>key</code> loads from the key register, <code>present</code> ends a frame. Adding a UART, a timer and an interrupt controller that way means a compiler change per device, which is the opposite of what a dogfood is for — the kernel would teach the language nothing. The alternative that fits what this language already says about itself (README: effects are capability values you pass) is to make the address space a value._</p> <p>_A <strong>`Raw`</strong> is a window onto physical memory: a base and a length. It is opaque, nothing constructs one, and there is no function that mints one — the only source is the argument <code>--bare</code> hands to the program's top-level <code>main</code>, and <code>raw_window</code> can only narrow. Offsets are relative to the window, so a driver holding a UART window cannot express an address outside it. All three ways out are closed and each fails differently: forging one from ints is a type error, widening one faults at construction, and an offset past the end faults at the access._</p> <p>_So this reads as a promise rather than a hope:_</p> <pre><code> let putc = fn (uart: Raw) -> fn (c: int) -> raw_poke8 uart 0 c; </code></pre> <p>_<code>putc</code> can touch the UART and nothing else — not the heap, not the stack, not another device — and that is visible in its signature instead of being a claim about its body and every body it calls. That is the whole argument for a value over an ambient builtin._</p> <p>_The bounds check is not free and not optional: a window's length is a runtime field, so there is nothing to fold at compile time even when the offset is a literal. Three instructions on an MMIO poke buys a guarantee that holds, which is the better trade than a nominal one._</p> <p>_Device MMIO now sits above any RAM — the UART's data register is at <code>0x10000000</code>, the address QEMU's <code>virt</code> machine uses, so a driver written today is not inventing a private convention. <code>--ram</code> is capped so RAM can never reach it, which means a device address does not move when the RAM size does. (The fantasy console's framebuffer and keys predate this and still live in the reserved top of RAM; they move with <code>--ram</code> and will migrate when that demo is next touched.)_</p> <p>_<code>--bare</code> also refuses <code>print</code> / <code>print_int</code> / <code>print_no_nl</code> / <code>print_err</code>: those lower to the emulator's write syscall, which no real machine answers, and a bare program that depends on the courtesy would break the moment it left the emulator. A UART window is three lines away — see <a href="../examples/riscv_bare_uart.mere">examples/riscv_bare_uart.mere</a>, which prints through one and reads the 16550 line-status register back._</p> <p>_Every other backend refuses these names, because there is no honest physical address in a hosted process. The C backend had to be taught to: without an arm of its own the raw_\<em> names fell through to the closure path and emitted a call to an undefined `mu_raw_poke8` plus an unknown `Raw` C type, so the refusal arrived from clang as "type specifier missing" — loud, but about the wrong thing. LLVM and Wasm already refused._</em></p> <p>_Five new tests, fourteen on this backend now. parity 83/83, ctest 13/13, <code>dune runtest</code> 2322/0; the interpreter-vs-emulator sweep of <code>test/parity</code> is unchanged at 37 / 41 / 6._</p> <hr> <h2 id="v0-1-186-2026-08-11">v0.1.186 — 2026-08-11</h2> <p>_A RAM size instead of three hardcoded addresses, and the self-hosted compiler runs on the RV32I emulator again._</p> <p>_v0.1.185 ended by reporting that the self-hosted compiler no longer fits on this backend. The reason it did not fit was not really its appetite: the stack top, the print scratch buffer and the fantasy-console framebuffer were three immediates baked into codegen, which pinned the heap's ceiling at 0x7E0000 and gave every program on this backend the same 5.86MB no matter what it was doing._</p> <p>_They now derive from one number. The top 128KB of RAM holds the scratch buffer and the MMIO; the stack starts just below that and grows down; the heap grows up from 2MB; everything between the two growing ends belongs to them. At the default 8MB every address comes out exactly where it has always been, so an emulator sized for the old layout needs no change. <code>mere -rv --ram <MB></code> (and <code>-rvs --ram</code>) raises it._</p> <p>_With that, the measurement the previous slice could not make: the self-hosted compiler's heap peaks somewhere between 14 and 18MB — it fails at <code>--ram 16</code> and completes at <code>--ram 20</code>. At 20MB it compiles <code>1+2</code> on the Mere-written RV32I emulator and emits WAT byte-identical to the native interpreter. The tower from v0.1.147 stands again, and this time the requirement is written down rather than implied: a binary says how much RAM it wants, an emulator is told the same number, and a mismatch fails loudly at the first allocation past the limit instead of corrupting a frame._</p> <p>_The emulator side of that (a RAM size argument, and dropping the 100M instruction budget the compiler ran past) lives in the memu project, not here. Two tests lock the layout: the default still puts the stack at 0x7E0000, and <code>--ram 32</code> moves it to 0x1FE0000. <code>dune runtest</code> 2317/0; the interpreter-vs-emulator sweep of <code>test/parity</code> at the default size is unchanged at 37 identical / 41 refused / 6 mismatching._</p> <hr> <h2 id="v0-1-185-2026-08-11">v0.1.185 — 2026-08-11</h2> <p>_Three holes in the RV32I backend, found by asking what a program that never returns would need._</p> <p>_The next dogfood for this backend is a bare-metal kernel: a scheduler, a trap handler, a shell. Every one of those is a loop that never ends, and iteration here is recursion — explicitly, and also under <code>while</code>, which the parser desugars to a tail-recursive local closure. So the first question was how long a recursion this backend can actually sustain, and the answer was: not long, and it does not say so. A zero-allocation tail-recursive counter completed at 500,000 and died silently at 1,000,000; raising the emulator's instruction budget twentyfold did not change that, so it was the stack, not the clock. A <code>while</code> loop, which pays for a closure frame per iteration, died at 300,000._</p> <p>_<strong>Tail calls.</strong> A saturated call in tail position now tears the frame down first and jumps, so the callee returns straight to our caller and the stack stays flat. The tail-position bookkeeping mirrors codegen_wasm's <code>wasm_tail_pos</code> (which lowers to <code>return_call</code>): compile_expr clears the flag for every subexpression and the cases whose value IS the enclosing value — if branches, let bodies, match arms, annotations — put it back. Direct calls become <code>j u_f</code> instead of <code>jal ra, u_f</code>; the closure form, which is the shape a local <code>let rec loop = fn ...</code> and every desugared <code>while</code> actually take, becomes <code>jalr x0</code> on the code pointer. Calls with nine or more arguments keep the old path: args 9+ travel on the caller's stack, which a teardown would drop. The counter now runs 10,000,000 iterations in constant stack._</p> <p>_<strong>Regions.</strong> <code>region R { ... }</code> compiled to its body and nothing else — the comment said "no reclamation" and meant it. <code>gp</code> is the only allocation state on this backend, so the fix is the one Wasm already uses on <code>__lang_bump</code>: park it on entry, roll it back at the closing brace. Eight rounds of 100,000 allocations inside a region now complete with a flat heap; the same program without the region reports exhaustion. The body is deliberately not in tail position — a tail call out of a region would skip the rollback._</p> <p>_A region also could not call anything. <code>vars_in</code>, which decides which top-level functions are reachable and therefore emitted, had no <code>Region_block</code> case, so a function called only from inside a region was never emitted and assembly died with <code>undefined label u_f</code>. Two lines reproduce it: <code>let f = fn x -> x + 1;</code> and <code>region R { f 41 }</code>. It stayed hidden because a region body of nothing but builtins resolves fine. <code>free_vars_of</code> had the same gap, which would have dropped a capture._</p> <p>_<strong>Heap exhaustion.</strong> The heap grows up from 2MB and the stack grows down from 0x7E0000 with nothing between them, and when they met the bump pointer overwrote a live frame's return address with whatever it was allocating — the program then jumped into the middle of a string. No message, no exit code, just an emulator reporting a wild load. Every bump now checks <code>bgeu gp, sp</code> and lands on <code>__oom</code>, which prints and exits 3._</p> <p>_That check immediately reported something. The self-hosted compiler, compiled with <code>-rv</code> and run on the Mere-written RV32I emulator, now says it is out of memory — and with the check patched out it dies the old way, on a wild load, so this is not a regression from this slice. The self-hosted codegen's output has grown about a hundredfold since that demo was first verified (a five-line input now emits ~5,200 lines of WAT, nearly all of it fixed prelude), and building that with <code>++</code> on a bump allocator that never reclaims needs far more than the 5.86MB this memory map leaves. Recorded, not fixed: it wants a configurable RAM size and a memory map that separates the MMIO region from the heap's path, which is the same work the kernel needs._</p> <p>_This backend had <strong>zero</strong> automated coverage — the region bug was a hard crash that nothing in the suite would have caught. Seven tests now assert on the emitted listing, so they lock the instruction actually chosen (<code>j</code> vs <code>jal</code>, the bump park/restore, the heap check) rather than just that codegen ran. parity 83/83, ctest 13/13, <code>dune runtest</code> 2315/0. A differential sweep of <code>test/parity</code> through the interpreter and through <code>-rv</code> + the emulator: 37 identical, 41 rejected by the backend as unsupported, 6 mismatching — byte-for-byte the same three numbers and the same six files before and after this slice._</p> <hr> <h2 id="v0-1-184-2026-08-11">v0.1.184 — 2026-08-11</h2> <p>_<code>to_json</code> on LLVM, and a parity harness that counts what it did not check._</p> <p>_This one is not a dogfood, and saying so is the point. Nothing forces <code>to_json</code> on LLVM: everything native in this repo goes through C, and no app names LLVM. Inventing an app to justify the capability would invert the discipline the browser dogfoods have been run on all week — apps force capabilities, not the reverse._</p> <p>_What justifies it is coverage, and coverage is a number. <code>scripts/parity.sh</code> treated a clean refusal as a passing row, so a backend that checked nothing said so in a word buried in a line. It now tallies per backend and names the cases:_</p> <pre><code> unchecked on llvm: 7 of 83 (refused at emit time) nested_tuple of_json_composite ... </code></pre> <p>_Five of LLVM's seven were the JSON family, and that number grows with every test that touches <code>to_json</code> — <code>contrib/schema</code> being a library means future apps using it would go unchecked there too._</p> <p>_<code>to_json</code> turned out to be <code>show</code> with different literals: the same structural walk, the same per-type functions, the same recursion. So it is not a second emitter but one with a mode — <code>emit_struct_fn ~json:true</code> — which is also the arrangement that keeps them from drifting. The differences are all shape: <code>[..]</code> for a tuple, <code>{"f":..}</code> for a record, <code>","</code> rather than <code>", "</code> in a list, a quoted name for a nullary constructor and a single-key object for a carrying one, and <code>null</code> for unit. Option is the one real special case — JSON spells it as the value or <code>null</code>, not as <code>{"Some":4}</code> and <code>"None"</code> — and getting that wrong was the last diff before the backends agreed._</p> <p>_LLVM's blind spot is 7 → 6, and every remaining case is <code>of_json</code>. That stays deferred: decoding needs a JSON parser written in the target language, C and Wasm each have their own hand-written one, and nothing rides on a third. The number is in the harness now, so the cost of leaving it is visible rather than argued about._</p> <p>_parity 83/83, ctest 13/13, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-183-2026-08-11">v0.1.183 — 2026-08-11</h2> <p>_<code>of_json_like</code>: the target type from a witness value, so a decoder can live inside a polymorphic function — and <code>contrib/schema</code>, which is what that was for._</p> <p>_<code>of_json</code> reads its target type off the call node. At a use site with an annotation that is exactly right; inside a generic helper it is a variable, and the interpreter has no runtime types to resolve it with. The monomorphizing backends managed — with v0.1.182's fix a generic setter compiled and ran correctly on C — so a program could be compiled and not interpreted, which is the wrong kind of split for a language whose parity harness treats the interpreter as the reference._</p> <p>_A witness closes it. <code>of_json_like : 'a -> str -> 'a</code> takes a value of the target type; the interpreter reads the type off its runtime shape (a record carries its type's name, a constructor gives one through <code>Typer.constructors</code>) and the compiled backends read it off its static type, which is the same variable the result unifies with. <code>of_json_opt_like</code> is the non-crashing form, and the one a generic setter actually needs: it tries a shape and finds out whether it decoded. The witness is never an imposition — replacing one field of a record means holding the record._</p> <p>_<code>contrib/schema/reflect</code> is the payoff: <code>schema_fields</code> / <code>schema_text</code> / <code>schema_with</code> over any record, from the name-keyed view the compiler already synthesises for <code>to_json</code>. <code>examples/claims</code> had carried a per-record copy of exactly this since v0.1.176 and now imports it, so a form generated from a record declaration is a library rather than a trick in one app._</p> <p>_Two collection gaps surfaced while wiring it. <code>collect_mono_variant_instances</code> walked fn signatures and not fn bodies, so <code>person option</code> — produced by <code>of_json_opt_like</code> inside a generic setter whose own signature never mentions an option — was never registered and the emitted C named an undeclared struct. And the new collector arms called <code>ty_tag</code> before checking the type was concrete, which inside the generic skeleton it is not._</p> <p>_A polymorphic record still needs the annotation: a value carries its type's name but not its type arguments, so a witness cannot describe <code>Box[int]</code>. LLVM is unchanged — it has no <code>to_json</code> at all, which is a separate gap._</p> <p>_<code>test/parity/schema_reflect.mere</code> runs the one implementation over two record types. parity 83/83, ctest 13/13, dune runtest 2308/0, claims browser check 17/17 against the native server._</p> <hr> <h2 id="v0-1-182-2026-08-11">v0.1.182 — 2026-08-11</h2> <p>_The monomorphization bug, found and fixed. <code>specialize_single_use_local_fns</code> read "one concrete use" as "used at one type"._</p> <p>_Three slices ago this was a silent miscompile; two ago it became a named refusal; here it is a working program. The cause is one line of judgement in a codegen pass._</p> <p>_<code>specialize_single_use_local_fns</code> fixes a local polymorphic fn's type in place when its body contains exactly one concrete use of it — a good optimisation, since a fn used at one type needs no multi-instantiation machinery. But <code>find_all_concrete_arrows_in</code> only reports the uses it can already read, and a use sitting inside a polymorphic function is not concrete <strong>yet</strong>:_</p> <pre><code class="language-mere"> let hold = fn (v) -> (v, v); let mk = fn (v) -> (hold v, hold 1); </code></pre> <p>_<code>hold 1</code> is concrete. <code>hold v</code> is not, and only becomes so once <code>mk</code> is instantiated — at which point it may be a different type. The pass counted one arrow, unified <code>hold</code>'s definition with <code>int</code>, and the <code>str</code> instantiation had nothing left to unify with, so it was emitted with the <code>int</code> body. It now also requires that the body contain no unresolved use of the name. False only when a use genuinely cannot be read yet, so a fn that really is used at one type still specializes._</p> <p>_How it was found is worth recording, because reasoning about it was wrong three times. Instrumenting <code>generalize</code> showed <code>hold</code> correctly generic with one quantified variable. Instrumenting the skeleton collection showed it arriving at codegen as <code>int -> (int * int)</code>. Removing each post-typing pass in turn changed nothing. Watching the specific type variable's binding site printed <code>Loc.dummy</code> — which is not a source location at all, and which only codegen uses._</p> <p>_The payoff: <code>contrib/state/store</code> uses <code>contrib/state/cell</code> again. It had been writing its three one-slot vecs out longhand since v0.1.178 for exactly this reason — a store instantiated at two state types calls <code>cell_new</code> at a type derived from S and at plain <code>int</code> for its token counter — so the module that names the trick can now use it._</p> <p>_<code>test/parity/poly_helper_fixed_and_free.mere</code> has outlived two expectations (wrong, then refused, now <code>1s2</code> on all four backends) and is kept because the shape is easy to break again. parity 82/82, ctest 13/13, dune runtest 2308/0, claims browser check 17/17 against the native server, and all five browser clients rebuild._</p> <hr> <h2 id="v0-1-181-2026-08-11">v0.1.181 — 2026-08-11</h2> <p>_Two corrections and a better repro. No new capability._</p> <p>_The monomorphization repro from v0.1.179 used <code>vec_new</code> and read as if mutable containers, the narrow value restriction or the region model were involved. None of them are. Reduced further, <code>hold</code> allocates nothing:_</p> <pre><code class="language-mere"> let hold = fn (v) -> (v, v); let mk = fn (v) -> (hold v, hold 1); // parameter-derived AND fixed let (a1, a2) = mk 1 in let (b1, b2) = mk "s" in ... </code></pre> <p>_Three neighbouring shapes compile and run — one instantiation of <code>mk</code>, <code>hold</code> called only at the parameter-derived type, and <code>hold</code> used directly at two types — so it is the combination and nothing smaller. <code>test/parity/poly_helper_fixed_and_free.mere</code> now holds that version._</p> <p>_The search for where the skeleton gets fixed narrowed without landing. <code>generalize</code> is called on <code>hold</code> and <code>instantiate</code> substitutes without mutating, so use sites cannot be reaching back to the definition — and yet by the time codegen takes its pristine clone the skeleton reads <code>int -> (int * int)</code> while <code>mk</code> beside it is still <code>'a -> (('a * 'a) * (int * int))</code>. Whatever fixes it runs before codegen. Recorded in the test rather than guessed at._</p> <p>_The second correction is to v0.1.180's memory measurement, and is written into that entry: two samples read as a leak in <code>contrib/store/kvlog</code>, and nine thousand requests show RSS oscillating (4528, 3568, 5840, 7536, 6656, 8272 KB) rather than climbing. That is malloc churn as the region takes and returns blocks. The native HTTP arena is doing its job; there is no kvlog leak to fix, and the item is withdrawn rather than carried._</p> <p>_parity 82/82, ctest 13/13, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-180-2026-08-11">v0.1.180 — 2026-08-11</h2> <p>_The native HTTP runtime gets the four externs a middleware stack needs, so <code>examples/claims</code> runs on both hosts from the one source._</p> <p>_It implemented six. contrib/http's middlewares need four more: <code>http_current_status</code> and <code>unix_time</code> for access_log, <code>http_arena_mark</code> for the per-request arena, <code>http_send_file</code> for static. A server with a middleware stack could therefore be built for the Node host and not the native one — which is half of "the same source runs on both", and the half nobody had checked because the browser dogfoods only ever ran the Wasm host._</p> <p>_All four are in. The arena checkpoint is the interesting one: the C region is a chain of malloc'd blocks with a bump pointer, so a mark records the block chain and the pointer, and the release frees every block newer than the mark and winds the pointer back. It runs after the response has gone out on the socket, not before, because the body it just wrote lives in that arena. The checkpoint is taken where the request began rather than wherever the handler calls <code>http_arena_mark</code>, so the request line and the body copy — made by the accept loop before the handler is entered — are inside it too. Still opt-in: nothing is released unless a handler asks._</p> <p>_<code>http_send_file</code> writes the file straight to the socket without it passing through a Mere <code>str</code>, which is the point of that binding — a str is NUL-terminated and a file is not — and tells the accept loop not to write a second response after it._</p> <p>_Measured on the native build: <strong>116 KB reclaimed per request</strong>, with no new blocks allocated on the paths that fit, so the region stays in its first 4 MB. Two thousand 404s in a row moved RSS 3920 → 1920 KB, i.e. down._</p> <p>_(Correction, made while writing v0.1.181: this entry first read a second measurement — 1920 → 6336 KB over two thousand requests that read the kvlog — as locating a leak in <code>contrib/store/kvlog</code>. Sampling nine thousand requests instead of two thousand shows RSS oscillating rather than climbing: 4528, 3568, 5840, 7536, 6656, 8272 KB. That is malloc churn as the region takes and returns blocks, not a per-request leak, and two samples were not enough to say which.)_</p> <p>_<code>examples/claims/browser_check.mjs</code> passes 17/17 against the native server, unchanged from the Wasm host. parity 82/82, ctest 13/13, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-179-2026-08-11">v0.1.179 — 2026-08-11</h2> <p>_The monomorphization gap v0.1.178 recorded without a repro, isolated to seven lines — and made loud, which is as far as this slice goes._</p> <p>_The shape, self-contained and with no imports:_</p> <pre><code class="language-mere"> let hold = fn (v) -> let c = vec_new () in let _ = vec_push c v in c; let mk = fn (v) -> (hold v, hold 1); // parameter-derived AND fixed let (a1, a2) = mk 1 in let (b1, b2) = mk "s" in ... </code></pre> <p>_A polymorphic helper called at both a parameter-derived type and a fixed one, inside a function that is itself used at two types. Written <code>(hold v, hold v)</code> it is fine, and used directly at two types it is fine; it is the combination._</p> <p>_What it produced: <code>hold</code> monomorphized into an <code>int</code> copy and a <code>str</code> copy, both declarations with the right signature and <strong>both bodies with the `int` one's operations</strong> — a <code>mere_vec_str*</code> function calling <code>mere_vec_int_push</code>. Not a wrong answer, no answer, and only when the C compiler saw it, with a message about pointer conversions naming nothing in the source._</p> <p>_The mechanism, traced: codegen keeps a pristine clone of each polymorphic skeleton and unifies a fresh copy with every instantiation's arrow. That unify was wrapped in <code>try ... with _ -> ()</code>, and when it fails the spec's body belongs to another type. It cannot fail while the skeleton is genuinely polymorphic — and here it is not. Instrumenting the clone showed the typer hands codegen <code>hold</code> already fixed at <code>int -> Vec[__heap, int]</code>, while <code>mk</code> beside it is still <code>'a -> ...</code>. Written <code>(hold v, hold v)</code> the skeleton arrives polymorphic._</p> <p>_All three compiled backends now refuse instead of emitting, with a message that names the function, the arrow it cannot take, and the type it is stuck at. That turns a wrong program into a named one; it is not the fix. The fix is upstream of codegen, in whatever fixes the skeleton before it is cloned, and <code>test/parity/poly_helper_fixed_and_free.mere</code> will change from UNSUP to a real answer when that lands._</p> <p>_This is why contrib/state/store writes its three one-slot vecs out longhand instead of using contrib/state/cell: a store instantiated at two state types calls <code>cell_new</code> at a type derived from S and at plain <code>int</code> for its token counter. The module that names the trick does not get to use it, and now says why. parity 82/82, ctest 13/13, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-178-2026-08-11">v0.1.178 — 2026-08-11</h2> <p>_A page with two views, so that a watcher's lifetime becomes a question — and the answer that <code>drop type</code> cannot give._</p> <p>_<code>examples/claims</code> grew a tab bar over one store: a Form view that builds the generated controls, the line rows and the actions, and a read-only Summary view. Switching drops the old view's DOM wholesale. Dropping the nodes did not drop the watchers, because contrib/state had no way to take one back, so <strong>three round trips between the tabs left eleven watchers running, nine of them painting into nodes no longer in the document.</strong> The counter in the tab bar is how that became visible at all._</p> <p>_<code>store_watch</code> now returns a token and <code>store_unwatch</code> takes it; the app collects a view's tokens and releases them on close. <strong>11 → 2, and constant however far the user navigates.</strong>_</p> <p>_The language question this was chosen to ask has a negative answer, which is worth having. Mere's one mechanism for enforced release is a <code>drop type</code> bound by <code>with</code>, whose <code>close</code> runs at scope end — and <strong>a subscription's lifetime is not the scope that created it.</strong> The view is built in one event and torn down in another, so by the time <code>with</code> would close the handle the view has not even been shown. A drop type also cannot be placed in a region, which is where the watcher list lives. So releasing stays a convention, recorded in contrib/state's README rather than papered over._</p> <p>_Two compiler findings on the way. A binding called <code>entry</code> did not compile on LLVM at all: values and basic-block labels share one namespace there and every emitted function opens with a block called <code>entry</code>, so a parameter of that name claimed the slot first — "unable to create block named 'entry'". Not a wrong answer, no answer, from a name nothing warns about; <code>entry</code> is the obvious name for an element of an association list, which is how it turned up. <code>llvm_safe_local</code> renames the parameter, since the <code>entry:</code> label is written into every hand-authored runtime blob in that file. <code>test/parity/reserved_local_entry.mere</code> holds it across the shapes that emit a parameter separately — top-level fn, lifted inner fn, closure adapter._</p> <p>_And the store could not use contrib/state's own <code>cell</code>: a store instantiated at two different state types puts <code>cell_new</code> at three types derived from S, and that shape does not survive monomorphization on C or LLVM. The builtin vec does, so the module that names the trick writes its three slots out longhand. Recorded in the source; a minimal repro is not yet isolated._</p> <p>_parity 81/81, ctest 13/13, dune runtest 2308/0, claims browser check 17/17 — including that three round trips between views leave no watchers behind._</p> <hr> <h2 id="v0-1-177-2026-08-11">v0.1.177 — 2026-08-11</h2> <p>_Stage 4 of the shared-schema dogfood: change the schema in ways other than adding a <code>str</code>, and see what the derived machinery does. It found a hole in the mechanism and a soundness hole underneath it._</p> <p>_The mechanism first. <code>claim_with</code> took the JSON shape to write back from the shape already there, which is exact except for an absent optional — <code>null</code> cannot say what it would have held. The first version guessed "string" and named the two fields that needed clearing, and adding a <code>seat: int option</code> to the record broke setting it: the guess produced a string and the decode failed. Where the value cannot say, ask the decoder instead — try the shapes in order and keep the first that survives <code>of_json_opt</code>. Blank text tries <code>null</code> first, which is how an optional is cleared without anything knowing which fields are optional. All four cases now behave: an optional <code>str</code> and an optional <code>int</code> clear to <code>None</code>, a required <code>str</code> blanks to <code>""</code>, a required <code>int</code> to <code>0</code>, and the hardcoded field names are gone._</p> <p>_Underneath it: the probe set every field from text, including the variant-typed <code>status</code>, and <code>"7"</code> was accepted. The interpreter's decoder built a nullary constructor from whatever string arrived without asking whether the variant had a case by that name, so <code>of_json_opt</code> returned <code>Some</code> holding a value outside its own type, which <code>to_json</code> then printed straight back out. Both compiled backends answered <code>None</code> — the reference the parity harness compares against was the one in the wrong, which is why nothing had caught it. The object form was checked for existence but not ownership, letting a payload case of an unrelated variant through; <code>constr_info.type_name</code> closes both, and a case of this variant arriving in the wrong shape (<code>"Rejected"</code> as a bare string when it carries a payload) is refused too._</p> <p>_<code>test/parity/of_json_variant_tag.mere</code> holds it and fails without the fix. It took a dogfood that round-trips a variant field through a form to surface, which is the same shape as v0.1.175's finding: the boundary bugs live where a value is only ever decoded. parity 80/80, ctest 13/13, dune runtest 2308/0, claims browser check 14/14._</p> <hr> <h2 id="v0-1-176-2026-08-11">v0.1.176 — 2026-08-11</h2> <p>_Stage 2 of the shared-schema dogfood: the form is the record._</p> <p>_The plan was to let the app pick the mechanism rather than choose one in advance, and the app picked one that needs no language change at all. A record already has a total, name-keyed view of itself — the one the compiler synthesises for <code>to_json</code>. Reading it back gives the field names in declaration order, and replacing one key and decoding gives a setter. <code>claim_fields</code> / <code>claim_text</code> / <code>claim_with</code> are that, in about forty lines of schema.mere, and the controls on the page are built from the list at startup. index.html now has a <code><div id="form"></code> and no field markup._</p> <p>_Measured the same way as stage 0, by adding a <code>cost_center: str</code> field:_</p> <table> <thead><tr><th>stage 0</th><th>stage 2</th></tr></thead> <tbody><tr><td><code>type claim</code></td><td>edit</td><td>edit</td></tr> <tr><td><code>blank_claim</code></td><td>compile error if omitted</td><td>compile error</td></tr> <tr><td>a rule + <code>claim_problems</code></td><td>silent</td><td>derived</td></tr> <tr><td>the field table in app.mere</td><td>silent</td><td>gone</td></tr> <tr><td>index.html markup</td><td>silent</td><td>generated</td></tr> </tbody></table> <p>_Two edits, in one file, and the compiler forces the second. <strong>Silent sites 3 → 0.</strong> Checked rather than assumed: adding the field and changing nothing else puts a control labelled "Cost center" on the page and round-trips its value to storage. <code>claim_problems</code> is derived too — it walks the same field list and applies <code>rule_for</code>, so a rule is registered in one place instead of two._</p> <p>_Two mechanisms were considered and rejected on evidence. A build-time generator over <code>contrib/parser</code> foundered on the parser itself: the self-hosted parser has no expression-level record literal, field access or record update (<code>ERecordLit</code> and friends are declared in ast.mere and never constructed), so it cannot parse a schema file that also contains rules — the prerequisite is larger than the generator. Compiler-synthesised derive was not needed once the JSON view turned out to be enough._</p> <p>_Two things this does not fix, both named in the source. <code>of_json</code> is not polymorphic — <code>to_json</code> is, but <code>of_json</code> is directed by an annotation at the call site, so <code>claim_with</code> has to name <code>claim</code> and there is one copy per record type. And <code>rule_for</code> still ties a field name to its rule by hand; nothing in a declaration can say which rule judges which field, but nothing checks the names either._</p> <p>_<code>examples/claims/browser_check.mjs</code> covers what a dump cannot: that a generated <code><input></code> behaves like one, that leaving it runs the rule the schema associates with its name, and that the server's own refusals land in the generated error slots. 14/14 in Chrome. parity 79/79, ctest 13/13, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-175-2026-08-11">v0.1.175 — 2026-08-11</h2> <p>_Stage 1 of the shared-schema dogfood: a type reachable only as another type's field is still a type the C backend has to declare._</p> <p>_The C backend decides which structs to emit by walking what the program mentions — the expression tree and the function signatures. Neither walk descended into a record declaration's field types, so:_</p> <pre><code class="language-mere"> type item = { name: str }; type f = { title: str, items: item list, note: str option }; </code></pre> <p>_left <code>item</code>, <code>list_item</code> and <code>option_str</code> undeclared while the emitted code referred to all three. <code>unknown type name 'list_item'</code>._</p> <p>_Two walks were short, and both are fixed the same way. <code>collect_record_names</code> now follows a record's field types when it registers it, so a record reachable only as another record's field element gets declared; and <code>collect_mono_variant_instances</code> walks monomorphic record declarations the way it already walked monomorphic variant declarations, so container specializations reachable only as field types get registered. <code>seen</code> is set before recursing, so a self-referential record terminates._</p> <p>_This had been latent since records and <code>of_json</code> first coexisted, because a program that builds one of those values anywhere registers the type on the way past — and, as the test found the hard way, so does taking one apart: mentioning <code>Some</code> in a pattern is enough. The failure needs container-typed fields that are <strong>only ever decoded</strong>, which is exactly what a program looks like once the schema is shared and the codec is synthesised, since <code>of_json</code> becomes the only producer. examples/claims was written that way and found it; <code>test/parity/of_json_field_only.mere</code> holds it, and reads only the scalar fields on purpose — an earlier draft called <code>list_len</code> and matched <code>Some</code>, and passed on the broken compiler._</p> <p>_examples/claims now emits and compiles as C; what stops a native build is only the two HTTP externs the native runtime does not implement, which is the gap recorded in v0.1.174 and still its own slice. parity 79/79, ctest 13/13, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-174-2026-08-11">v0.1.174 — 2026-08-11</h2> <p>_<code>examples/claims</code>: the shared-schema dogfood, stage 0 — build it the way you would build it today, and count what is written by hand._</p> <p>_An expense claim: title, date, purpose, a list of lines each with a category that may carry its own text, an optional note and approver, a status. Enough type variety that a naive answer to "generate the boilerplate" would not survive it. <code>schema.mere</code> declares the record and the rules, and both replicas import it._</p> <p>_Half the boundary already costs nothing, which is worth stating before the complaint. <code>to_json</code> / <code>of_json</code> are synthesised per type, so neither side writes down a field name for the network — <code>examples/profile</code> has a hand-written <code>serialize</code> and <code>parse_pairs</code>, and this one has neither. The validation functions are the same functions on both sides, so a rule and its message exist once. <code>problem</code> is a declared type, so the server's refusals arrive as records rather than as text to parse, and a server-only rule (over ¥100,000 needs an approver, and the approver must exist) lands in the same error slot a local complaint would._</p> <p>_The measured half: adding a <code>cost_center: str</code> field to <code>claim</code> takes four edits in Mere and one in HTML, and the compiler catches exactly one of them — the record initializer. The rule, the field table and the markup are all silent. Stopping after the record and its initializer leaves a program where both replicas compile, the server stores the field and the browser round-trips it, and it never appears on screen; the rendered page mentions it zero times. That is the number stage 2 exists to move._</p> <p>_Two compiler findings fell out on the way. An extern whose result is <code>unit</code> could not be used as a value at all: <code>unit</code> is Mere's int 0 and C's <code>void</code>, and the closure adapter returned the call directly, so the emitted C returned void from a function declared to return int. That is the shape of middleware — contrib/http's <code>with_arena</code> wraps <code>http_arena_mark</code> — so no native build of a server using it could compile. Fixed, with <code>test/ctests/extern_unit_as_value.mere</code> holding it. And <code>of_json</code> on the C backend does not register container instances that are reachable only through a record field type: a record with both a nested record and a <code>list</code>/<code>option</code> field emits <code>unknown type name 'list_item'</code>. It is latent because a program that constructs one of those values by hand anywhere registers it — which the claims app happens to do, and a generated codec would not. That one is stage 1._</p> <p>_The native HTTP runtime implements six externs, and <code>with_arena</code> / <code>with_access_log</code> need two it does not have (<code>http_arena_mark</code>, <code>http_current_status</code>). So a server that releases its per-request allocations exists on the Wasm host and not the native one. Recorded, not fixed here. parity 78/78, ctest 13/13, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-173-2026-08-10">v0.1.173 — 2026-08-10</h2> <p>_Two bugs behind one build failure: a regression from v0.1.172, and the latent one it exposed._</p> <p>_v0.1.172 left mere-ruby unable to compile — 19 <code>use of undeclared identifier 'mu_..._as_value'</code> — while parity, ctest and the OCaml suite all stayed green. Both harnesses that compare stdout are blind to a program that never links, and <code>scripts/ctest.sh</code>, which does invoke the C compiler, had not been run._</p> <p>_The regression: in codegen_c the two arms that made a direct call to a top-level or inner-lifted fn were folded into the new shadowing guard. That guard is position-aware — a builtin used above a later same-named binding is still the builtin — so any call it declined fell through to the closure path instead of the direct one. Which of the three call shapes to use is a question about what the name is, not about where in the file the caller sits, so the fallthrough arm is back. (LLVM and Wasm were never affected: their equivalent arms stayed in the match as a catch-all after the builtin arms.)_</p> <p>_The latent bug that made it fatal: a polymorphic fn used at more than one type is emitted once per instantiation under a mangled name, and <code>_as_value</code> closure wrappers are only defined for those. The unmangled name stays registered in <code>toplevel_fn_names</code> so source-level call sites still dispatch — so value position asked for <code><base>_as_value</code>, which is never defined. <code>let ident = fn (x) -> x</code> used at two types and then passed as a value has never compiled on the C backend; it now picks the instance from the reference's own type, the same way the direct-call path does. Wasm refuses this case, which is honest — it has no single function to hand out either — but said "unbound variable" for a variable that is plainly bound, and now names the actual limitation._</p> <p>_<code>test/parity/multi_inst_as_value.mere</code> covers it. A parity case rather than a ctest because Wasm's refusal is a documented UNSUP there, and because parity reports a backend whose output will not compile as MISCOMPILE — which is exactly the signal that was missing. mere-ruby builds again and its corpus is 51/51 against ruby. parity 78/78, ctest 12/12, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-172-2026-08-10">v0.1.172 — 2026-08-10</h2> <p>_Shadowing a builtin, in general — the family the <code>join</code> fix in v0.1.169 was one member of._</p> <p>_Each backend dispatches on builtin names in about a hundred match arms, and each arm decided on its own whether to ask if the program had bound that name itself. Those questions had been added one incident at a time, after someone was bitten: C had guarded 39 of 95, Wasm 14 of 139, LLVM 6 of 70. The rest were silent. <code>let str_len = fn (s: str) -> 999</code> returned 999 on the interpreter and 5 on all three compiled backends — no error, no warning, a different answer. Both a top-level and a local binding were affected._</p> <p>_Each backend now asks once, before any builtin arm can match: if the head of the application spine is a name the program bound, the call goes to the ordinary call paths — inner-lifted fn, top-level fn, or closure value — and never meets the builtin arms. Those three paths were already contiguous at the end of each backend's App group, so they lifted out into one function (<code>emit_user_app</code>, and a trio of local helpers in codegen_c) with nothing duplicated. Safe by construction: the guard is false unless the program actually bound the name, so a program that shadows nothing reaches exactly the arms it reached before._</p> <p>_The first version of the guard was wrong in a way worth recording. It asked whether a name was bound <strong>anywhere</strong> in the program, but top-level bindings are sequential — the typer rejects a forward reference — so a builtin used above a later same-named binding is still the builtin. Under the first version a helper written before <code>let show = ...</code> silently started calling the user's show. Each backend now records the declaration position of every top-level fn and the guard compares against the body being emitted, with <code><=</code> so a recursive fn still counts as binding its own name. Wasm needed the position question asked ahead of its name-only tables (<code>fn_closure_table_idx</code>, <code>top_globals_wasm</code> also hold top-level fn names), and LLVM needed the cursor reset for main's body, which it emits without a host-scope switch._</p> <p>_<code>test/parity/shadow_builtin.mere</code> covers the shapes the guard has to recognise separately: bound at top level, locally, or as a capturing inner fn that gets lifted; called with one argument or three (the name three levels down a spine); used as a value rather than called; and the ordering rule. parity 77/77, dune runtest 2308/0._</p> <p>_Swept up while here: <code>file_size</code> / <code>file_pread</code> / <code>file_pwrite</code> left the LLVM "no lowering yet" list they had been on since v0.1.163, the stale claim in <code>test/parity/file_pio.mere</code> that LLVM refuses positioned I/O, and the example count in <code>mere --help</code> (118 → 282)._</p> <hr> <h2 id="v0-1-171-2026-08-10">v0.1.171 — 2026-08-10</h2> <p>_contrib/state: the one-slot-vec trick, named — and the thing naming it does not fix._</p> <p>_Mere has no mutable cell, and every browser client built one the same way: a vec allocated once, pushed once, then read and written at index 0. It works — the bump arena is page-lifetime, so the slot survives across event firings — and by v0.1.170 there were ten of them across four apps, each with a comment explaining the trick. <code>contrib/state/cell</code> names it: <code>cell_new</code> / <code>cell_get</code> / <code>cell_set</code>, three lines over the same vec._</p> <p>_That is the smaller half, because the noise was never the real problem. <code>examples/profile</code> shipped with three of those cells and seven hand-written calls to a recompute function, one after each mutation site; the seventh was added during debugging, and the eighth would have been a screen quietly disagreeing with the state behind it. <code>contrib/state/store</code> holds one value and a list of watchers, and <code>store_update</code> writes and then tells them. The seven calls became zero, and the app's three cells became one store._</p> <p>_Making the screen derived forced the app to be honest about something the first draft had fudged. Focusing a field takes its complaint off the screen but does not make the value right — Save has to stay withheld until the field is left and re-judged. The ad-hoc version got that by writing to the DOM behind the state's back, which worked only because nothing else ever repainted. The model now carries "what is wrong" and "what we are currently saying out loud" as two separate facts, which is what they always were._</p> <p>_<code>examples/tasks</code> keeps cells and no store, deliberately: its screen is not derived from its state but reconciled against it — rows that stopped matching are removed one at a time so the row being typed into is never rebuilt — and a watcher that redrew on every write would destroy the one thing that app exists to protect. Two of its four cells are a timer handle and a retry delay that nothing renders at all._</p> <p>_chat, tally and tasks moved to <code>cell</code>; profile moved to <code>store</code>. All four still pass their harnesses (chat and tally and tasks headless, profile 15/15 in Chrome). <code>test/parity/store_watch.mere</code> locks watcher order, the immediate first run, read-modify-write, and two stores at different types across all four backends. parity 77/77, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-170-2026-08-10">v0.1.170 — 2026-08-10</h2> <p>_A form, as opposed to a list — and the six bindings that separate the two._</p> <p>_The three browser clients before this one were lists. A list is edited one item at a time, every keystroke is worth acting on, and the only question the UI ever asks is "what is on screen". <code>examples/profile</code> is a settings form, which is a different animal: it is a set of fields with a shape. It is valid or it is not. It differs from what was loaded or it does not. Answering either question means holding two versions of the same record at once — what the server confirmed and what the user has done since — and comparing them. The change count, whether Save is offered, what Revert restores: all three are derived from that pair, none of them is a flag anyone sets._</p> <p>_Six bindings fall out, and each is here because the form cannot be written without it. <code>dom_on_blur</code> and <code>dom_on_focus</code> are what make validation feel like a form rather than a nag — judge the field when the user leaves it, take the complaint back down when they return to fix it; checking on every keystroke tells someone their email is invalid while they are still typing the part before the <code>@</code>. <code>dom_on_change</code> is the only event a <code><select></code> produces, so without it the theme picker is inert. <code>dom_checked</code> / <code>dom_set_checked</code> exist because a checkbox has no useful <code>value</code> — its meaning is <code>el.checked</code>, a property that is not the attribute of the same name. And <code>dom_remove_attr</code> closes a hole <code>dom_set_attr</code> left in v0.1.152: a form could disable its save button while the input was invalid and then never enable it again._</p> <p>_The fields are described once — id, label, a getter, a setter, a check — and everything else loops over that list: painting, comparing, validating, reverting. Record fields cannot be named at runtime, so without the get/set pair each of those loops would have been written out once per field._</p> <p>_The server rejects things on purpose. The client checks what it can see; the server also enforces a rule the client cannot know (a reserved display name) and answers 422 with per-field messages, which land in the same error state a local complaint does. <code>run_dom_headless.mjs</code> gained <code>--pick</code> and <code>--check</code> for the two controls that are not text, <code>removeAttribute</code> and <code>checked</code> in its DOM stub, and <code>value</code> / <code>checked</code> in its dump — a filled-in form used to render identically to an empty one. <code>examples/profile/browser_check.mjs</code> covers what the stub cannot: it can fire a blur, but only a browser can cause one. 15/15 in Chrome, parity 76/76, dune runtest 2308/0._</p> <p>_Correction: the version bump in v0.1.169 left <code>test/test_basic.ml</code>'s version assertion pinned at 0.1.151, so that slice's suite was red as committed. Fixed here._</p> <hr> <h2 id="v0-1-169-2026-08-10">v0.1.169 — 2026-08-10</h2> <p>_<code>args ()</code> on LLVM — the last reason a Mere CLI ran on three backends out of four — and the shadowing hole finding it exposed._</p> <p>_The B+-tree in <code>mbtree</code> has run on all four backends since v0.1.163, when positioned file I/O landed on LLVM. Its command line had not: <code>args</code> was still interp + C, so <code>mere -ll</code> refused the program that wrapped the store. LLVM now stores <code>main</code>'s argc/argv into globals and folds them into a <code>str list</code>, dropping the program name, which is what interp and C hand back. <code>main</code> keeps its no-argument signature unless the program actually asks for argv. The strings are argv's own rather than copies into the current region: a str is a plain NUL-terminated pointer on this backend and argv outlives the program, so there is nothing to allocate and nothing that can outlive what it points at._</p> <p>_Writing the parity case for it turned up something worse. The test defined a <code>join</code> helper — the obvious name for joining strings — and LLVM lowered the call to <code>pthread_join(i64)</code> and handed it a <code>str list</code>. <code>join</code> is also the thread builtin, and LLVM guarded <code>str_eq</code> / <code>is_digit</code> / <code>is_alpha</code> / <code>is_space</code> against a user's same-named binding but not <code>join</code>, which C has guarded since Phase 30.0 with a comment predicting exactly this. The guards had been added one incident at a time; they now go through one <code>user_shadows_llvm</code> that asks about locals, lifted inner fns and top-level fns alike, the way C's <code>user_shadows</code> does. <code>test/parity/shadow_builtin.mere</code> locks it down, and docs/reserved-names.md gained the section distinguishing this axis — a name Mere owns — from the C-symbol collisions the rest of that document is about._</p> <p>_<code>mbtree data.db set 42 100</code> / <code>get</code> / <code>selftest</code> now produce identical output on interp, C, LLVM and Wasm. parity 76/76, dune runtest 2308/0. <code>mere -v</code> also reports the truth again: <code>lib/version.ml</code> had been left at 0.1.151 while the changelog ran to 0.1.168._</p> <hr> <h2 id="v0-1-168-2026-08-10">v0.1.168 — 2026-08-10</h2> <p>_A list you can filter and edit, and the three bindings it forced._</p> <p>_Every browser client so far only appended, and a list that only grows can be redrawn wholesale: <code>dom_set_text el ""</code> drops every child and the rest is rebuilt from state. <code>examples/tasks</code> cannot. Each row holds an <code><input></code> you type into, so rebuilding the list while you are editing takes the field out from under the caret — which makes three things necessary rather than convenient. <code>dom_remove</code> detaches one node and leaves its siblings, and their focus and half-typed text, alone. <code>dom_on_input</code> fires on every keystroke, so the filter can narrow as you type instead of on submit. <code>dom_set_timeout</code> / <code>dom_clear_timeout</code> do two jobs: a keystroke cancels the pending filter and queues a new one, so a burst of typing costs one re-render rather than one per character; and a save that fails comes back on a doubling delay instead of being dropped._</p> <p>_Filtering reconciles rather than redraws: rows that stopped matching are removed one at a time, rows that started matching are built and appended, and a row that stays is never touched. The browser check asserts exactly that — after typing in the search box, the row being edited is the same DOM node with its caret still at offset 3 — along with delete removing exactly one row, and a save failing and landing on the retry. All nine pass in Chrome._</p> <p>_<code>examples/tasks/server.mere</code> is deliberately thin: contrib/store/kvlog behind one tab-separated mutation endpoint, plus <code>POST /api/flaky</code> to fail the next N saves on purpose, because a retry path that is never taken is a path that is never tested. <code>scripts/run_dom_headless.mjs</code> gained <code>--type <id>=<text></code>, which sets a field and fires <code>input</code> the way a keystroke does, and its DOM stub now implements <code>remove()</code>. parity 74/74, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-167-2026-08-10">v0.1.167 — 2026-08-10</h2> <p>_An open region is closed to the heap before codegen, which unblocks the last LLVM gap the dogfoods hit._</p> <p>_<code>vec_new</code> and <code>file_pread</code> hand back a <code>Vec[R, T]</code> whose region no <code>region</code> block ever constrained, and the typer is content to leave R open. That is harmless on the C backend, which erases types, and fatal on LLVM, which does not — and the failure arrived three removes from its cause. An open R makes the Vec non-concrete; a helper taking one is therefore not resolvable and gets dropped from the backend's function list; the inner fn that uses it treats it as a capture instead of a known top-level name; and the emitted call site refers to an SSA value that does not exist. What clang reported was a type mismatch on a register several functions away._</p> <p>_A pass before lifting closes any open region variable to the default heap region. That is completing the inference rather than working around it, which is what keeps every downstream name stable — an earlier attempt tagged open regions leniently instead, and made a struct's name depend on how far unification had progressed, so the same type was registered under one name and referred to under another._</p> <p>_The B+-tree from the mbtree dogfood now compiles and runs on LLVM, and a tree it writes there reads back correctly under the C backend. <code>test/parity/lifted_vec_capture.mere</code> locks the shape. mbtree's CLI still needs <code>args()</code>, which has no LLVM lowering. parity 74/74, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-166-2026-08-10">v0.1.166 — 2026-08-10</h2> <p>_Two corrections to the capture typing added in v0.1.165, and a precise account of what still blocks mbtree on LLVM._</p> <p>_<code>ty_is_concrete</code> is the wrong question to ask about a capture. A <code>Vec[R, int]</code> whose region is still a variable is not concrete, yet <code>llvm_ty_of</code> lowers every Vec to <code>ptr</code> and never looks at the region — so rejecting it left the capture untyped. The lookup now asks whether the backend can represent the type at all._</p> <p>_v0.1.165 also fell back to searching every top-level fn body for a concrete type of the captured NAME. Names are not unique across functions, so a capture of <code>b : Vec[R, int]</code> picked up an unrelated <code>b : int</code> from elsewhere in the program and was declared <code>i64</code>. Removed — a wrong type is worse than no type, and the loud error is what should happen._</p> <p>_What still blocks mbtree, stated exactly: <code>get_i8</code>, a top-level curried helper, is not among the backend's <code>fn_decl</code>s, so the free-variable scan treats it as a capture of the inner fn that uses it. Its type carries an unresolved region, and the emitted call site refers to <code>%get_i8</code> — an SSA value that does not exist, because the value is a top-level function. Two things would have to change: the name would have to be known as a top-level binding so it is never captured, or a captured top-level function would have to be materialised at the call site._</p> <p>_A fix was attempted and backed out: tagging the region position of <code>Vec</code> / <code>Map</code> / <code>StrBuf</code> leniently makes a struct's name depend on how far unification has progressed, so the same type got registered under one name and referred to under another, and <code>test/parity/bytes_typed_fn_unused.mere</code> regressed to <code>llvm:MISCOMPILE</code>. Any real fix has to keep the tag stable. parity 73/73, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-165-2026-08-10">v0.1.165 — 2026-08-10</h2> <p>_A capture the LLVM backend cannot type is refused instead of guessed._</p> <p>_When an inner fn is lifted, each captured variable becomes a parameter, and the capture's type came from the first <code>Var</code> occurrence inside the fn body whose recorded type was fully concrete. When none was, the search fell through to its initial value — <code>TyUnit</code>, which lowers to <code>i64</code>. So a capture the backend could not type was silently declared <code>i64</code> while the call site passed a pointer, and the failure surfaced as a clang type error about an SSA register, several steps from the cause. That is what stopped the mbtree dogfood from building here._</p> <p>_The lookup now prefers a concrete type, falls back to any recorded one, and takes the binding site's type when a later use has been generalized; failing all of that it raises a Mere codegen error naming the variable. mbtree's real blocker is now stated plainly: <code>unsupported LLVM codegen type element: 'a</code> — capturing a <strong>polymorphic</strong> value, which this backend cannot represent and the lifting code already says so in a comment. The monomorphisation there covers a local <code>let rec</code> applied at one type; a polymorphic top-level helper captured by an inner fn is still open._</p> <p>_parity 73/73, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-164-2026-08-10">v0.1.164 — 2026-08-10</h2> <p>_A partially applied extern is a value, not a call._</p> <p>_The Wasm extern path collapses a curried <code>App</code> chain into one <code>call $name</code>, taking the argument count from the call site rather than the declaration. That is right when the application is saturated and emits a call with the wrong arity when it is not, so <code>worker_call req</code> on a two-argument extern produced WAT that wat2wasm rejected outright. Found while writing contrib/async, where the whole premise is that an extern with its request applied is already a Task — the tally client had to wrap every one in <code>fn (cb) -> … cb</code>._</p> <p>_Under-application now eta-wraps: the missing arguments become a lambda and the expression routes through the ordinary anonymous-closure path, so the partial application is a closure like any other value. The wrapper is gone from examples/tally/app.mere. Two regression tests pin both halves — a partial application reaches its callee through <code>call_indirect</code>, a saturated one still emits a direct <code>call</code>. parity 73/73, dune runtest 2308/0._</p> <hr> <h2 id="v0-1-163-2026-08-10">v0.1.163 — 2026-08-10</h2> <p>_Positioned file I/O on LLVM, closing the last backend gap in that group._</p> <p>_<code>file_openrw</code> / <code>file_size</code> / <code>file_pread</code> / <code>file_pwrite</code> / <code>file_fsync</code> / <code>file_close</code> were interp + C + Wasm; parity reported <code>llvm:UNSUP</code> for <code>test/parity/file_pio.mere</code>. The LLVM runtime now implements them over libc with the same contract as everywhere else — a handle, bytes crossing as a <code>Vec[int]</code>, a short read past EOF rather than a padded one — and that case is <code>llvm:MATCH</code>. All four backends agree on writing past the end, overwriting a window in the middle, and reading a window back after a reopen._</p> <p>_A <code>File</code> travels as an i64 here rather than a raw <code>ptr</code>: lifted inner functions type every parameter uniformly, so a pointer could not be passed through one. The runtime converts at its own boundary._</p> <p>_Not fixed, and worth naming because it is what stops the mbtree dogfood from building on LLVM: a lifted inner function declares all its parameters <code>i64</code>, so passing a <strong>closure</strong> to one fails to typecheck in the emitted IR. <code>args()</code> also has no LLVM lowering. Neither is about files — mbtree hits both — but they are the two things between this backend and running that dogfood. parity 73/73, dune runtest 2306/0._</p> <hr> <h2 id="v0-1-162-2026-08-10">v0.1.162 — 2026-08-10</h2> <p>_<code>mere install</code> fetches before checking out._</p> <p>_A dependency's repo is cloned once and cached, keyed by repo-and-rev, but an existing cache was never refreshed. So the first install after a new commit was pushed failed with <code>fatal: reference is not a tree: <sha></code>, and the only remedy was deleting <code>~/.mere/cache</code> by hand — a git error with no mention of a cache, a long way from the cause. It now fetches before checkout._</p> <p>_Found while repointing the mere-blog dogfood at a current revision, which also repaired its packaged path: its host was pinned to a release whose JS glue predates the current value representation, so a build made with a current compiler linked and then failed on its first request. With everything pinned to one commit, <code>mere serve</code> runs that app on the vendored host end to end._</p> <hr> <h2 id="v0-1-161-2026-08-10">v0.1.161 — 2026-08-10</h2> <p>_Sequencing for callback-shaped work, and what it does not fix._</p> <p>_Everything that crosses a thread or a network hands its result to a closure. One such call costs an indentation level, which is nothing. The cost shows up when steps depend on each other: an ordinary fold over a list where each element is a round trip cannot be written as a fold, and becomes a recursion carrying its own continuation. The tally client had exactly that recursion written out by hand, and so would every app that sequenced two requests._</p> <p>_<code>contrib/async/async.mere</code> carries it once. A Task is <code>(str -> unit) -> unit</code>; <code>async_each</code> is the fold, <code>async_map</code> collects results in list order rather than completion order, <code>async_then</code> chains, <code>async_all</code> fans out and joins. It is ordinary Mere — no language support, just the callback shape given names — and <code>test/parity/async_combinators.mere</code> pins the ordering across all four backends. examples/tally/app.mere now uses it and the hand-rolled recursion is gone._</p> <p>_What it does not fix is the nesting: <code>async_then</code> still puts each step inside the previous one's closure, so the four-step sync in tally is four levels deep. Removing that needs syntax, not a library — some form of do-notation or await — and shipping the combinators first is how to find out how much of the pain was the fold and how much is the nesting. On the evidence of one app: the fold was the part worth removing, and the nesting is legible enough to live with for now._</p> <p>_Found while writing it: an extern cannot be partially applied on the Wasm backend. <code>worker_call req</code> emits a direct one-argument call rather than a closure, so a Task built from an extern still needs an explicit <code>fn (cb) -> extern req cb</code>. Worth eta-wrapping the way nullary builtins already are._</p> <hr> <h2 id="v0-1-160-2026-08-10">v0.1.160 — 2026-08-10</h2> <p>_A request's allocations can be released when the request ends._</p> <p>_The default region is a bump arena with no free, so a long-lived server keeps every request's working memory forever — the parsed body, the strings a handler concatenated on the way to a response, and every string the host wrote in. A handler that builds a 200-piece string cost 162,840 bytes per request, none of it reclaimed. The dogfoods noted this as a constraint twice without measuring it; the number above is a 300-request run against a Node host, reading the module's own <code>__lang_bump</code>._</p> <p>_<code>contrib/http/arena.mere</code> marks the arena on the way into a request, and the glue rewinds it once the response has been copied out of linear memory. Same handler, same run: <strong>24 bytes per request</strong>, and 200 consecutive responses are byte-identical, so nothing is reclaimed early. A plain <code>region</code> block around the handler gets most of the way there on its own (162,840 → 722, the copied-out response), which is worth knowing when no host cooperation is available._</p> <p>_It is opt-in, and the reason is the rule it depends on: nothing a handler allocates may outlive its response. That holds for ordinary request/response work and fails for a handler that stashes a value in something longer-lived — an in-memory session store, a cache, a subscriber list. mere-blog is exactly that case and deliberately does not use it; <code>examples/tally/server.mere</code> keeps all its state in a log file and does._</p> <p>_parity 72/72, dune runtest 2306/0; the tally client still syncs against the wrapped server._</p> <hr> <h2 id="v0-1-159-2026-08-10">v0.1.159 — 2026-08-10</h2> <p>_<code>args()</code> returns the arguments on a plain Wasm host._</p> <p>_The runners have supplied <code>arg_count</code> / <code>arg_get</code> for a long time; the builtin was never wired to them and returned Nil unconditionally, so a CLI compiled to Wasm silently saw no arguments while the same source on C saw them all — a disagreement with no error anywhere. It now builds the list from the host, and a host that reports 0 yields Nil, which is what the hardcoded empty list got right for a browser. contrib/dom answers 0 so a page keeps the behaviour it had._</p> <p>_Verified by running the same program on both backends with arguments: <code>foo bar</code> gives <code>n=2 [foo] [bar]</code> on C and on Wasm. parity 72/72, dune runtest 2306/0._</p> <hr> <h2 id="v0-1-158-2026-08-10">v0.1.158 — 2026-08-10</h2> <p>_One definition of the host boundary, instead of five._</p> <p>_v0.1.157 added a number so a host and a module could refuse each other. This removes the reason they drifted in the first place: <code>readCStr</code>, <code>writeStr</code>, <code>bumpAlloc</code> and the closure caller existed in five separate copies, and a fix to one never reached the others. <code>run_wasm.js</code> grew the string length header and open-coded it at four call sites; contrib/dom learned the i64 closure convention and contrib/http did not; a fixed 4KB scratch window outlived the move to the shared heap in exactly one glue. Each was a wrong answer at runtime, never a link error._</p> <p>_<code>scripts/mere_host.js</code> now holds all of it — <code>makeMarshal</code> for the four value operations plus <code>writeBytes</code> / <code>readBytes</code> / <code>copyToStr</code> for the byte buffers a driver moves, and <code>makeClosureCaller</code> for dispatch — with the layout written down once at the top. <code>run_wasm.js</code>, <code>run_http_server.js</code>, <code>pg_env.js</code> and <code>contrib/http/http.glue.js</code> all use it: 251 lines deleted, 48 added._</p> <p>_<code>contrib/dom/dom.glue.js</code> keeps its copy, because it is an ES module a browser fetches and cannot require the shared one. That is now the only place the layout appears twice, so <code>scripts/check_host_abi.js</code> checks it: the ABI constants must agree, and the copy must still write the length header, return a pointer past it, and pass closure arguments as BigInt. Each of those three was wrong in some host at some point._</p> <p>_parity 72/72, dune runtest 2306/0; the chat, tally and mere-blog apps all rebuild and pass their checks. The ABI guard earned itself immediately — a stale mbtree build from before v0.1.157 was refused by name instead of quietly returning empty strings._</p> <hr> <h2 id="v0-1-157-2026-08-10">v0.1.157 — 2026-08-10</h2> <p>_An ABI number, so a host and a module can refuse each other._</p> <p>_Nearly every bug the three dogfoods turned up was one shape: a boundary written when a Mere value was 4 bytes and a <code>str</code> was a plain C string, left behind when both changed. A host and a compiled module agree on far more than the import list — the value representation, the string layout, how a closure is called — and none of it is checked, so an older host links cleanly and then returns empty strings or crashes on the first request. That is how a request line arrived as "", how a password hash hashed to nothing, and how a result set came back with every column empty._</p> <p>_Every module now exports <code>__mere_abi</code>, and every host checks it before touching the instance: <code>scripts/mere_abi.js</code> for the Node runners and contrib/http, inlined in contrib/dom since it loads in a browser. A module without the global is refused as pre-ABI-1 with a note to recompile; a newer one tells the host to update. The self-host emitter in <code>contrib/codegen/codegen_wasm.mere</code> emits the same global, so both codegens stay in step and the bootstrap fixpoint still holds._</p> <p>_ABI 1 names what was implicit: i64 values with addresses in the low word, <code>[i32 len][bytes][NUL]</code> strings whose value points at byte0, closure records of <code>{ i32 env, i32 fn_idx }</code> called as <code>(i64, i64) -> i64</code>, 8-byte compound fields and 16-byte <code>{ tag, payload }</code> variant cells._</p> <p>_An audit of the remaining host boundaries found three more of the same shape, all in <code>run_http_server.js</code> and all fixed by routing through the shared <code>writeStr</code> rather than open-coding the layout a fourth time: <code>getenv</code>, <code>sha256_hex</code> and <code>__lang_str_of_float</code>. Open-coding is precisely how these drifted — <code>run_wasm.js</code> grew the length header at each of its own call sites and none of the copies elsewhere followed. parity 72/72, dune runtest 2306/0._</p> <hr> <h2 id="v0-1-156-2026-08-10">v0.1.156 — 2026-08-10</h2> <p>_<code>of_json</code> gets a working Wasm backend, and the last of the raw-C-string handoffs go._</p> <p>_<strong>The Wasm JSON decoder was written for the 4-byte value model.</strong> The parser runtime carried i64 signatures over i32 bodies, and the generated <code>__ojnode_*</code> decoders built records with 4-byte fields and variant cells of 8 bytes, so typed request decoding had no Wasm backend at all. The runtime's JSON tree is private to the parser and now says so — i32 throughout — while every decoder produces a real Mere value: 8-byte record and tuple slots, 16-byte <code>{ tag, payload }</code> cells, and <code>__mj_atoi</code> accumulating in i64 so a number past 2^53 survives. <code>test/parity/of_json_composite.mere</code> covers records, lists, options (including None-on-error), nested lists of records, variants both nullary and payload-carrying, and the wide integer._</p> <p>_<strong>`show` and `to_json` on C returned bare string literals</strong> for bool, unit, closures, the empty list, nullary constructors and every <code>null</code> — and a bare literal is not a Mere str, which carries its length in the word before byte0. <code>print (show true)</code> looked fine because print formats with %s and stops at the NUL; <code>print ("x" ++ show true)</code> segfaulted, and <code>str_len (show true)</code> returned whatever preceded the literal in rodata. Same for <code>str_repeat</code> at n≤0, <code>str_join</code> of an empty list, and <code>__lang_fail_str</code>._</p> <p>_<strong>Two Wasm hosts still handed over raw bytes</strong>: <code>mem_to_str</code> in <code>scripts/pg_env.js</code>, which is where every column value a database driver reads off the wire becomes a str — an entire result set came back empty — and <code>read_file</code> in <code>scripts/run_http_server.js</code>. And <code>contrib/http/http.glue.js</code> read the response body by scanning for a NUL, so a <code>.wasm</code> asset served out of <code>read_file</code> truncated to nothing at its first byte._</p> <p>_Together these close the gap the previous entry left open: mere-blog now builds and runs on <strong>both</strong> backends against Postgres — signup, cookie sessions, typed request decoding, authenticated post creation — and serves the same Wasm admin client byte-identically from either. The admin UI's browser checks pass against the native binary and the Wasm host alike. parity 72/72, dune runtest 2306/0._</p> <hr> <h2 id="v0-1-155-2026-08-10">v0.1.155 — 2026-08-10</h2> <p>_Four boundaries that a database-backed web app walks straight into, found by building an admin UI for the mere-blog dogfood._</p> <p>_<strong>`to_json` on Wasm</strong> had the same rot <code>show</code> did at v0.1.153: every case of the emitter built 4-byte cells with i32 fields and kept the accumulator str in an i32 local, so a module that serialized anything was rejected by wat2wasm. <code>to_json_int</code> also read its i64 argument through i32 comparisons, so sign and magnitude were wrong past the low word. mere-blog serializes a typed record on every response, so the whole Wasm backend was unavailable to it. <code>test/parity/to_json_composite.mere</code> covers it._</p> <p>_<strong>The native runtime handed Mere raw C strings.</strong> A Mere <code>str</code> is <code>[size_t len][bytes][NUL]</code> with the value at byte0, so <code>__lang_str_size</code> reads the length from <code>s[-1]</code> — and the native HTTP server passed a <code>static char[]</code> request line straight to the handler, which read back as "". Every route on a native build 404'd. Same for <code>http_current_body</code> and <code>http_get_header</code>, and for the crypto/encoding externs: <code>__to_hex</code> / <code>__to_b64</code> / <code>gen_request_id</code> malloc'd their results, so <code>sha256_hex</code> returned "" and every password hash with it. They now allocate through <code>__lang_str_alloc</code>._</p> <p>_<strong>A `unit` parameter broke extern closure adapters on C.</strong> <code>extern fn f: unit -> str</code> lowers to <code>str f(void)</code>, but the adapter that lets it be used as a value passed its argument through, calling a 0-arity function with one._</p> <p>_<strong>The native HTTP response measured its body with `strlen`.</strong> A Mere str carries its length and may contain NULs, so a <code>.wasm</code> read through <code>read_file</code> was truncated at its first zero byte — a native Mere server could not serve its own compiled client. It now uses <code>__lang_str_size</code>._</p> <p>_Together those let mere-blog build and run natively against Postgres with a current compiler — signup, cookie sessions, authenticated post creation — and serve a Wasm admin client whose form validates drafts with the same <code>validate.mere</code> the server enforces, compiled to Wasm on one side and C on the other. parity 71/71, dune runtest 2306/0._</p> <p>_Known and not fixed: <code>of_json</code> on Wasm is still entirely 4-byte-model, runtime and generated decoders both, so typed request decoding has no Wasm backend until that is rebuilt. Native and interpreter are unaffected._</p> <hr> <h2 id="v0-1-154-2026-08-10">v0.1.154 — 2026-08-10</h2> <p>_A local-first app in Mere, split across three replicas of one store. <code>contrib/store/kvlog.mere</code> is an append-only key/value log over the positioned file I/O from v0.1.153: a write appends a record and fsyncs, a read replays. <code>examples/tally</code> runs it three ways — <code>store.mere</code> owns the browser's copy off the UI thread, <code>server.mere</code> owns the authoritative copy, and both <code>import</code> the same kvlog source, so the two replicas are one store compiled twice rather than two stores that agree on a format. A log written by the C backend reads back under Wasm and interp, and the reverse. <code>file_size</code> joins the positioned group on Wasm, since an append-only store needs the end of the file without reading it._</p> <p>_The UI half (<code>app.mere</code>) reaches storage through a new <code>worker_call</code> binding in contrib/dom: a request string in, a reply handed to a closure later. In a browser that is postMessage to a Worker, which is where the store has to live because an OPFS access handle — the only synchronous positioned file I/O a browser offers — exists only off the main thread. <code>scripts/run_dom_headless.mjs --worker <store.wasm></code> runs the same split under Node with replies deferred to a later turn, so the asynchrony is faithful and the whole app is testable without a browser: add a counter, reopen in a fresh process and see it persisted, press +1, sync against a running server, and lose the server and watch it fall back to local. <code>examples/tally/store.worker.js</code> carries the OPFS binding, and <code>scripts/check_browser.mjs</code> drives it in Chrome: write through the store, reload, restart the browser process, and confirm the counters come back off disk. Playwright is not a dependency, so a missing install is a SKIP. The check also confirms <code>createSyncAccessHandle</code> is absent on the main thread, which is the constraint the whole split exists for. A log written by the browser through OPFS parses identically under <code>kvlog.mere</code> compiled to C and run natively, and compiled to Wasm and run under Node — one source, three hosts, one byte format._</p> <p>_What the split says about the language. Every endpoint is straight-line code — <code>handle</code> in store.mere returns its reply as a value and never mentions asynchrony — and every crossing is a callback. With one request in flight that costs an indentation level, which is what the chat client measured in v0.1.152. The cost shows up when steps depend on each other: sync reads local state, fetches remote, writes the merge back key by key, then publishes it, and the middle step is an ordinary fold over a list where each element is a round trip. Written against callbacks it becomes a recursion that carries its own continuation and calls it when the list runs out. That rewrite — a fold that cannot be a fold — is the clearest argument so far for giving Mere a way to name the result of a call._</p> <p>_Also found: a <code>let rec</code> that closes over a Vec is rejected by both compiled backends ("captured variable has no recorded type" on C, "inner-lifted capture not in scope" on Wasm), so kvlog threads its byte buffer through as an explicit parameter. And <code>dom_set_text el ""</code> turns out to be the way to clear a container, since setting textContent drops every child — no <code>dom_remove</code> binding needed yet._</p> <hr> <h2 id="v0-1-153-2026-08-09">v0.1.153 — 2026-08-09</h2> <p>_Positioned file I/O on the Wasm backend, and the three broken builtins that finding it uncovered. <code>file_openrw</code> / <code>file_pread</code> / <code>file_pwrite</code> / <code>file_fsync</code> / <code>file_close</code> were interp + C only, on the reasoning that Wasm has no filesystem. That is true of the browser main thread and false of a Worker, which gets synchronous positioned read / write / flush from an OPFS access handle — the same contract these builtins already describe. They now lower to host imports, with bytes crossing in the <code>mere_bytes</code> layout the <code>read_file_bytes</code> path already uses, so there is no per-byte host crossing and the host never needs to know the Vec layout. <code>scripts/run_wasm.js</code> backs them with positioned <code>fs</code> calls against a handle table._</p> <p>_The result: mbtree — a persistent B+-tree written against those builtins — compiles to Wasm unchanged and passes its durability selftest (20 keys, node splits, a root split, fsync, close, reopen) on interp, C and Wasm alike. A tree file written by the C backend reads back correctly under Wasm and interp, and the reverse, so the on-disk format is one format across backends rather than three that happen to agree._</p> <p>_Getting there needed three fixes to builtins that were stale for the i64 value model and that nothing exercised. <code>show</code> over any composite type emitted WAT that wat2wasm rejected — the tuple, record, variant and list emitters all still built 4-byte cells with i32 fields and kept the str accumulator in an i32 local — so <code>print (show [1, 2, 3])</code> could not be assembled at all. <code>vec_to_list</code> on Wasm had the same rot. On LLVM, <code>vec_to_list</code> stored the payload tuple into the node by value, but the Phase 24 variant layout makes that field a pointer, so 16 bytes went into an 8-byte slot and the helper segfaulted for any input. <code>test/parity/show_composite.mere</code> and <code>test/parity/file_pio.mere</code> cover both gaps; the existing 68 parity inputs only ever showed scalars and never opened a file handle. Full suite: parity 70/70, dune runtest 2306/0._</p> <p>_Known, not fixed: <code>args()</code> on the plain Wasm backend is hardcoded to the empty list even though <code>run_wasm.js</code> supplies <code>arg_count</code> / <code>arg_get</code>, so a CLI program silently sees no arguments there while the C backend sees them. Correct for a browser host, wrong under Node._</p> <hr> <h2 id="v0-1-152-2026-08-09">v0.1.152 — 2026-08-09</h2> <p>_The chat client rewritten in Mere, and the four stale host boundaries it exposed. <code>examples/chat/app.mere</code> replaces the 74 lines of hand-written JavaScript that <code>examples/http_chat.mere</code> used to serve, so both halves of the demo are now Mere and both share <code>contrib/http/escape.mere</code> for JSON escaping — one implementation compiled to C on the server and Wasm in the browser. <code>contrib/dom</code> gains 12 externs in the three groups a document-shaped app needs and a game does not: element construction (<code>dom_create</code> / <code>dom_append</code> / <code>dom_set_attr</code> / <code>dom_set_value</code> / <code>dom_scroll_to_end</code> / <code>dom_on_submit</code>), request/response (<code>dom_fetch</code> blocking + <code>dom_fetch_async</code> callback, sharing <code>dom_fetch_status</code> / <code>dom_fetch_header</code>), and server push (<code>dom_sse</code>). Both request shapes ship deliberately: the app performs its bootstrap through each so the cost of a callback continuation is visible in Mere source rather than argued about._</p> <p>_Four host-side boundaries had drifted from the compiler and only a real app touched them. <strong>`str` layout</strong>: <code>mere_strbuf_to_str</code> in <code>lib/codegen_wasm.ml</code> hand-rolled its bump allocation and skipped the <code>[i32 len]</code> header that <code>__lang_strlen</code> reads from <code>ptr-4</code>, so every <code>strbuf_to_str</code> result read back as <code>""</code> on Wasm alone — invisible under <code>print</code>, which exits through the host and scans to NUL. The same header was missing from the <code>writeStr</code> helpers in <code>contrib/http/http.glue.js</code>, <code>contrib/dom/dom.glue.js</code>, <code>scripts/run_wasm.js</code> and <code>scripts/pg_env.js</code>; <code>run_wasm.js</code> had the correct sequence open-coded at four call sites, which is why the fix never reached the shared helpers. <strong>Closure ABI</strong>: <code>http.glue.js</code> still passed plain numbers to the <code>(param i64 i64)</code> closure type v0.1.127 introduced, so every <code>contrib/http</code> demo threw on its first request when rebuilt. <strong>Scratch memory</strong>: <code>dom.glue.js</code> still wrote host strings into a fixed 4KB window at 56K that wrapped around, which a multi-KB bootstrap response overruns. <strong>Missing import</strong>: the runners never supplied <code>time</code>, which the prelude imports unconditionally. <code>test/parity/strbuf.mere</code> closes the coverage gap that let the first of these live — StrBuf had no parity case among the other 68._</p> <p>_<code>contrib/http/static.mere</code> now serves assets through a new <code>http_send_file</code> host extern instead of <code>read_file</code>. Mere strings are NUL-terminated, so the old path truncated any binary file at its first zero byte, and a <code>.wasm</code> module begins with one: the server could not serve its own compiled client. Bytes now go from disk to socket without entering Mere, which also distinguishes an empty file from ENOENT. <code>scripts/run_dom_headless.mjs</code> runs a <code>mere -w</code> module against <code>contrib/dom</code> under Node with a small DOM, <code>fetch</code>, synchronous XHR and <code>EventSource</code>, so browser-targeted Mere is testable without a browser._</p> <hr> <h2 id="v0-1-151-2026-08-08">v0.1.151 — 2026-08-08</h2> <p>_RV32I fantasy-console I/O + a browser build. Two new externs the <code>mere -rv</code> backend lowers to memory-mapped I/O and a syscall, turning a <code>mere -rv</code> program into a playable cartridge: <code>key n</code> reads the held state of button <code>n</code> from an input register at <code>0x7F9000 + n</code> (a <code>lbu</code>), and <code>present ()</code> ends a frame and yields to the host via <code>ecall a7=100</code>, resuming on the next instruction next frame — so a program's main loop is a coroutine whose state lives on the RISC-V call stack. Paired with the existing <code>fb_set</code> (framebuffer store), these three are the whole hardware contract. <code>contrib/site/playground/rvconsole.mere</code> is the memu RV32IM emulator compiled to WebAssembly and wired to the DOM (ROM via <code>dom_rom_byte</code>, input via <code>dom_key_held</code>, framebuffer blitted to a <code><canvas></code>), and <code>game.mere</code> is an arrow-key-playable cartridge; both ship to the playground. <code>lib/codegen_riscv.ml</code>, <code>contrib/site/build_full.sh</code>, <code>contrib/site/build.mere</code>._</p> <hr> <h2 id="v0-1-150-2026-08-08">v0.1.150 — 2026-08-08</h2> <p>_Full structural <code>==</code> / <code>!=</code> on the RV32I backend. A comparison at a compound type (tuple, record, or payload-carrying variant) now generates a per-type <code>__eq_<tag>(a,b)</code> helper that recurses over the structure — mirroring codegen_c's <code>eq_<tag></code>. Helpers are deduped by a type tag and emitted from a worklist, so recursive types (e.g. a cons list) terminate; type parameters are substituted with the concrete arguments at the use site, so <code>list int</code> and <code>list str</code> get distinct monomorphic helpers. Verified byte-identical to the interpreter across tuples, records, single- and tuple-payload constructors, <code>Circle 5</code> vs <code>Dot</code>, a recursive <code>ilist</code>, <code>option</code>, and a tuple with a string field. Only <code>==</code> on functions is rejected. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-149-2026-08-08">v0.1.149 — 2026-08-08</h2> <p>_A framebuffer primitive for the RV32I backend: <code>fb_set x y v</code> lowers to a byte store into a 64×32 framebuffer at 0x7F8000 (above the stack). Declared in a program as <code>extern fn fb_set: int -> int -> int -> unit;</code>, it lets a Mere program draw pixels; an emulator that renders that region turns it into a tiny "fantasy console" — a Mere program, compiled by <code>mere -rv</code>, drawing graphics on the Mere RISC-V CPU (see the memu project's riscv-console demo). <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-148-2026-08-08">v0.1.148 — 2026-08-08</h2> <p>_Correct <code>==</code> / <code>!=</code> on enums (RV32I). A comparison at a non-primitive type was comparing heap pointers; now an all-nullary variant type (an enum) compares its tag word, which is exact. Compound values (tuples, functions, payload-carrying constructors) would need a recursive structural equality — they now raise a clear Codegen_error pointing at pattern matching instead of silently comparing pointers. Ints/bools/type-variables are unaffected. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-147-2026-08-08-the-mere-compiler-runs-on-the-mere-cpu">v0.1.147 — 2026-08-08 — the Mere compiler runs on the Mere CPU</h2> <p>_The self-hosting tower closes: the Mere-written compiler (lexer + parser + typer + Wasm codegen), lowered by <code>mere -rv</code> to a ~380KB RV32IM binary, runs on the Mere-written RV32I emulator and emits WAT byte-identical to the interpreter. Self-language → self-backend → self-CPU._</p> <p>_The last bug was a memory-map overlap: the globals+heap region sat at 0x10000 (64KB), but the self-hosted compiler's code is ~88KB and extended past it, so a global write corrupted the code and the program jumped into garbage. Small programs (<64KB of code) never hit it. Fix: move globals+heap to 0x200000 (2MB), well above any code — layout is now code [0,2MB) | globals+heap ↑ | stack ↓ from 0x7E0000 | print scratch 0x7F0000 (needs an ≥8MB emulator). Global slot addressing now materialises the full address, so the global count is unbounded. Verified: the self-hosted compiler produces byte-identical WAT on RV32I for arithmetic, let/if, and a recursive factorial (133 lines of WAT); all existing tests still pass. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-146-2026-08-08">v0.1.146 — 2026-08-08</h2> <p>_Long-range conditional branches on the RV32I backend. A bare B-type branch reaches only ±4KB and silently truncated its offset in large functions (the self-hosted compiler has functions well past that). Every conditional branch is now emitted as an inverted branch skipping a J-type jump (±1MB reach), so branch targets are correct at any distance. All existing tests stay byte-identical to the interpreter. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-145-2026-08-08">v0.1.145 — 2026-08-08</h2> <p>_An injected Mere-source runtime prelude for the RV32I backend (<code>lib/rv_prelude.ml</code>) — the string / char-class / Map tail the self-hosted compiler needs, written on top of the primitives codegen_riscv emits instead of hand-assembled. <code>mere -rv</code> prepends it to the user source, so it goes through the normal typer + desugar; the definitions shadow the builtins of the same name (compile_app resolves user bindings first) and reachability emits only the ones a program uses. Provides <code>is_digit</code>/<code>is_alpha</code>/<code>is_space</code>, <code>not</code>, and <code>str_starts_with</code> / <code>str_ends_with</code> / <code>str_index_of</code> / <code>str_contains</code> / <code>str_repeat</code> / <code>str_rev</code> / <code>to_lower</code> / <code>to_upper</code> / <code>str_trim</code> / <code>str_join</code> / <code>str_split</code> / <code>str_replace</code> / <code>str_unescape</code> / <code>int_of_str</code>. <strong>Map</strong> is an assoc-list in a one-cell Vec with <code>str_eq</code> keys (mirroring the self-hosted Wasm backend): since the typer forces the <code>Map</code> type on the <code>map_new</code> name, codegen_riscv intercepts the <code>map_*</code> builtins and dispatches to <code>rvmap_*</code> helpers. Also relocated the print scratch buffer out of the heap region (to 0x7F0000, stack to 0x7E0000) so large programs don't clobber it. Verified byte-identical to the interpreter across the whole string/Map surface; existing tests still pass. With this, the Mere-written compiler compiles to a ~94k-instruction RV32I binary and runs on the emulator (reaching its own typer) — the last correctness gaps on real input are being chased. <code>lib/codegen_riscv.ml</code>, <code>lib/rv_prelude.ml</code>._</p> <hr> <h2 id="v0-1-144-2026-08-08">v0.1.144 — 2026-08-08</h2> <p>_Vec — a mutable, growable array — on the RV32I backend (M3). A Vec is a <code>[len][cap][dataptr]</code> cell over a cap-word buffer; <code>vec_push</code> doubles the buffer when full (allocating a new one and copying, since the bump heap can't realloc). <code>vec_new</code> / <code>vec_push</code> are runtime helpers; <code>vec_get</code> / <code>vec_set</code> / <code>vec_len</code> are inlined. Verified byte-identical to the interpreter across push/get/set/len, growth well past the initial capacity, and an iterating sum. (<code>ref</code> turned out to be unused in the self-hosted compiler — Mere's mutability flows through Vec/Map, so no separate reference cell is needed.) Remaining for self-host: the <code>Map</code> collection and a tail of string builtins (<code>str_replace</code> / <code>str_join</code> / <code>str_split</code> / …). <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-143-2026-08-08">v0.1.143 — 2026-08-08</h2> <p>_A big step toward self-hosting on RV32I — driven by feeding the Mere-written compiler (lexer + parser + typer + codegen) through <code>mere -rv</code> and closing each gap it hit:_</p> <ul> <li>_<strong>Top-level value bindings (globals).</strong> Non-function top-level <code>let</code>s now</li> </ul> <p> live in a fixed memory region (below the heap), initialised in order at the start of <code>__main</code>; any top-level function can read them. The peeler no longer stops at the first non-function binding, so functions defined after a value binding are still lifted._</p> <ul> <li>_<strong>Recursive local closures</strong> (<code>let rec f = fn ... in ...</code>): the closure is</li> </ul> <p> allocated first and <code>f</code> bound to it before the captures are filled, so the body's self-reference resolves._</p> <ul> <li>_<strong>Fully-recursive pattern binding</strong> (arbitrarily nested tuples / records /</li> </ul> <p> constructors; <code>as</code>-patterns; string patterns) via a container-pointer parked on the stack. <strong>Record update</strong> <code>{ r | f = e }</code>. <code>region { }</code> is a no-op (the bump heap doesn't reclaim)._</p> <ul> <li>_<strong>String / char builtins:</strong> <code>str_of_int</code>, <code>ord</code>, <code>chr</code>, <code>char_at</code>,</li> </ul> <p> <code>substring</code>, <code>print_no_nl</code>, <code>fail</code>, and int-only <code>show</code>; plus <strong>StrBuf</strong> (<code>strbuf_new</code> / <code>strbuf_push</code> / <code>strbuf_to_str</code> / <code>strbuf_len</code>)._</p> <ul> <li>_<strong>> 8-argument calls:</strong> args beyond a0–a7 are passed on the stack with</li> </ul> <p> caller cleanup._</p> <ul> <li>_<strong>Fix:</strong> a user binding (local / global / top-level) now shadows a</li> </ul> <p> same-named builtin, matching the interpreter._</p> <p>_All existing RV32I tests remain byte-identical to the interpreter. The self-hosted compiler now gets much deeper before hitting the remaining gaps (more string builtins like <code>str_replace</code>, and the <code>Vec</code>/<code>Map</code> collections). <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-142-2026-08-08">v0.1.142 — 2026-08-08</h2> <p>_Records on the RV32I backend (M3, second slice). A record is a heap block whose fields are laid out in declaration order (from the <code>Top_record</code> decl); a <code>Record_lit</code> reorders its fields to that order, evaluates, and fills the block, a <code>p.field</code> reads the field's slot (the field's index is resolved from <code>p</code>'s type via the typer's <code>.ty</code>), and a record pattern <code>T { f = a, .. }</code> binds each field by its offset — in both <code>let</code> and <code>match</code>. Verified byte-identical to the interpreter across construction, field access, out-of-order literals, record pattern destructuring in <code>let</code>, a string field, nested records (<code>s.a.x</code>), and field patterns with guards in <code>match</code>. Next: attempt the Mere-written <code>selfhost-compile</code>. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-141-2026-08-08">v0.1.141 — 2026-08-08</h2> <p>_String builtins + content comparison on the RV32I backend (M3, first slice toward self-hosting). <code>==</code> / <code>!=</code> / <code><</code> / <code><=</code> / <code>></code> / <code>>=</code> on <code>str</code>-typed operands now compare content, not pointers (dispatched on the typer's <code>.ty</code>), via new <code>__str_eq</code> / <code>__str_cmp</code> runtime helpers (the latter normalised to -1/0/1, matching the interpreter). New builtins: <code>str_of_int</code> (itoa into a heap string), <code>str_eq</code>, <code>str_compare</code>, <code>ord</code>, <code>chr</code>, <code>char_at</code>, <code>substring</code> (end-exclusive, matching the interpreter's <code>String.sub s start (end-start)</code>), and <code>print_no_nl</code>. Verified byte-identical to the interpreter across equality/ordering, signed <code>str_of_int</code>, char access, and substring. Next M3 steps: records, then attempting the Mere-written <code>selfhost-compile</code>. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-140-2026-08-08">v0.1.140 — 2026-08-08</h2> <p>_Closures on the RV32I backend (M2, final slice) — the last piece before self-hosting. A closure is a heap block <code>[code_ptr][captured...]</code>; <code>fn x -> body</code> captures the locals its body uses, lifts the body to a top-level lambda (<code>code(env in a0, arg in a1)</code> — captures loaded from the env at entry, param from a1), and evaluates to the block. Application splits two ways: a saturated direct call to a known top-level function keeps the fast register-allocated <code>jal</code> path, while everything else (lambdas, higher-order params, curried/partial application through values) evaluates the head to a closure and applies arguments one at a time via an indirect <code>jalr</code>. That unlocks first-class and higher-order functions: verified byte-identical to the interpreter for apply/twice/compose, free-variable capture, and — the milestone — the prelude's own <code>list_map</code> / <code>list_fold</code> / <code>list_filter</code> / <code>range</code> / <code>list_product</code> driven by lambda arguments over a <code>Cons</code>/<code>Nil</code> list, all running on the Mere-written CPU. (Partial application of a bare top-level function still wants an explicit lambda; a follow-up.) <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-139-2026-08-08">v0.1.139 — 2026-08-08</h2> <p>_Strings on the RV32I backend (M2, third slice). A string is a pointer to <code>[len:4][bytes][pad to 4]</code>. Literals become rodata blocks emitted after the code, loaded by a new <code>LoadAddr</code> item (lui+addi of the label's absolute address — the binary loads at 0, so absolute = offset); a new <code>Bytes</code> item carries the raw data. <code>print</code> writes the bytes then a newline (print_endline semantics, matching the interpreter), <code>++</code> calls a new <code>__str_concat</code> runtime helper that bump-allocates and byte-copies both operands, and <code>str_len</code> reads the length header. Verified byte-identical to the interpreter for literals, concat chains, <code>str_len</code>, and strings flowing through functions, an ADT payload, and tuple destructuring. <code>mere -rvs</code> now also lists the rodata. Next: closures (the last M2 piece before selfhost). <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-138-2026-08-08">v0.1.138 — 2026-08-08</h2> <p>_ADTs and pattern matching on the RV32I backend (M2, second slice). A constructor is a heap block <code>[tag][payload]</code> — the tag is the variant's index within its type (from Top_type decls), the payload is one word (an int, or a pointer; a tuple pointer when the constructor has several fields). <code>match</code> stashes the scrutinee in a binding slot, then for each arm tests the pattern (constructor tag compare, int/bool literal, or an irrefutable tuple/var bind) — branching to the next arm on mismatch — and binds its variables before running the body; guards are supported. Covers the top level plus one level of sub-structure, enough for Option/Result and typical enums (deeper nesting raises a clear Codegen_error). Verified byte-identical to the interpreter across a nullary enum, single- and tuple-payload constructors, a recursive <code>ilist</code> (sum/len/max over a hand-rolled cons list), and the built-in <code>option</code>. Next: strings, then closures. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-137-2026-08-08">v0.1.137 — 2026-08-08</h2> <p>_The RV32I backend grows a heap (M2, first slice) — tuples. <code>_start</code> now sets <code>gp</code> as a bump-heap top pointer (heap at 0x10000, below the print buffer and stack); a tuple literal evaluates its elements onto the memory stack, bump-allocates an n-word block, and fills it (no call between the bump and the stores, so the block pointer stays put), leaving the pointer as its value. A tuple-pattern <code>let (a, b, ...) = e</code> loads each field into its binding. This is the first non-integer value representation — values are now "a word that is either an int or a heap pointer". Verified byte-identical to the interpreter across tuple construction, 3-field tuples, tuple-returning functions, elements that are themselves calls, tuple-in/tuple-out (swap), and nested-tuple dot products. Next slices: ADTs + Match, strings, closures. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-136-2026-08-08">v0.1.136 — 2026-08-08</h2> <p>_A disassembler for the RV32I backend — the debugging surface the direct byte-emitter skipped. <code>lib/riscv_disasm.ml</code> decodes one RV32IM word to a readable mnemonic (mirroring the emulator's imm_<em> decoders, inverse of the enc_</em> encoders), recognising the mv / li / ret / j / nop / beqz pseudo-ops. Two new modes use it: <code>mere -rvs file.mere</code> prints an assembly listing of the compiler's own output (address, hex, mnemonic, with real label names on jumps/branches), and <code>mere -rvd file.bin</code> disassembles a flat binary. This makes the register-allocated code inspectable — e.g. factorial shows the param pinned in s1, <code>n <= 1</code> folded to <code>slti a0, a0, 2</code>, and <code>n * fact(n-1)</code> as <code>mul a0, s1, a0</code> — and sets up debugging for the heap/closure work ahead._</p> <hr> <h2 id="v0-1-135-2026-08-08">v0.1.135 — 2026-08-08</h2> <p>_Register allocation for the RV32I backend (M1). The M0 stack machine kept every named binding in a memory frame slot and every intermediate on the memory stack; M1 puts a function's params and lets in the callee-saved registers s1..s11 (spilling only the 12th-plus binding to memory), folds the hot <code>n - 1</code> / <code>n < 2</code> of recursion into a single <code>addi</code> / <code>slti</code>, and reads binop/comparison operands straight out of their registers when possible — a value in a callee-saved register survives the other operand's evaluation, nested calls included, so no spill is needed. Static instruction count drops 16–32% (~23% average) across the sample programs; still byte-identical to the interpreter across factorial, Fibonacci(25), Ackermann, deep recursion (sumto 1000), gcd, and a 14-local function that exercises the memory-overflow path. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-134-2026-08-08">v0.1.134 — 2026-08-08</h2> <p>_A fifth backend — Mere lowers to native RV32IM machine code. Where <code>-c</code> / <code>-ll</code> / <code>-w</code> delegate to a C compiler / LLVM / a Wasm runtime, <code>mere -rv file.mere</code> emits a <strong>flat little-endian binary</strong> directly (no external assembler or linker) that runs on the Mere-written RV32I emulator: the self-made language now runs on the self-made CPU. This is the M0 vertical slice — 32-bit integers, arithmetic, comparisons, short-circuit <code>&&</code>/<code>||</code>, <code>if</code>, <code>let</code>, top-level (mutually) recursive functions, saturated calls, and <code>print_int</code> — lowered by a simple stack machine (a0 accumulator, fp-relative frame slots, no register allocation yet) with a two-pass label assembler and a self-contained <code>_start</code> / <code>print_int</code> (itoa + <code>ecall write</code>) runtime. Only the top-level functions reachable from <code>main</code> are emitted, so the prelude is skipped entirely. Anything outside the slice (closures, strings, ADTs, heap) raises a clear <code>Codegen_error</code>. Verified byte-identical to the interpreter across recursion (factorial, Fibonacci, Ackermann), mutual recursion, gcd, short-circuit logic, and signed div/mod. <code>lib/codegen_riscv.ml</code>._</p> <hr> <h2 id="v0-1-129-2026-08-07">v0.1.129 — 2026-08-07</h2> <p>_Byte-safe strings on the Wasm backend — the arc that made C strings byte-safe (v0.1.127) now extends to Wasm. A <code>str</code> in linear memory is <code>[i32 len][bytes][NUL]</code>: the length header lives immediately before the pointer, so embedded NULs survive (<code>("a" ++ chr 0 ++ "b")</code> has length 3) while NUL-free strings stay host/C-interop compatible via the preserved terminator. <code>__lang_strlen</code> reads the header; a new <code>__lang_str_alloc</code> centralises header-writing allocation; every string producer (concat, substring, trim, rev, repeat, replace, upper/lower, escape/unescape, split/join, str_of_bytes, hex_of_bytes, char_at, show_int, JSON string cells, read_stdin) and every string literal now carries a header, and <code>==</code> / <code>compare</code> / <code>starts_with</code> compare over the header length instead of scanning to a NUL. Host glue (<code>run_wasm.js</code>, playground) writes the header for <code>read_file</code> / <code>str_of_float</code> / <code>getenv</code> / arg strings. This gives the browser mere-ruby playground binary-safe strings (<code>pack</code> / <code>unpack1</code> / embedded-NUL <code>length</code> now match ruby byte-for-byte)._</p> <hr> <h2 id="v0-1-128-2026-08-06">v0.1.128 — 2026-08-06</h2> <p>_First native MIDI input capability — the seed of a MIDI dogfood. Six <code>extern fn</code> entry points (<code>midi_init</code>, <code>midi_default_input</code>, <code>midi_open_input</code>, <code>midi_poll</code>, <code>midi_read</code>, <code>midi_close</code>), backed by a PortMidi runtime in the C backend. The polling model (<code>midi_poll</code> then <code>midi_read</code>) matches the synchronous FFI shape that <code>tcp_*</code>/<code>udp_*</code> already use, and a whole MIDI message packs into one int, so — unlike the socket externs — the read side needs no byte arena._</p> <p>_The surface is uniformly <code>int -> int</code>: the two conceptually-nullary calls take an ignored dummy <code>0</code>. That is not cosmetic — the codegen synthesizes a first-class closure adapter (<code>name(__x)</code>) for every concrete <code>A -> B</code> extern, so a <code>unit -> int</code> signature would emit <code>midi_init(__x)</code> against a <code>void</code>-param C function and fail to compile. Keeping everything <code>int -> int</code> sidesteps it and matches the arena-FFI convention._</p> <p>_The PortMidi runtime (and its <code>#include <portmidi.h></code>) is emitted only when a program declares a <code>midi_*</code> extern (a new <code>uses_midi</code> gate, mirroring <code>uses_tls</code>), so non-MIDI native builds need no portmidi. Native-C only, like the socket capabilities; linking asks for <code>-lportmidi</code> the same way TLS asks for <code>-lssl</code>. Example: <code>examples/midi_listen.mere</code> echoes Note On/Off as note names (C4, A4, …) with velocity and channel._</p> <p>_Verified: generated C compiles, links, and runs against a faithful PortMidi stub — the "no input device" exit path is clean, and a scripted event stream decodes correctly (Note On C4 vel80, Note Off C4, Note On A4 vel100). The gate excludes portmidi from non-MIDI programs (tcp_smoke: 0 refs)._</p> <hr> <h2 id="v0-1-127-2026-08-06">v0.1.127 — 2026-08-06</h2> <p>_The Wasm backend's int is now <strong>64-bit</strong> — the receipt from "The Int That Stayed 32 Bits" came due. The trigger was exactly the documented one: a real dogfood (a Date.now()-driven clock, and mere-ruby running Ruby arithmetic) that needs 64-bit integers in the browser. Epoch-milliseconds (~1.75e12) overflowed i32: the clock trapped at <code>int_of_float</code>, and any Ruby snippet above 2^31 either failed to compile (literals, loudly) or couldn't run._</p> <p>_The design is the uniform one the receipt named for long-running use: every value slot widens from 4 to 8 bytes — ints are true i64, pointers carry a 32-bit address zero-extended, wrapped back to i32 exactly at memory operations, tags/indices/the allocator stay 32-bit internally. The JS boundary keeps its 32-bit ABI (host imports are declared <code>$name_h</code> taking i32 pointers; generated in-module shims adapt), so hosts stay BigInt-free except where true i64 values cross: the closure call type <code>(param i64 i64) (result i64)</code> (JS glue passes <code>BigInt(env)</code>) and channel payloads (the ring is now BigInt64)._</p> <p>_Verified: the four-backend parity suite is <strong>59/0</strong> including three new int64 cases (big-literal arithmetic, epoch divmod, <code>int_of_float</code> above 2^31, 64-bit bitwise); ctest 12/0; self-host fixpoint all-passed; a live clock prints the correct time on Node; and mere-ruby — a 17k-line Ruby interpreter — compiles to Wasm (500k lines of WAT), validates, and computes <code>1234567890123 + 1</code> correctly. Also fixed en route: the C backend's <code>int_of_float</code> truncated through a 32-bit <code>(int)</code> cast (epoch-ms came out as <code>INT_MAX</code>), and <code>time</code> / <code>print_no_nl</code> gained Wasm host wiring._</p> <hr> <h2 id="v0-1-115-2026-08-04">v0.1.115 — 2026-08-04</h2> <p>_Positioned <strong>write</strong> — the write half of the file API, and the forcing function for an on-disk store (a paged B-tree dogfood, <code>mbtree</code>, comes next)._</p> <p>_<code>file_pread</code> (v0.1.83) could read an arbitrary window of a file, but there was no way to write one: <code>write_file</code> / <code>write_file_bytes</code> only replace a whole file. Three new builtins complete random-access file I/O, interp + C only (the LLVM/Wasm MVP backends have no filesystem and cleanly refuse):_</p> <ul> <li>_<code>file_openrw : str -> File</code> — open a read/write handle, creating the file if</li> </ul> <p> absent and never truncating an existing one (<code>r+b</code>, falling back to <code>w+b</code>)._</p> <ul> <li>_<code>file_pwrite : File -> int -> Vec[int] -> int</code> — seek to the offset and write</li> </ul> <p> the byte vec, extending the file past its end if needed; returns the count written._</p> <ul> <li>_<code>file_fsync : File -> unit</code> — flush buffered writes to stable storage</li> </ul> <p> (<code>fflush</code> + <code>fsync</code>), for commit points in a durable store._</p> <p>_<code>file_pread</code> and <code>file_close</code> now also accept the read/write handle, so a store reads and writes through one <code>file_openrw</code> handle. On the interpreter the handle is a <code>Unix.file_descr</code> (<code>V_rwfile</code>); on C it is a single <code>FILE*</code>. Round-trips are byte-identical across interp and C._</p> <hr> <h2 id="v0-1-114-2026-08-04">v0.1.114 — 2026-08-04</h2> <p>_<code>contrib/mlint</code> grows from a one-rule demo into a real linter, and forces a C-backend codegen fix._</p> <p>_<code>mlint</code> now carries three rules, all as <code>dyn Rule</code> trait objects — unused bindings, unused parameters, and shadowed bindings (the last threading its own scope environment) — with a two-method trait (<code>rname</code> + <code>check</code>). It reads a source path from <code>args</code> and lints that file (falling back to a built-in sample), so it runs on the interpreter and the native C backend; the LLVM/Wasm MVP backends cleanly refuse <code>args</code>/<code>read_file</code> (no filesystem)._</p> <p>_C-backend fix (found by mlint, affects any <code>dyn Trait</code>): the arrow-type collector skipped <strong>polymorphic</strong> records' field types, but the struct emitter monomorphizes a generic trait dictionary <code>Trait__dict 'a</code> (left generic on <code>Trait__pack</code>'s dictionary parameter) at the TyParam-erased default <code>int</code>, emitting <code>Trait__dict_int</code>. When a method's field closure type at <code>'a = int</code> (<code>int -> R</code>) is instantiated nowhere else, the emitted struct referenced an undefined C type. <code>mlint</code>'s <code>check : 'a -> program -> diag list</code> triggered it (<code>int -> program -> diag list</code> appears nowhere else); <code>examples/trait_object.mere</code> compiled only because its <code>int -> int</code> / <code>int -> str</code> closures exist elsewhere. Fixed by walking polymorphic-record field types at their monomorphized instances in <code>collect_arrow_types</code>._</p> <p>_Ergonomics note recorded in <code>contrib/mlint/README.md</code>: a trait-object consumer must annotate its parameter as <code>dyn Trait</code> (<code>fn (ru : dyn Rule) -> …</code>) to select object dispatch; an unannotated <code>fn ru -> check ru …</code> is inferred with a <code>Rule 'a =></code> dictionary constraint instead._</p> <p>_Full suite including the bootstrap fixpoint stays green — 2291 checks, 0 failures. Line-numbered diagnostics remain deferred: the self-host AST is position-less, and adding spans would ripple through the whole self-host compiler plus the bootstrap._</p> <hr> <h2 id="v0-1-113-2026-08-04">v0.1.113 — 2026-08-04</h2> <p>_A linter for Mere, written in Mere (<code>contrib/mlint</code>), plus two <code>contrib/parser</code> fixes it forced. <code>mlint</code> parses source into the shared self-host AST and runs lint rules over it — dogfooding the trait system on an AST-sized program: rules are <code>dyn Rule</code> trait objects, diagnostics <code>derive (Eq, Ord)</code> for dedup + sort, and <code>Ord</code> is a super-trait of <code>Eq</code>. Its one rule so far flags a <code>let</code> binding whose name never occurs in scope; it runs on all four backends._</p> <p>_Forced upstream in <code>contrib/parser</code>:_</p> <ul> <li>_<code>parse_program_ast : str -> program</code> — the parser only exposed</li> </ul> <p> <code>parse_str_program</code> (a debug string); AST consumers (a linter, an analyzer) need the <code>program</code> value._</p> <ul> <li>_the parser defined its own <code>list_append</code> fixed to <code>top_decl list</code>, which</li> </ul> <p> shadowed the prelude's polymorphic <code>list_append</code> for every importer (so <code>list_append</code> on any other element type failed to type). Renamed the private helper to <code>append_top_decls</code>._</p> <p>_(Both are self-host compiler components; the full suite including the bootstrap fixpoint stays green — 2291 checks, 0 failures.) Recorded pain in contrib/mlint/README.md: the self-host AST is position-less (message-only diagnostics), and <code>type T = Ctor;</code> — a single nullary variant — parses as a type alias unless written <code>type T = | Ctor;</code>._</p> <h2 id="v0-1-112-2026-08-04">v0.1.112 — 2026-08-04</h2> <p>_Fix a parser declaration-table leak across programs parsed in one process. <code>Pipeline.parse_program</code> reset only <code>imported_files</code>, so the constructor / record / module / alias tables accumulated: a <code>type Rect = { ... }</code> record in one program left <code>Rect</code> registered, and a later program's constructor pattern <code>Rect r</code> then mis-parsed as a record pattern (<code>expected '{' for record pattern</code>). This bites any host that parses several programs in one process — the CI test binary hit it (a <code>module Shapes { type Rect = {...} }</code> test poisoning a later trait-object test's <code>Rect</code> constructor), aborting the run._</p> <p>_<code>Pipeline.parse_program</code> now calls <code>Parser.reset_decl_state ()</code> once before parsing the prelude (which re-registers its own types / constructors), giving each program a clean parser state. The REPL, which drives <code>Parser.parse_program</code> directly to accumulate definitions across lines, is unaffected. Regression test added; the full test binary now runs to completion (2289 checks, 0 failures)._</p> <h2 id="v0-1-111-2026-08-04">v0.1.111 — 2026-08-04</h2> <p>_Fix an inner-function over-capture on the LLVM and Wasm backends. When a lifted inner function A calls another lifted inner function B, A's captures are extended with B's (the transitive-capture closure) so A can forward them. But a capture of B that is bound <em>inside A's own body</em> — a let local, a nested-fn param, or a match-arm binder — is already in scope in A and must not be threaded in; otherwise A over-captures, and when A's host calls it the host is asked to pass a name it never had (<code>use of undefined value %row</code> on LLVM, <code>inner-lifted capture </code><code>row</code><code> not in scope</code> on Wasm)._</p> <p>_The interpreter and C backend already excluded body-bound names (v0.1.48); this ports that exclusion to LLVM and Wasm. Surfaced by examples/sudoku.mere, whose inner <code>cell</code> (which fills a row) captures the match-arm variable <code>row</code> bound in its enclosing <code>load</code> — sudoku now runs on all four backends (previously llvm:MISCOMPILE / wasm:UNSUP). Locked by test/parity/inner_capture_match_binder.mere; parity 47/0, unit suite green._</p> <h2 id="v0-1-110-2026-08-04">v0.1.110 — 2026-08-04</h2> <p>_Structural <code>==</code> / <code>!=</code> and <code><</code> <code><=</code> <code>></code> <code>>=</code> on compound values (variant / record / tuple, including recursive ones like list) now work on the LLVM backend. Previously the LLVM backend refused structural <code>==</code> (a clean UNSUP) and mis-compiled structural ordering, so a program comparing compounds — e.g. <code>deriving Eq/Ord</code> for a variant key — only ran on interp / C / Wasm._</p> <p>_Implemented as the LLVM siblings of the C backend's <code>eq_<tag></code> / <code>cmp_<tag></code> (and the interpreter's <code>value_eq</code> / <code>value_compare</code>): per-type <code>define i1 @eq_<tag></code> and <code>define i64 @cmp_<tag></code> (returning <0/0/>0) that recurse structurally over components — extractvalue for tuples/records, tag + boxed payload for variants (recursive variants via the pointer-to-node layout), and <code>strcmp</code> for strings — matching how <code>show_<tag></code> already walks these shapes. The Cmp handler lowers <code>==</code>/<code>!=</code> to a call to <code>@eq_<tag></code> and ordering to <code>@cmp_<tag></code> compared against 0; the needed types (and their transitive components) are collected into <code>eq_types</code> / <code>cmp_types</code> and emitted._</p> <p>_Effect: deriving <code>Eq</code> / <code>Ord</code> for a variant or record key now compiles on all four backends, so ordset over such a key works everywhere. Locked by test/parity/struct_eq_cmp.mere (variant/record/tuple/list eq+cmp), derive_variant.mere, and two LLVM-IR test_basic assertions; parity 47/0, unit suite green. (The interp, C, and Wasm backends already implemented structural comparison — this brings LLVM to parity, so all four backends now agree.)_</p> <h2 id="v0-1-109-2026-08-04">v0.1.109 — 2026-08-04</h2> <p>_<code>derive</code> — generate trait instances from a trait's defaults. <code>derive (Eq, Ord) int;</code> (or single <code>derive Eq int;</code>) expands to one empty <code>impl Ti T {}</code> per listed trait; each empty impl inherits the trait's default method bodies. A trait is therefore <strong>derivable iff every method has a default</strong> — deriving a trait with a method that has no default is the ordinary "missing method" error._</p> <p>_This makes structural instances a one-liner: a <code>trait Eq 'a { eq : 'a -> 'a -> bool = fn a -> fn b -> a == b; }</code> (default in terms of the builtin structural <code>==</code>) is derivable for any key type, and <code>derive (Eq, Ord) int;</code> gives working <code>Eq</code> / <code>Ord</code> instances with no hand-written bodies. Pure sugar over the empty impl + default-method machinery (v0.1.101); adds the <code>derive</code> keyword and works inside <code>module</code> bodies too._</p> <p>_<code>contrib/ordset</code> now carries structural defaults on its <code>Eq</code> / <code>Ord</code> traits and <code>examples/ordset_demo.mere</code> derives the <code>int</code> instances (its <code>color</code> key keeps a custom rank-based ordering). Note: structural <code>==</code> / <code><</code> on a variant / record is still an LLVM-backend limitation, so deriving for such a type works on interp / C / Wasm but not LLVM (a pre-existing gap, independent of derive). Locked by test/parity/derive.mere and two test_basic assertions; parity 45/0, unit suite green._</p> <h2 id="v0-1-108-2026-08-03">v0.1.108 — 2026-08-03</h2> <p>_Trait objects: <code>dyn Trait</code>. A heterogeneous collection of values that all implement a trait, with dynamic dispatch — <code>[dyn Shape (Circ 2), dyn Shape (Rect 3)]</code> is a <code>(dyn Shape) list</code>, and a trait method called on a <code>dyn Shape</code> (<code>area o</code>) dispatches dynamically. A function that consumes objects annotates its parameter <code>fn (o : dyn Shape) -> ...</code> (a function inferred as <code>Shape 'a =></code> would instead expect a concrete dictionary-carrying value)._</p> <p>_Implemented entirely as elaboration, with no new backend support: for an object-safe trait (every method takes the trait parameter as its single <code>self</code> argument and doesn't otherwise mention it — so <code>area : 'a -> int</code> qualifies, <code>eq : 'a -> 'a -> bool</code> does not), trait_elab auto-generates an object record <code>Trait__obj</code> of self-capturing method thunks plus a constrained packer <code>Trait__pack</code>. <code>dyn Trait e</code> is sugar for <code>Trait__pack e</code>; <code>dyn Trait</code> (type) is sugar for <code>Trait__obj</code>; and a method use on a value of object type lowers to reading and forcing the captured thunk. Because a <code>dyn Trait</code> is just a record of closures, every backend handles it unchanged. Adds the <code>dyn</code> keyword._</p> <p>_This is ergonomic sugar over a pattern already expressible by hand (a record of self-capturing closures); the sugar removes the per-instance-type boilerplate. Locked by test/parity/trait_object.mere, two test_basic assertions, and examples/trait_object.mere (circles + squares in one list on all four backends); parity 44/0, unit suite green._</p> <h2 id="v0-1-107-2026-08-03">v0.1.107 — 2026-08-03</h2> <p>_Traits and impls may now be declared inside a <code>module</code> body. Previously a module body accepted only <code>let</code> / <code>let rec</code> / nested <code>module</code> (types were already allowed and kept global); <code>trait</code> / <code>impl</code> were rejected, so a reusable trait-based library could not be namespaced. They are now accepted and — like types — kept global (the module namespaces only its functions), so a consumer writes bare <code>impl Ord T</code> but calls <code>M.of_list</code>. The top-level trait/impl parsing was factored into shared helpers used by both the top-level and module-body parsers._</p> <p>_Also fixes a spurious non-exhaustive-match warning for a <code>type</code> declared inside a module: the module qualifies constructor uses (<code>M.Leaf</code>) but the variant registry keys on the bare name, so the exhaustiveness checker now normalizes a qualified constructor to its bare last segment before comparing._</p> <p>_Surfaced by the <code>contrib/ordset</code> dogfood — a generic sorted set (BST) over an <code>Ord</code> key, with a consumer (<code>examples/ordset_demo.mere</code>) that instantiates it at <code>int</code> and a user-defined <code>color</code> variant. The dogfood also hit a pre-existing limitation (a top-level polymorphic value binding like <code>empty = Leaf : 'a tree</code> has no use site to fix <code>'a</code> and is rejected by the LLVM backend), worked around in the library by exposing <code>empty</code> as a thunk. Locked by test/parity/trait_in_module.mere and a test_basic assertion; parity 43/0, unit suite green._</p> <h2 id="v0-1-106-2026-08-03">v0.1.106 — 2026-08-03</h2> <p>_Extend v0.1.105's local-fn duplication to a single self-RECURSIVE local <code>let rec f = fn ... in body</code> used at several concrete types. Each type gets its own monomorphic copy, and the recursive self-call inside each copy is redirected to that copy, so the C and LLVM backends compile it (LLVM previously refused). The transform requires that <code>f</code> is not shadowed in either its body or its continuation, which keeps the self-call rename unconditional and safe. Mutual (multi-binding) local <code>let rec</code> groups used at several types are still left alone — a rarer remaining case. Locked by test/parity/local_poly_rec_multi_type.mere; parity green, unit suite green._</p> <h2 id="v0-1-105-2026-08-03">v0.1.105 — 2026-08-03</h2> <p>_A LOCAL polymorphic function used at several distinct concrete types now compiles correctly on the C and LLVM backends (<code>let id = fn x -> x in (id 1, id 1.5)</code> and friends). Both backends lift a local function to a single top-level function, so a multi-type use previously defaulted it to one type and miscompiled on C, or was refused on LLVM. The interpreter and Wasm already handled it._</p> <p>_Fix: a pre-pass (<code>duplicate_multi_use_local_fns</code>) that, before lifting, splits such a local binding into one monomorphic copy per distinct concrete use type and rewrites each use to its copy — turning the unsolved "multi-instantiate a lifted local fn" problem into the already-solved monomorphic case for every backend. It is deliberately conservative: it fires only on a non-recursive local <code>let</code> (nested inside a function body, so top-level functions are left to the ordinary multi-instantiation machinery), only when the function is not shadowed and every use is at a concrete type. Implemented once and shared by both native backends._</p> <p>_Locked by test/parity/local_poly_multi_type.mere (int / float / bool) and local_poly_multi_type_hof.mere (a higher-order local fn at two types); parity 39/0, unit suite green. Still open: the recursive local case and top-level mutually-recursive functions used at multiple types._</p> <h2 id="v0-1-104-2026-08-03">v0.1.104 — 2026-08-03</h2> <p>_Constrained recursive functions in a LOCAL <code>let rec ... in</code> — self- and mutually-recursive — now work. Previously only top-level <code>let rec</code> groups were handled; a local one failed with "ambiguous trait constraint". Two changes: the typer now records a local <code>let rec</code> binding whose scheme carries trait constraints into <code>trait_local_constrained</code> (it already did this for a local non-recursive <code>let</code>), and trait_elab threads the group's shared dictionary through every intra-group reference of a local <code>let rec</code> group, mirroring the top-level handling._</p> <p>_The C and LLVM single-use monomorphization pass is extended to local <code>let rec</code> groups so a local constrained recursive function used at a single non-int type (e.g. float) emits at that type instead of defaulting to int. This is restricted to dictionary-taking (trait-constrained) members and excludes each member's own body from the use scan, so it cannot mis-specialize a genuinely polymorphic recursive function used at several types (e.g. the prelude's <code>list_fold</code>)._</p> <p>_Works on all four backends at a single instance type (int / float / user variant). Locked by test/parity/trait_local_rec_self.mere, trait_local_rec_mutual.mere and two test_basic assertions; parity 39/0, unit 2283/0. (A local polymorphic recursive group used at several distinct types remains gated by the same pre-existing multi-instantiation limit as top-level and trait-free polymorphic recursion.)_</p> <h2 id="v0-1-103-2026-08-03">v0.1.103 — 2026-08-03</h2> <p>_Super-traits: <code>trait Ord 'a : Eq 'a { ... }</code>. A super-trait declares that any instance of the sub-trait must also be an instance of the super-trait. <code>impl Ord T</code> now requires <code>impl Eq T</code> (checked transitively, and for every super in a multiple-super list <code>: Eq 'a, Show 'a</code>); omitting it is a clear error rather than a confusing failure at a later use site._</p> <p>_Method access needs no special dictionary machinery: Mere's inference records a separate constraint for every trait method actually used, so a generic function that uses both an Ord method and an Eq method on one value already receives both dictionaries (there are no signature-level constraint annotations that could under-specify this). This is why super-traits reduce, for Mere, to the declaration plus the well-formedness guarantee._</p> <p>_As part of this, impl method bodies are now type-checked at their concrete instance type (<code>param := target</code>), so an impl body that calls another trait's method on the instance value — e.g. a super-trait method — resolves to that trait's concrete dictionary instead of leaving an unresolved dispatch variable. (Same-trait sibling calls are still inlined before type-checking, so no self-referential dictionary arises.) Locked by test/parity/trait_super.mere and three test_basic assertions; parity 37/0, unit suite green._</p> <h2 id="v0-1-102-2026-08-03">v0.1.102 — 2026-08-03</h2> <p>_Support top-level mutually-recursive constrained functions (a <code>let rec f = ... and g = ...;</code> group where the members require a trait). This used to be rejected ("mutually-recursive constrained function ... is not yet supported")._</p> <p>_Intra-group references are typed monomorphically (before generalization), so they carry no constrained-use obligation and must be threaded by hand: a reference to a constrained group member — itself or a sibling — is applied to the dictionary parameter(s) that member expects. Because the group is typed monomorphically, mutually-recursive members share the dispatch variable(s), so those dict parameters have the same names as the current member's own and are in scope. This generalizes the single self-recursive case (which becomes the one-element instance of the same code path)._</p> <p>_Scope: a single instance type works on all four backends. A polymorphic mutual-rec group used at two distinct types hits a separate, pre-existing backend multi-instantiation limitation (it fails the same way for trait-free mutually-recursive polymorphic code). Local (<code>let rec ... in</code>) constrained recursion — self or mutual — remains a distinct open path. Locked by test/parity/trait_mutual_recursion.mere and two test_basic assertions; parity 36/0, unit suite green._</p> <h2 id="v0-1-101-2026-08-03">v0.1.101 — 2026-08-03</h2> <p>_Trait method DEFAULTS, and impl method bodies that reference sibling methods. A trait method may now be written <code>m : ty = expr</code>; an impl that omits <code>m</code> inherits that default. Both this and an impl body that calls a sibling method (e.g. <code>neq = fn a -> fn b -> if eq a b then ...</code>) previously failed — the sibling reference had an unresolved dispatch type ("ambiguous trait constraint"), and resolving it to a dictionary field would have required the dictionary to reference itself, which Mere has no way to express._</p> <p>_Both are solved the same way: trait_elab completes each <code>impl Trait T</code> before any type-checking — every method gets a source body (the impl's own, else the trait default, else a "missing method" error), and every reference to a sibling method name inside a body is replaced by that sibling's (recursively inlined) source body. Cyclic defaults are rejected. After completion each method is a self-contained body with no trait-method name references, so type inference sees ordinary Mere and the dictionary stays a plain, non-recursive record — every backend is unchanged. An impl may still override a default by providing the method. Locked by parity cases trait_sibling_method.mere / trait_default_method.mere and five test_basic assertions; parity 35/0, unit 2277/0._</p> <h2 id="v0-1-100-2026-08-03">v0.1.100 — 2026-08-03</h2> <p>_Fix a dictionary mix-up when a generic function carries two trait constraints on the SAME type variable (e.g. <code>(Num 'a, Sh 'a) => 'a -> str</code>). The elaboration's variable→dict-parameter map was keyed by the type variable's id alone, so the second constraint's dictionary parameter clobbered the first — and a <code>Num</code> method use resolved to the <code>Sh</code> dictionary, failing with "record Sh__dict has no field: add"._</p> <p>_Fix: key the map by (variable id, trait). Since <code>resolve_dict</code> already knows which trait a method belongs to, each method use now selects the correct dictionary. A function constrained by multiple traits on one variable elaborates and runs correctly on all four backends. Locked by the parity case <code>trait_multi_constraint.mere</code> and a <code>test_basic</code> assertion; parity 34/0, unit 2272/0._</p> <h2 id="v0-1-99-2026-08-03">v0.1.99 — 2026-08-03</h2> <p>_Monomorphize single-use local polymorphic functions on the C and LLVM backends. A local <code>let f = fn ... in ...</code> is let-generalized by the typer, so its binding keeps an unresolved scheme while each use site instantiates a fresh concrete copy. Both native backends lift such a local fn to one top-level fn and default its residual type variable to <code>int</code> — so a local fn whose sole use is at, say, <code>float</code> was emitted as an int-typed C function and the float call site mismatched at compile time. (The interpreter and Wasm already handled the general case.) This is the long-standing "local polymorphic fn not multi-instantiated" limitation that the v0.1.98 trait local-<code>let</code> support ran into — reproducible without traits._</p> <p>_Fix: a whole-program pre-pass (<code>specialize_single_use_local_fns</code>) run before fn-type resolution and inner-fn lifting. When a local fn is used at exactly one concrete type, it unifies the binding type with that use arrow; because the body's type variables are shared mutable union-find cells, this propagates into the body, so the lifted fn AND any generic callee inside it (e.g. <code>list_fold</code>) resolve concretely. Running before <code>resolve_fn_types</code> is essential — the top-level multi-instantiator only sees a generic callee's concrete use once the enclosing local fn's body is concrete (cf. the v0.1.28 poly-through-poly fix)._</p> <p>_Effect: a constrained generic function defined in a local <code>let</code> now compiles on all four backends at any single instance type — <code>int</code>, <code>float</code>, or a user-defined variant. Multiple distinct use types on one local fn are left to the existing defaulting (a larger multi-instantiation increment). Locked by the trait-free-equivalent parity cases <code>trait_local_let.mere</code> (int) and <code>trait_local_let_variant.mere</code> (user variant); the four-backend differential harness stays 33/0 and the unit suite 2271/0._</p> <h2 id="v0-1-91-2026-07-31">v0.1.91 — 2026-07-31</h2> <p>_Real TLS on the native backend: <code>tcp_starttls</code> / <code>tcp_starttls_verified</code> are no longer stubs. Declaring either extern swaps in an OpenSSL-backed runtime — <code>tcp_starttls</code> does the handshake with SNI; <code>tcp_starttls_verified</code> adds peer certificate verification and hostname matching (with an optional CA-bundle path) — and <code>tcp_read</code> / <code>tcp_write</code> / <code>tcp_close</code> route through the per-fd <code>SSL*</code> when a socket has been wrapped, so the rest of a client is unchanged between HTTP and HTTPS. The whole thing is gated on a <code>uses_tls</code> flag set only when a program declares a starttls extern: a plaintext TCP program emits zero OpenSSL references and still links with just <code>-lm</code>, so TLS's external dependency is opt-in. A TLS program compiles with, e.g., <code>-I$(brew --prefix openssl@3)/include -L.../lib -lssl -lcrypto</code>.</p> <p>Surfaced and verified by the mhttps dogfood — an HTTPS GET client in pure Mere: it fetches <code>https://example.com/</code> and <code>https://api.github.com/</code> (the latter requiring a valid verified handshake) and prints <code>HTTP/1.1 200 OK</code> for both. test_basic guards that starttls pulls in the OpenSSL runtime and that a plaintext program does not. TLS on the Wasm host is still separate (browsers do TLS transparently); native LLVM shares the C runtime. suite passed._</p> <h2 id="v0-1-90-2026-07-30">v0.1.90 — 2026-07-30</h2> <p>_<code>mere install</code> detects same-major version conflicts instead of silently picking one. A module path may resolve to only one revision in a build; if two packages in the dependency graph demand the same path at different revs, the installer used to fetch both and let the second overwrite the first (last-writer-wins). It now tracks the resolved sha per module path and fails with both revisions named, asking for explicit reconciliation (pin it in the top-level mere.toml). This is the correct answer for Mere's model: it pins exact revisions with no version ranges, so there is nothing to minimize over — the npm/cargo MVS problem does not arise. Incompatible majors sidestep the conflict entirely by living at different module paths (<code>.../v2</code>, SIV, v0.1.89), which the check leaves untouched (distinct paths → no conflict). Together with v0.1.88–89 this closes the version-resolution axis of Q-013 for an exact-pin package manager. Hand-tested against a diamond (two libs pinning the same library at different revs → conflict; the SIV app with distinct paths → installs clean). suite: 2255 passed / 0 failed._</p> <h2 id="v0-1-89-2026-07-30">v0.1.89 — 2026-07-30</h2> <p>_Two <code>module</code>s with the same name now coexist correctly on every backend — the language-side half of Go-style Semantic Import Versioning (SIV). A library and its <code>/v2</code> (an incompatible major) both name their module the same (<code>module Greet { ... }</code>), so their members desugar to identically-qualified top-level names (<code>Greet.hello</code> defined twice, of different types) that shadow by declaration order. The interpreter already honoured that — a closure captures the env at its definition and the env prepends, so a reference binds to the most-recent prior definition — but the native backends resolve a top-level name globally and mis-assigned one version's body to the other (a C build emitted <code>Greet.hello : str -> str</code> with the <em>other</em> version's closure-returning body). New pipeline pass <code>Ast.uniquify_toplevel_module_shadows</code> walks the decls in order and, when a <strong>dotted</strong> (module-qualified) name is redefined, alpha-renames the redefinition (<code>Greet.hello</code> → <code>Greet.hello__v2</code>) and rewrites later references, so all four backends see distinct symbols. Only dotted redefinitions are touched, so ordinary programs are unaffected.</p> <p>Verified with the version-resolution dogfood — an app whose two dependencies pull incompatible majors of a shared library via SIV distinct paths (<code>.../mgreet</code> and <code>.../mgreet/v2</code>): it now prints the v1 and v2 results side by side on interp, C, LLVM, and Wasm. This closes the native gap that the dogfood surfaced; combined with the full-path installer (v0.1.88), SIV works end to end. Minimal-version selection (MVS, for compatible same-major demands) remains the one deferred package axis. suite passed._</p> <h2 id="v0-1-88-2026-07-30">v0.1.88 — 2026-07-30</h2> <p>_<code>mere install</code> grows up to match the Go-style import model (Q-013): full-path layout, cross-repo transitive resolution, and a verifying lockfile. Driven by the first genuinely multi-repo dogfood — a 3-repo transitive graph <code>mcalc → mbigfmt → mbignum</code> where the app never names the leaf. Three gaps surfaced and were fixed:_</p> <ul> <li>_<strong>Full-path layout.</strong> Installs went to <code>.mere_modules/<bare-name>/</code>, but a</li> </ul> <p> Go-style import resolves to <code>.mere_modules/github.com/owner/repo/</code>, so even a direct dependency failed to resolve. The installer now reads each fetched package's own <code>mere.toml [package] path</code> and installs under it (bare-name fallback for legacy packages)._</p> <ul> <li>_<strong>Cross-repo transitive deps.</strong> The installer only followed <code>../</code> relative</li> </ul> <p> imports (monorepo siblings); a dependency declaring its own <code>[dependencies]</code> in another repo was never followed. It now reads each fetched package's <code>[dependencies]</code> and queues them, so transitive cross-repo deps are pulled in (deduped on the <code>(git, rev, subdir)</code> coordinate for diamonds)._</p> <ul> <li>_<strong>Verifying lock.</strong> <code>mere.lock</code> already recorded resolved shas + content</li> </ul> <p> hashes; now a re-install parses the existing lock and, if a pinned <code>(git, rev)</code> coordinate produces a different hash, fails loudly (go.sum-style tamper/corruption detection) instead of silently building against changed content._</p> <p>_The resolved lock records the full transitive graph, so <code>mcalc</code>'s lock pins <code>mbignum</code> even though <code>mcalc</code> only depends on <code>mbigfmt</code>. Verified end-to-end: <code>mcalc fact/fib N</code> matches python on interp and C, reading both deps out of the full-path <code>.mere_modules/</code>. Unit tests cover the <code>[package] path</code> parse and the write_lock/read_lock round-trip; the fetch path is git-integration-tested by hand. suite: 2248 passed / 0 failed._</p> <h2 id="v0-1-87-2026-07-29">v0.1.87 — 2026-07-29</h2> <p>_A user top-level binding named <code>main</code> now compiles on every backend (finishing the follow-up left open in v0.1.86). Mere has no <code>main</code> convention — the entry point is the file's trailing expression — so a <code>main</code> binding is just an ordinary value that happens to share the synthesized entry's name. The C backend already mangled it (<code>mu_main</code>), but LLVM and Wasm emitted the raw name and hit a duplicate-<code>main</code> link/assemble error. Rather than patch each backend, the fix is one backend-agnostic pass in the pipeline: <code>Ast.reserve_toplevel_main</code> alpha-renames a top-level <code>main</code> to a reserved name (<code>__mere_user_main</code>) via the existing scope-aware <code>rename_free_vars</code>, so an inner <code>main</code> (a local let or a parameter) still shadows and is untouched. Verified: <code>let main = fn () -> 42 in main ()</code> prints 42 on interp, C, LLVM, and Wasm. The v0.1.86 note's "not fixed" caveat is superseded. suite passed._</p> <h2 id="v0-1-86-2026-07-29">v0.1.86 — 2026-07-29</h2> <p>_<code>str_eq</code> on the LLVM backend. The interpreter and C backend had string equality, but the LLVM backend never defined it, so any LLVM-compiled program using <code>str_eq</code> failed at emit with "unbound variable: str_eq" (surfaced by the bignum and mpath dogfoods, both of which pattern on single characters). The 2-arg call now lowers to a new <code>@__lang_str_eq</code> runtime — a byte compare over two NUL-terminated strings returning i1 — mirroring the C backend's strcmp path. Guarded in test_basic; verified equal/unequal/empty/prefix cases match the interpreter on a compiled LLVM binary.</p> <p>Not fixed here (documented in the mpath dogfood's PAIN): the Wasm backend emits a user top-level binding named <code>main</code> as <code>$main</code>, colliding with the exported entry <code>$main</code> ("redefinition of function $main"). It is narrow (only a literal <code>main</code> binding, only on Wasm) with a trivial rename workaround; a proper fix mangles or reserves the entry name and is left as a follow-up. suite passed._</p> <h2 id="v0-1-85-2026-07-29">v0.1.85 — 2026-07-29</h2> <p>_A module-level value binding compiles on the C backend, and a new <code>contrib/bignum</code> library. Writing bignum surfaced the bug: <code>let base = 1000000000</code> inside <code>module Bignum { ... }</code> carries a dotted name (<code>Bignum.base</code>), and the C let-emitter used the raw binder name for its internal temporaries, so it emitted <code>__auto_type __let_tmp_Bignum.base = ...</code> — a <code>.</code> in a C identifier, which does not compile. Every earlier contrib module bound only functions (whose names already route through name-mangling), so a module <em>value</em> binding had never been exercised. Fix: flatten the dot for the <code>__let_tmp_</code> / <code>__let_result_</code> temp names (<code>Bignum__base</code>); ordinary undotted names are untouched, so only the previously-broken module-value case changes. Guarded in test_basic.</p> <p><code>contrib/bignum/bignum.mere</code> is a reusable arbitrary-precision natural-number library — little-endian base-1e9 limbs as a persistent <code>int list</code> (from_int / add / mul_small / mul / cmp / to_str / fact / fib). Base 1e9 keeps limb products under 2^63 on the 64-bit backends. <code>examples/bignum_demo.mere</code> imports it by full path and prints 100!, fib 200, and a product that match python's bignum exactly on interp and C. Backend reach: interp + C exact; Wasm runs add/fib but mul overflows its 32-bit int (a base-1e9 product is ~1e18); LLVM rejects a polymorphic inner-closure capture (a known monomorphization gap) — both documented in the library README. suite: 2244 passed / 0 failed._</p> <h2 id="v0-1-84-2026-07-29">v0.1.84 — 2026-07-29</h2> <p>_Fire-and-forget threads: <code>detach : ThreadHandle -> unit</code>. <code>spawn</code> returns a joinable handle, and the only way to reclaim a worker's resources was <code>join</code>, which blocks. A server's accept loop that spawns one handler per connection and never joins therefore leaked a joinable thread per connection, so a long-running server slowly exhausted thread resources. <code>detach h</code> releases the thread without waiting for it (pthread_detach on the C backend; a no-op on the reference interpreter, whose domains are not the server target). Surfaced by the mhttpd dogfood (a concurrent HTTP/1.1 server): with detach plus a fixed pool of arena buffers handed hand-to-hand over a channel, mhttpd serves 400 sustained concurrent requests where the naive version aborted at ~256 (each connection had leaked a fresh 64 KB from the no-free byte arena). No new limitation in the byte arena itself — its loud abort-on-exhaustion already told the server to pool and reuse buffers; detach is the missing concurrency primitive. suite passed / 0 failed._</p> <h2 id="v0-1-83-2026-07-29">v0.1.83 — 2026-07-29</h2> <p>_Positioned reads: <code>file_pread : File -> int -> int -> Vec[R, int]</code>. <code>file_pread handle offset len</code> seeks to <code>offset</code> and reads up to <code>len</code> bytes from an open handle, returning them as an int vec (same byte representation as read_file_bytes). Reads fewer than <code>len</code> bytes only at EOF (partial tail). Until now the only binary read was <code>read_file_bytes</code>, which loads the whole file — fine for a WASM module inspector, useless for a random-access format where the point is to touch only the pages you need. This is the capability a B-tree file format wants: read one page at an offset without paying for the whole file. Interp uses seek_in/really_input on the in_channel; the C backend emits <code>__lang_file_pread</code> (fseek + a bounded fgetc loop building a region vec_int). Scope is interp + C, inheriting the file I/O family's boundary — a program that preads first opens the handle with file_open, which already refuses cleanly on LLVM/Wasm ("v0.1.59 scope = interp + C"), so file_pread needs no separate unsupported arm there. Surfaced by the msqlite dogfood (a read-only SQLite reader): it now prints <code>SELECT * FROM t</code> byte-for-byte against sqlite3 on both interp and C, reading the header, sqlite_master, and a table's leaf page by positioned reads. suite: 2242 passed / 0 failed._</p> <h2 id="v0-1-82-2026-07-29">v0.1.82 — 2026-07-29</h2> <p>_The LLVM backend's region allocator grows the arena instead of overrunning it. <code>__lang_region_alloc</code> was a pure bump — add the aligned size to the top pointer and return, with no bounds check — so once the default 4 MB arena filled, allocations ran past the malloc'd buffer and corrupted the heap. An allocation-heavy program (a per-pixel renderer that materializes a float triple per pixel) crashed with SIGSEGV at larger image sizes on LLVM while interp, C, and Wasm all produced the same checksum; the C backend had gained bounds-checked, block-chained growth in v0.1.25 (found by a long-running server) but the LLVM runtime never received it. The LLVM <code>%__lang_region</code> struct now carries a 4th <code>blocks</code> field (a chain of malloc'd blocks, each with a 16-byte header holding the <code>prev</code> link so the data that follows stays 16-aligned); <code>__lang_region_alloc</code> compares <code>top + aligned</code> against <code>base + cap</code> and, when it would overrun, chains on a geometrically larger block (doubling until it fits) via a new <code>__lang_region_add_block</code> helper; <code>__lang_region_init</code> seeds the first block and <code>__lang_region_free</code> walks the chain. Blocks never move, so pointers into earlier blocks stay valid across growth — matching the C semantics exactly. New guard <code>test/parity/region_growth.mere</code> folds a checksum over ~6 MB of live region allocations (two shallow loops so it exercises arena growth, not stack depth) and now matches across all four backends. Guard verified by reverting the fix: the LLVM column goes DIFF (empty output from the crash) on the old allocator and returns to MATCH with the growth in place. suite: 2240 passed / 0 failed; parity 28/28 (was 27)._</p> <h2 id="v0-1-81-2026-07-29">v0.1.81 — 2026-07-29</h2> <p>_Go-style full-path imports adopted in-repo: the self-host resolver learns the module path, and contrib's cross-package imports migrate. v0.1.80 taught the OCaml resolver to resolve an import under the project's declared module path (<code>mere.toml [package] path</code>) to local files; but the self-host toolchain has its own import inliner (<code>inline_imports_in</code> in contrib/codegen), whose <code>resolve_import_path</code> only knew base-dir-relative resolution — a first migration attempt made the self-host tests fail with a doubled path (<code>contrib/eval/github.com/.../ast.mere</code>). <code>resolve_import_path</code> now mirrors the OCaml resolver with its signature unchanged: an import whose first segment looks like a host name (contains a dot, not dot-relative) walks up from the importing file probing for a mere.toml that declares a matching module path and resolves module-root-relative; anything else keeps the historical behavior, and plain relative imports never probe. A missing mere.toml reads uniformly as "" on every backend (the language-level read_file fails catchably under try_or on interp/C; the Wasm host returns an empty string — its ENOENT log is now silent since a miss is an expected probe result). The helpers follow the file's Phase 54.32 style (inner loops hoisted to top-level rec fns with explicit args, the wasm-codegen capture workaround). With that in place, the repo declares <code>path = "github.com/merelang/mere"</code> in a root mere.toml and contrib's 11 cross-package <code>../</code> imports (typer/fmt/codegen/eval -> parser, http -> log, feed -> xml, site -> markdown/path, webhook -> http) migrate to full-path spelling — the same import now works in-repo (module-path-local) and vendored (<code>.mere_modules/<full-path>/</code>). site/playground keeps its own build pipeline untouched. suite: 2238 passed / 0 failed; self-host bootstrap fixpoint all-passed; ctest 12/12; parity 27/27._</p> <h2 id="v0-1-80-2026-07-29">v0.1.80 — 2026-07-29</h2> <p>_Module-path-local resolution for Go-style full-path imports (Q-013, compiler side). A project declares its module path in <code>mere.toml</code> (<code>[package] path = "github.com/owner/repo"</code>); the OCaml resolver walks up to the nearest such mere.toml and resolves an import that starts with the declared path to local files relative to the module root. External consumers already resolved full-path imports via <code>.mere_modules/<full-path>/</code> (the walk-up resolver handled deep paths unchanged) — this adds the in-repo half, so a package's cross-package imports use the same spelling in-repo and when vendored. Resolution order: module-path-local, importer-relative, <code>.mere_modules</code> walk-up, <code>-I</code>/MERE_PATH; a project with no declared path is unaffected. Two unit guards; packages.md documents the convention. suite: 2238 passed / 0 failed._</p> <h2 id="v0-1-79-2026-07-28">v0.1.79 — 2026-07-28</h2> <p>_Doc-only: memory-model.md documents the heap-element overwrite leak in copy-on-store containers (a hot loop overwriting the same <code>vec_set</code>/<code>map_set</code> slot with fresh strings grows O(writes) — the container region is bump-allocated, so old copies are unreclaimable; measured ~550 MB for 4M overwrites; scalar elements unaffected). Eliding the copy needs type-level region tracking on <code>str</code> (deferred); prefer scalar slots or StrBuf reuse in hot-overwrite loops._</p> <h2 id="v0-1-78-2026-07-28">v0.1.78 — 2026-07-28</h2> <p>_A race/cancellation example, and the finding that the structured-concurrency "select" gap is smaller than assumed. <code>channel_recv_timeout ch 0</code> turns out to be a general non-blocking try-recv (empty -> None, ready -> Some, verified on interp and C), which makes poll-based select and cooperative cancellation expressible with the primitives already in the language: examples/race.mere spawns N workers, takes the first to finish via a shared results channel, then broadcasts one cancel token per worker that each worker observes with a 0-ms recv on every step and stops early. So the only genuinely-missing piece of E-1 is a <em>blocking</em> multi-channel select (an efficiency win over busy-polling), not a capability gap — it stays deferred. (channel_recv_timeout is interp+C scope, v0.1.48, so the example is interp+C; LLVM/Wasm report it cleanly unsupported.) No compiler change. suite: 2236 passed / 0 failed._</p> <h2 id="v0-1-77-2026-07-28">v0.1.77 — 2026-07-28</h2> <p>_Map-accumulator memory fix (C backend), surfaced by a word-frequency dogfood. Counting words into a <code>Map[str, int]</code> over a 37.6 MB file with only ~13 distinct words held 62.5 MB of RSS — O(file), not O(distinct keys). Isolated to <code>map_set</code>: copy-on-store (v0.1.30) deep-copied the key into the map's region UP FRONT, before the hash lookup, so every update to an existing key leaked one key copy into the never-reclaimed bump region. A pure churn of 8M sets over 3 keys reproduced it (62.5 MB). The fix copies only what is actually stored: hash and key comparison use the caller's (content-identical) key, an existing-key update copies just the new value, and only a fresh insert copies the key. Churn and word-count both drop to ~1.3 MB (~45x), same output. This is the ubiquitous counter / histogram / accumulator pattern (a KV server, a frequency table), previously O(total writes). LLVM and Wasm were already flat here (they do not copy-on-store). Added an in-process guard that the key copy sits after the lookup loop. suite: 2236 passed / 0 failed._</p> <h2 id="v0-1-76-2026-07-28">v0.1.76 — 2026-07-28</h2> <p>_More parity coverage (test/parity/ 22 -> 26) and a documented known divergence. Added four shapes that agree across all four backends: an or-pattern match arm with a shared binding, functional record update (<code>{ base | f = e }</code>), float builtins with int_of_float, and a variant whose constructors carry different payload shapes. Probing the divergence-prone corners recorded their status: a nested let-record/constructor pattern (<code>let pt { x = a } = p</code>) is a parser limitation (rejected before codegen, not a backend gap); <code>ref</code>/<code>:=</code> mutable cells are not a Mere idiom. One genuine but already-known correctness divergence was reconfirmed and is deliberately NOT in the pass/fail corpus (it would be a permanent red): a 64-bit integer computation (<code>100000 * 100000</code> = 10^10) is correct on interp and C but silently wrong on LLVM and Wasm, whose integers are i32 — the documented i64-widening limitation (a non-goal per the earlier LLVM assessment). No compiler change. suite: 2235 passed / 0 failed._</p> <h2 id="v0-1-75-2026-07-28">v0.1.75 — 2026-07-28</h2> <p>_Port the v0.1.70 referenced-but-unresolved poly-fn recovery to the Wasm backend. A polymorphic helper whose arrow keeps a residual type variable at every use site (e.g. <code>result_and_then</code> applied only to <code>Ok</code>, so the error type never grounds) is dropped by the resolver as unused — but if emitted code still references it, the direct call site emits <code>call $<name></code> to a function that was never defined, which C fixed in v0.1.70 but Wasm still hit, producing an invalid module (<code>undefined function variable "$result_and_then"</code>). The parity harness (v0.1.73) surfaced it. Wasm's <code>resolve_fn_types</code> now runs the same recovery fixpoint C does: after the normal resolution, scan the emitted spine (<code>Codegen_c.find_live_arrow</code>, which accepts arrows with tyvars and skips dropped fn definitions) for live references to still-unresolved skels, and emit each with <code>Codegen_c.deep_erase_tyvars</code> erasing residual tyvars to int (both helpers are backend-agnostic and reused directly). The unconstrained-error <code>result</code> program now emits a valid module and runs on Wasm (== interp/C == 42); LLVM continues to report it a clean codegen error (documented subset limit). Added test/parity/result_residual.mere and an in-process guard asserting the Wasm definition is emitted, not just called. suite: 2235 passed / 0 failed._</p> <h2 id="v0-1-74-2026-07-28">v0.1.74 — 2026-07-28</h2> <p>_Parity corpus expansion (test/parity/ 9 -> 21) plus a harness classification fix. Added twelve diverse self-contained programs — nested tuple pattern, prelude option/result/list helpers, nested-variant match, string/char ops, curry-3, value shadowing, negative div/mod, tuple-capturing closure, boolean short-circuit — each run through all four backends. All 21 now agree with the interpreter. Two backend divergences surfaced along the way: (1) a nested let-tuple pattern (<code>let ((a,b),(c,e)) = t</code>) is rejected by C, LLVM, and Wasm with a clean "not supported in <backend> codegen subset — use match" (a consistent, documented limitation); the harness's emit-classifier now recognizes that phrasing as UNSUP rather than a hard failure, so it is not a false red. (2) A prelude <code>result</code> helper left with a residual (error) tyvar — only <code>Ok</code> used, so the error type never grounds — makes the Wasm backend emit a <code>call</code> to an undefined function (invalid module), where the C backend recovers it (v0.1.70) and LLVM errors cleanly; the corpus uses a fully-concrete <code>(int, str) result</code> instead, and the Wasm gap (it should recover like C or error like LLVM, not emit an invalid module) is recorded for a follow-up. No compiler change. suite: 2234 passed / 0 failed._</p> <h2 id="v0-1-73-2026-07-28">v0.1.73 — 2026-07-28</h2> <p>_A four-backend differential (parity) harness — <code>scripts/parity.sh</code> + <code>test/parity/</code> — plus the resolver bug it immediately caught. The harness runs each program through every backend (interp / C / LLVM / Wasm) and diffs stdout against the interpreter, classifying each backend MATCH / DIFF / MISCOMPILE / UNSUP (clean "unsupported" at emit) / SKIP (toolchain absent). It exists to catch the "interp-accepts / backend-rejects" and "backends-disagree" family before a dogfood stumbles on it. On its first run it found one: a top-level fn named <code>f</code> taking a tuple and matching on it compiled on interp / LLVM / Wasm but MISCOMPILEd on C. Root cause: the concrete-arrow discovery that drives per-instantiation monomorphization (find_concrete_arrow / find_all_concrete_arrows_in / find_live_arrow) walked into the bodies of resolved poly helpers ignoring binder scope — so list_fold / list_map's parameter <code>f</code> was read as a use of the user's top-level <code>f</code>, forcing a bogus <code>int -> int -> int</code> monomorphization that treated the tuple parameter as curried (<code>.f0</code> / <code>.f1</code> on a scalar). The fix makes those scans skip a <code>Fun</code> parameter that shadows the searched name; only the Fun binder is treated as shadowing, because a let / let-rec binding the name may itself be the poly fn's definition whose use sites live in its body (narrowing to Fun keeps chained multi-instantiation discovery working). This is the same name-collision family as the earlier <code>index</code> / <code>y0</code> param cases, but in the monomorphizer rather than name mangling. Added a corpus of nine self-contained parity programs (arithmetic, recursion, let-pattern, tuple/ADT/record match, closures, strings, mutual recursion) and an in-process guard for the fixed mono. suite: 2234 passed / 0 failed._</p> <h2 id="v0-1-72-2026-07-28">v0.1.72 — 2026-07-28</h2> <p>_<code>contrib/stream</code> — region-scoped line streaming combinators, closing the ergonomic side of the strings-lifetime gap. A <code>str</code> carries no region tag, so the escape checker must assume any line read in a streaming loop may escape and keeps it in the program-lifetime region; a naive line loop therefore grows to O(file). The reclaim machinery to avoid this has existed since v0.1.31 (a <code>region R {}</code> block redirects the thread-local current region, and an escape-clean block result — an int/bool/unit — is copied out while the block's scratch, including the line string, is freed on release), and the memory-model doc measured it, but there was no reusable combinator, so a streaming tool had to hand-roll the per-line region (mgrep's line loop simply didn't, and grew to ~file size). <code>module Stream { each_line, count_lines }</code> packages the pattern: <code>each_line path cb</code> runs a side-effecting <code>str -> unit</code> callback per line, and <code>count_lines path pred</code> returns a match count, each processing the line inside a per-line region block. Measured on a 57 MB / 1,000,000-line input, native C backend: peak RSS 61.7 MB (naive loop) -> 1.4 MB (combinator), a 44x drop, same output. Added examples/stream_lines.mere and an in-process codegen guard asserting the region redirect -> file read -> restore -> release ordering that makes the reclamation hold. The deeper fix — a type-level region tag on <code>str</code> so region-scoped strings can also be stored into outer containers — remains deferred until a dogfood forces it (the streaming case, which is what has recurred, is covered by this). Backends: interp + C (per-line file input is not implemented on Wasm/LLVM). suite: 2233 passed / 0 failed._</p> <h2 id="v0-1-71-2026-07-28">v0.1.71 — 2026-07-28</h2> <p>_C-backend hardening: two latent "compiles-to-C-then-fails" bugs fixed, plus the missing compile-and-run test path that let this family recur. (1) The <code>_as_value</code> closure adapter every top-level fn gets now sanitizes its parameter name through <code>c_safe_name</code> — a source parameter named like a C keyword (<code>case</code>, <code>default</code>) previously emitted an invalid C parameter declaration in the wrapper even when the fn was never used as a value, since wrappers are generated for all top-level fns. (2) An extern used in value position (passed to a higher-order fn, not directly applied) now lowers to a closure adapter <code>__ext_<name>_as_value</code> that calls the raw FFI symbol, instead of the mangled <code>mu_<name></code> (undeclared — a bare extern is a raw C function, not a closure struct). This is the reference-side twin of the v0.1.61 capture fix; restricted to a simple <code>A -> B</code> signature, a curried/higher-order extern-as-value is now a clear compiler error pointing at <code>fn x -> name x</code>. (3) <code>scripts/ctest.sh</code> + <code>test/ctests/</code> add a native-backend compile-and-run differential harness: each program is emitted to C, compiled with the C compiler, and (for extern-free programs) run and diffed against the interpreter; it also emits Wasm and assembles it with wat2wasm when available. The in-process suite only ever inspected the emitted C as text, so undeclared-identifier and closure-type failures escaped it; the harness compiles the emitted code for real. The corpus covers the family across both backends — reserved-name/keyword params, extern-as-value and extern-in-closure, top-level-fn-as-value, inner recursive uncurrying, mutual recursion, tuple capture, and multi-variable/deeply-nested captures (the Wasm backend, whose identifiers can't collide with C keywords and which already errors cleanly on extern-as-value, was confirmed free of the two C miscompiles). Two in-process string guards for the fixed regressions were also added. suite: 2232 passed / 0 failed._</p> <h2 id="v0-1-70-2026-07-27">v0.1.70 — 2026-07-27</h2> <p>_A referenced-but-never-concretized poly fn is now emitted (tyvars erased) instead of silently dropped. <code>resolve_fn_types</code> treated every fn without a concrete arrow as unused and skipped it — but a fn whose arrow keeps a residual tyvar at every use site (e.g. an unannotated wrapper taking a producer whose result is the bottom type of an endless loop) is NOT dead: call sites still emit a direct call, which failed at the C compile with an undeclared identifier. The fix is a recovery pass after the resolution fixpoint: scan the emitted spine — the program expression minus top-level fn definitions, plus the bodies of emitted and recovered fns (their own fixpoint) — for live references, and emit such fns with <code>deep_erase_tyvars</code> (residual tyvars become int, the representation the v0.1.69 emission erasure already names). The liveness scan must skip fn-definition bindings: scanning the whole expression would resurrect every generic prelude helper referenced from other dropped helpers' bodies. Downstream, the type- instance collectors learned the same erasure so recovered generic bodies register the instances their erased emission references: Channel / Vec / OwnedVec / Map element types erase before the concrete gate in c_type_of, and tuple-shape / mono-variant collection registers erased shapes (dead extras dedup by name). An unannotated polymorphic generator wrapper called with an endless producer now compiles and runs end-to-end. suite: 2230 passed / 0 failed._</p> <hr> <h2 id="v0-1-69-2026-07-27">v0.1.69 — 2026-07-27</h2> <p>_Residual type variables are erased at C codegen instead of rejected. A type variable that survives to codegen is either dead — the bottom result of a function that never returns, such as an endless generator loop — or genuinely unconstrained; no operation ever inspects such a value, so any representation works. <code>ty_tag</code> now names it <code>int</code> and <code>c_type_of</code> emits <code>long long</code> (the representation the resolver's use-site naming already assumes), instead of raising "unsupported C codegen type element: 'a". This fixes two failures found by a concurrency probe: an endless producer (<code>let rec go = fn a -> fn b -> ... go b (a + b)</code>) killed compilation, and a top-level fn whose body contained such a loop was silently dropped from <code>resolve_fn_types</code> while call sites still referenced its <code>_as_value</code> wrapper (undeclared identifier at the C compile). Both previously needed source workarounds (grounding the type with an unreachable unit branch / eta-expanding at the call site); the natural spellings now compile and run. Two artificial rejections became working programs and their tests were converted to positive assertions: an uninstantiated polymorphic variant now emits a concrete int instance, and a lifted closure capturing a tuple compiles and runs correctly (verified against the interpreter). suite: 2230 passed / 0 failed._</p> <hr> <h2 id="v0-1-68-2026-07-27">v0.1.68 — 2026-07-27</h2> <p>_C-backend maps get a hash index: <code>map_get</code> / <code>map_has</code> / <code>map_set</code> lookup is now O(1) amortized instead of a linear scan. A concurrency probe (an actor-owned "visited set" benchmark) showed the old cost dominating everything else: 100k check-and-insert ops against a ~30k-entry map took 5.8 s (~58 µs/op, ~300x the 97-entry case) — the map, not the messaging, was the bottleneck. The struct keeps its insertion-ordered keys/values arrays (so <code>map_iter</code> order, <code>map_len</code>, and <code>map_delete</code>'s shift-remove behavior are observably unchanged) and adds an open-addressing index of array positions: linear probing, power-of-two capacity, rehash at 0.7 load, rebuilt after a delete (deletes shift positions and are rare). Key hashing mirrors the structural key-equality emitter — splitmix64 for scalars, FNV-1a for strings, recursive combination for tuple / record / variant keys — so keys equal under <code>key_eq</code> always hash equal. Same benchmark after: 10 ms (~580x). A 5k-entry grow/rehash/delete/iter functional check and the full suite verify behavior parity; the interpreter's map is untouched (correctness-first reference). suite: 2230 passed / 0 failed._</p> <hr> <h2 id="v0-1-67-2026-07-18">v0.1.67 — 2026-07-18</h2> <p>_A caught <code>fail</code> is now silent on the C backend, found by the mere-ruby dogfood's exception milestone. mere-ruby implements Ruby <code>begin/rescue</code> on top of <code>fail</code> + <code>try_or</code>: a <code>raise</code> unwinds via <code>fail</code>, and <code>try_or</code> catches it. But the C runtime's <code>__lang_fail_impl</code> printed <code>fail: <msg></code> to stderr unconditionally, before checking whether an active <code>try_or</code> would catch the longjmp — so every rescued exception leaked a stderr line, even though the program continued correctly. A caught failure is control flow, not an error; it must be silent. The fix reorders the helper to longjmp first and print only when the failure is genuinely uncaught (about to abort). The LLVM backend and the interpreter were already silent-on-catch, so this also removes a cross-backend divergence. suite: 2230 passed / 0 failed (1 new test)._</p> <hr> <h2 id="v0-1-66-2026-07-18">v0.1.66 — 2026-07-18</h2> <p>_A C-backend duplicate-definition bug, found by the mere-ruby dogfood's first method milestone. Adding <code>def</code> / method calls turned the interpreter's evaluator into one large mutual-recursion group threading two Maps (locals and methods) through eleven functions, and the C backend refused to compile it: eighteen functions were each emitted twice, a "redefinition" error. The cause was in per-instantiation specialization. A polymorphic function's specialization list is grown, across resolution passes, from one concrete arrow type per use site. When a function is used from many sites, arrows that differ only in a region type variable — which the mangled-name tag erases — accumulate as distinct specs that all mangle to the SAME C symbol, so one fn_decl was emitted per spec and the identical definitions collided. The two later re-scan branches already deduped their arrows; the fix dedups the spec list by its emitted C symbol at the single emission chokepoint, so same-symbol specs collapse while genuinely distinct instantiations are preserved. With the fix mere-ruby's evaluator compiles clean and runs <code>def</code> / recursion / <code>return</code> byte-identical to <code>ruby</code>. suite: 2229 passed / 0 failed (1 new test)._</p> <hr> <h2 id="v0-1-65-2026-07-18">v0.1.65 — 2026-07-18</h2> <p>_Shortest round-trip float formatting, forced by the newest dogfood. The first program mere-ruby (a Ruby subset interpreter in pure Mere) could not print was <code>puts 0.1 + 0.2</code>: Ruby prints <code>0.30000000000000004</code>, but <code>str_of_float</code> formatted every float at 12 significant digits, printed "0.3", and the original double was unrecoverable from the string — <code>float_of_str (str_of_float x)</code> was not <code>x</code>. All four backends (the interp's format_float, the C runtime helper, the LLVM IR helper, and the Wasm JS hosts) now format at 12 digits first — every value that 12 digits already represented faithfully keeps its exact old rendering, so nothing else changes — and widen toward 17 until the string parses back to the same double, the same shortest-round-trip contract Ruby, JS, and Python print with. Reading the four implementations side by side also surfaced a real pre-existing divergence: the LLVM helper appended a bare "." to whole-valued floats ("100.") where every other backend renders ".0" ("100.0"). Fixed in the same slice; the four backends were verified byte-identical on a shared corpus. suite: 2228 passed / 0 failed (7 new tests)._</p> <hr> <h2 id="v0-1-64-2026-07-18">v0.1.64 — 2026-07-18</h2> <p>_A backend gap closed, found by the medit dogfood. <code>read_lines : str -> str list</code> type-checked and ran under the interpreter, but the C backend had no arm for it — so a compiled program that read a file into lines emitted an undefined <code>mu_read_lines</code> and failed to link. The surface language promised something the native target could not deliver, and the type checker could not see it. The fix adds <code>__lang_read_lines</code>, a helper that matches the interpreter's <code>input_line</code> semantics exactly: split on newlines, drop a single trailing empty element when the file ends in '\n', and return the empty list for an empty file — so "a\nb\n" and "a\nb" both give ["a"; "b"], "" gives [], and "\n" gives [""]. Verified line-for-line against the interpreter on those edge cases. suite: 2221 passed / 0 failed (2 new tests)._</p> <hr> <h2 id="v0-1-63-2026-07-18">v0.1.63 — 2026-07-18</h2> <p>_A native monotonic clock, so Mere can time itself. Every measurement in this project so far shelled out to the <code>time</code> command; <code>now_ms</code> (a self-contained native FFI over <code>clock_gettime(CLOCK_MONOTONIC)</code>, emitted like the tcp_<em> / udp_</em> externs) returns milliseconds since an arbitrary epoch, and that is enough to build a benchmark harness in pure Mere. The dogfood is mbench: it runs a kernel enough times to span a target wall-clock window, then reports iterations, total ms, and ns/iter, threading a checksum through the loop so the optimizer cannot delete the work. Writing it surfaced the universal benchmark trap first-hand — a kernel that ignores the loop counter is loop-invariant and clang -O2 hoists it out (a plain summation was even strength-reduced to i + C, reported as 0 ns/iter). The fix is the universal one: thread the counter into every kernel's input. A quiet observation falls out — Mere-on-C inherits clang's optimizer wholesale, for better (real kernels run fast) and for worse (arithmetic-reducible kernels vanish). suite: 2219 passed / 0 failed (1 new test)._</p> <hr> <h2 id="v0-1-62-2026-07-18">v0.1.62 — 2026-07-18</h2> <p>_A native UDP FFI, opened by a DNS resolver. mkv and mhttp used TCP; the new dogfood, mdns, is the first datagram-socket program, and it needed a capability that genuinely did not exist: udp_open / udp_send / udp_recv, connected SOCK_DGRAM sockets that send and receive one datagram at a time through the flat arena, reusing the protocol-agnostic tcp_close and tcp_set_timeout. On top of them mdns builds a DNS query packet byte by byte in the arena — a 12-byte header, length-prefixed labels for the QNAME, QTYPE/QCLASS — sends one datagram to a resolver, and parses the answer section, stepping past compressed names and reading each A record's four IPv4 bytes. The same binary-packet construction and length-prefixed parsing as a gzip block, but on the wire. Verified against <code>dig +short</code> on several names (including this project's own GitHub-Pages A set) and two resolvers. suite: 2218 passed / 0 failed (2 new tests)._</p> <hr> <h2 id="v0-1-61-2026-07-18">v0.1.61 — 2026-07-18</h2> <p>_An extern-in-closure capture bug, found the first time the client side of the TCP FFI was driven. The dogfood is mhttp — an HTTP/1.1 client in pure Mere over raw <code>tcp_connect</code> / <code>tcp_write</code> / <code>tcp_read</code> (mkv used the server side; this is the first <code>tcp_connect</code>). Its <code>send_all</code> helper called <code>tcp_write</code> from inside an inner recursive closure, and the C backend refused to compile: the closure-lift analysis captured <code>tcp_write</code> as a free variable and referenced it through the env as the namespaced <code>mu_tcp_write</code>, while the extern itself is emitted raw. The cause: the lift's "globals to exclude from capture" set held top-level fns and builtins but not extern fns — so an extern used as a value inside a helper was wrongly treated as a captured local. Externs are globals, referenced directly in the generated C, so they now join that excluded set. With the fix, mhttp parses status lines, case-insensitive headers, Content-Length bodies, and chunked transfer-encoding, verified byte-for-byte against curl (local server, both framings) and against a live server. suite: 2216 passed / 0 failed (2 new tests)._</p> <hr> <h2 id="v0-1-60-2026-07-18">v0.1.60 — 2026-07-18</h2> <p>_int_of_str semantics pinned across all four backends, caught by a Result-pipeline probe. The probe wrote an ordinary config pipeline — parse three fields, validate, combine — and the same program printed different errors on the interpreter and C: "not a number: abc" versus "out of range: 0". The cause: the interpreter's int_of_str raised on invalid input (so a try_or bridge caught it), while C was a bare atoll, LLVM a bare atoi, and Wasm a hand-rolled stop-at-first-non-digit loop — all three silently returning 0 or a partial prefix. The C emitter's own comment admitted it: "Fail handling is omitted." The shared spec is now strict decimal — optional surrounding whitespace, optional sign, one or more digits, nothing else — and invalid input FAILS on every backend, catchable by try_or: the interpreter validates before parsing (dropping OCaml int_of_string's 0x/0o/0b acceptance, which no compiled backend ever had), C gains a validating __lang_int_of_str over strtoll, Wasm's WAT helper validates and calls $__lang_fail with an interned message, and LLVM gains an IR helper over strtoll + endptr that calls __lang_fail_impl. The probe's other measurements — the Result-chain nesting tax and two phantom-type wrinkles — are design notes, not code changes. suite: 2214 passed / 0 failed (6 new tests)._</p> <hr> <h2 id="v0-1-59-2026-07-18">v0.1.59 — 2026-07-18</h2> <p>_Streaming file input, forced by a grep. The new dogfood is mgrep — a grep-lite over the backtracking regex engine (examples/regex.mere, whose probe also caught and fixed a real star-backtracking bug in the older contrib/regex engine: <code>a*a</code> failed on "aa" because a single returned end-position cannot give characters back to the rest of a sequence). The measurements came in three acts. Act one: grepping a 94 MB file with whole-file <code>read_file</code> + <code>str_split</code> peaked at 1.26 GB of RSS — thirteen times the file — which forced the new capability: <code>file_open</code> / <code>file_read_line</code> / <code>file_close</code>, an open read handle streaming one line at a time, with EOF as option None rather than <code>read_line</code>'s ambiguous "" sentinel (interp + C; Wasm/LLVM are pointed errors; File is Send but not Sync). Act two: streaming alone made it WORSE — 2.4 GB — exposing that the CPS matcher allocates its continuation closure on RSeq entry, before the first character is tested: ~20 bytes of never-freed arena per scanned byte. Act three: first-literal-byte and ^-anchor prefilters (what real greps do with memchr) collapse the churn to match candidates — the 94 MB grep now runs in 100 MB and 1.8 s, output identical to grep -rn. The residual 100 MB ≈ the file size is the cleanest number yet for the known strings-lifetime hole: even perfectly streamed input accumulates its line strings in the program-lifetime region. That, and the closure-on-entry pattern, are the next region-reclamation forcing cases. suite: 2208 passed / 0 failed (3 new tests)._</p> <hr> <h2 id="v0-1-58-2026-07-18">v0.1.58 — 2026-07-18</h2> <p>_The ray tracer reaches the browser, and an annotation census closes a question. The playground gains <code>/playground/raytrace.html</code>: the same ray tracer as <code>examples/raytrace.mere</code>, compiled to Wasm, drawing to a canvas through two new contrib/dom externs — <code>dom_canvas_fill_style</code> and <code>dom_canvas_fill_rect</code>, the frontend FFI's first pixel-output surface (no compiler change needed; the extern machinery took five-argument imports as is). A headless harness that captures the canvas calls rebuilds the exact PPM the native backends produce, and the page's status line shows the same Adler checksum — cross-backend parity, visible on a page. Separately, a census of the "annotate polymorphic params" wrinkle (T-4) measured what a bidirectional-inference fix would actually buy: of 1,484 parameter annotations across 255 examples, almost all are stylistic (plain int params, vec_get results, and float literals all infer fine unannotated); only two patterns genuinely require an annotation — a float flowing through a polymorphic binding, and a record update on a polymorphic parameter. Both have one-annotation workarounds, so the verdict is no type-system surgery: the float case already had a pointed hint (v0.1.50), and this release gives the record-update case its twin — the error now names the workaround with an example. suite: 2205 passed / 0 failed (1 new test)._</p> <hr> <h2 id="v0-1-57-2026-07-18">v0.1.57 — 2026-07-18</h2> <p>_The Wasm backend had never actually assembled a float-heavy program with functions, and a ray tracer proved it. The probe itself — three spheres, a mirror bounce, hard shadows, all vec3 math on (float, float, float) tuples — ran identically on the interpreter and C, but the Wasm build died in wat2wasm: <code>local.set expected [i32] but got [f64]</code>. The cause: floats on Wasm are boxed (an i32 pointer to a heap f64), and boxing needs a raw-f64 temp local. The machinery to type those temps existed (<code>local_types</code>, Phase 34.3) and the main-body emitter read it — but the three FUNCTION emitters (top-level, lifted, closure adapter) ignored it and blanket-declared every extra local <code>i32</code>. So float expressions at the top level worked, and any float temp inside a named function produced invalid WAT. All three emitters now declare typed locals. With that fixed, the ray tracer runs on all three executable backends with an identical checksum and a byte-identical PPM (interp / C / Wasm). The checksum had to be Adler-style rather than CRC-32 — 0xFFFFFFFF doesn't fit Wasm's 32-bit int (the v0.1.41 pointed error, working as designed), and a logical shift doesn't exist for it either. The boxing tax, measured: a 96×54 render allocates 26 MB from the never-freeing bump allocator (~5 KB per pixel); at 320×180 the tax exceeds the fixed 64 MB linear memory and traps — the first concrete forcing case for the deferred Wasm memory-growth work (E-2). <code>args</code> is also unsupported on Wasm (pointed error), so the example writes its PPM unconditionally instead of arg-gating it. examples/raytrace.mere. suite: 2204 passed / 0 failed (2 new tests)._</p> <hr> <h2 id="v0-1-56-2026-07-17">v0.1.56 — 2026-07-17</h2> <p>_Full namespacing of user value/function names in the C backend — the robust end of the reserved-name whack-a-mole. Six times a user name collided with the C namespace (<code>index</code>, <code>remove</code>, <code>acct</code>, <code>dup</code>, <code>run</code>, <code>y0</code>), each patched by adding to a hand-maintained reserved-word list or a missed sanitizer path. That list is now gone: <code>c_safe_name</code> prefixes every user value/function identifier with <code>mu_</code>, so nothing user-named can collide with a C keyword, a libc/POSIX symbol, or a libm function ever again. Two properties make the uniform prefix the real fix rather than a bigger list: additions to libm/POSIX can't reintroduce the bug, and because the prefix is uniform, any emission path that forgets to route a name through <code>c_safe_name</code> fails to compile for <em>every</em> function (not just reserved-named ones), so the test suite surfaces such bypasses immediately — that self-verifying property caught two latent def/use mismatches during this change (pattern-variable binders and lifted-call capture arguments), now fixed. Names emitted directly are unaffected: runtime and generated symbols (<code>__lang_*</code>, <code>__anon_*</code>, <code>__lifted_*</code>, <code>closure_*</code>, <code>mere_*</code>), FFI extern names, and the real <code>int main</code>. TYPE names (records and variants) are a separate C namespace and stay un-prefixed via a new <code>c_type_name</code>, leaving the recursive-variant machinery untouched. The self-hosting byte-identical fixpoint is unaffected — it runs on the WAT backend, which shares no naming with the C backend. suite: 2202 passed / 0 failed (~40 codegen-assertion needles updated to the <code>mu_</code> forms; behavior byte-identical for programs that never shadowed a builtin). Both halves of the reserved-name problem — Mere builtins (v0.1.54) and C symbols (this release) — are now closed._</p> <hr> <h2 id="v0-1-55-2026-07-17">v0.1.55 — 2026-07-17</h2> <p>_A reserved-name parameter bug, in the one function-emission path the earlier fix had missed. A date-arithmetic probe wrote <code>fn (y0: int) -> ...</code>, and <code>y0</code> (with <code>y1</code>, <code>j0</code>, <code>j1</code>, <code>gamma</code>) is a libm Bessel function, already on the reserved list. The interpreter ran it fine, but the C backend failed to compile: the top-level curried function declared its parameter raw as <code>long long y0</code>, while the body — which captures that parameter into the returned closure's environment — referenced the sanitized <code>y0_</code>, an undeclared identifier. v0.1.51 had fixed exactly this mismatch for <code>format_param</code> and the closure adapter after the gzip probe hit it with <code>index</code>, but the plain <code>emit_fn</code> path (a simple top-level curried function, not lifted) still inlined the raw parameter name. It now goes through <code>format_param</code> like the others, so the declaration and every reference agree. The probe itself — day-number conversions, days-between, add-days, all as <code>(y, m, d)</code> tuples since there is no date type — was otherwise new-bug-zero, matching a reference implementation on weekdays, intervals, leap boundaries, and a thirty-thousand-day round-trip, identically on both backends. suite: 2199 passed / 0 failed (2 new tests)._</p> <hr> <h2 id="v0-1-54-2026-07-17">v0.1.54 — 2026-07-17</h2> <p>_User definitions now shadow builtins at the call site (the recurring reserved-name pain, attacked at its other root). A Scheme-interpreter probe named a function <code>run</code>; on the C backend that call compiled to <code>__lang_run(...)</code> — the shell-exec builtin — because the builtin's direct-call App-arm matched the name before the ordinary user-call path. The interpreter had always shadowed correctly (a later <code>let</code> binding wins), so only C was wrong. This is the same family as the <code>join</code> / <code>is_digit</code> / <code>is_alpha</code> / <code>is_space</code> guards added case-by-case earlier: a builtin App-arm should defer to a same-named user binding. Rather than keep playing whack-a-mole, a single <code>user_shadows</code> helper (local / captured / lifted-inner / top-level) now guards ~30 collision-prone builtin arms (<code>run</code>, <code>spawn</code>, <code>even</code>, <code>odd</code>, <code>abs</code>, <code>show</code>, <code>fail</code>, <code>exit</code>, <code>sqrt</code>, <code>sin</code>, <code>cos</code>, <code>tan</code>, <code>chr</code>, <code>ord</code>, <code>args</code>, <code>len</code>, <code>not</code>, <code>fst</code>, <code>snd</code>, <code>sleep_ms</code>, <code>random_int</code>, <code>file_*</code>, <code>mkdir_p</code>, <code>list_dir</code>, <code>read_line</code>, <code>read_key</code>, <code>tty_*</code>). The guard is strictly safe: it fires only when the user actually bound that name, so programs that don't shadow a builtin are byte-for-byte unaffected. This addresses the Mere builtin half of the reserved-name problem; the C-keyword/POSIX half is still handled by the <code>c_safe_name</code> suffix sanitizer, and full top-level namespacing (which would subsume both) remains deferred. suite: 2197 passed / 0 failed (4 new tests)._</p> <hr> <h2 id="v0-1-53-2026-07-17">v0.1.53 — 2026-07-17</h2> <p>_Lowercase record types, and one more reserved name (found by a records-heavy ledger dogfood). Mere's convention is lowercase type names with capitalized constructors — <code>type 'a list = Nil | Cons ...</code>. Record types followed the same convention at declaration (<code>type addr = { ... }</code> was accepted), but the record <em>literal</em> <code>addr { ... }</code> only parsed for capitalized names, so a lowercase one fell through to a variable followed by a block and failed with "expected ';' or '}' in block" — an error far from its cause. A registered record name of any case followed by <code>{</code> now parses as a record literal; nested updates like <code>{ p | home = { p.home | city = ... } }</code> work throughout. Separately, the ledger named a function <code>acct</code>, which collided with POSIX <code>acct(2)</code> at C compile time; a batch of common short POSIX names (<code>acct</code>, <code>dup</code>, <code>read</code>, <code>write</code>, <code>open</code>, <code>close</code>, <code>time</code>, <code>stat</code>, ...) join the reserved-word sanitizer. That list is inherently incomplete — namespacing all user top-level names is the robust fix, deferred as a larger byte-stream change. <code>examples/ledger.mere</code> models double-entry accounting with nested record updates. suite: 2193 passed / 0 failed (3 new tests). One honest wrinkle unchanged: a record parameter that is updated must be annotated, so the update site knows its type — the same "annotate polymorphic params" rule as the numeric overload._</p> <hr> <h2 id="v0-1-52-2026-07-17">v0.1.52 — 2026-07-17</h2> <p>_Inner functions get uncurried too (the real win the gzip probe was pointing at). v0.1.27 gave curried TOP-LEVEL functions an uncurried <code>__direct</code> twin so a saturated N-arg call skips the closure chain; inner (nested) functions never got it, so a curried inner <strong>recursive</strong> function compiled to a chain of anonymous closures — allocating a fresh env from the never-freed region on every partial application AND every recursive step. In a hot loop that is catastrophic: a 4-arg curried inner rec fn called a million times allocated <strong>769 MB</strong> (the same work with a single tuple arg: 1.4 MB), and gzip's <code>huff_decode</code> made inflating 1 MB cost <strong>484 MB</strong>. Now curried inner-lifted functions (≥ 2 params, concrete types) also get a <code>__direct</code> twin, and saturated call sites — including the recursive self-call — use it. Measured: the 1M-iteration microbenchmark <strong>769 MB → 1.46 MB (~530x)</strong>; gzip inflate of 1 MB <strong>484 MB → 34 MB (~14x)</strong>, still byte-identical with a verified CRC-32. The single-param closure form stays for partial application, so the change is additive and byte-stable (self-host emission unchanged). suite: 2191 passed / 0 failed (3 new tests). This closes the memory question the C-2 gzip dogfood opened — it was inner-fn currying, not the bytes representation._</p> <hr> <h2 id="v0-1-51-2026-07-17">v0.1.51 — 2026-07-17</h2> <p>_Three C-codegen bugs a gzip inflater flushed out. Writing a real DEFLATE decompressor (stored + fixed + dynamic Huffman, ~300 lines) exercised the closure-lifting and pattern-matching machinery harder than any prior program, and each bug was an undeclared-identifier compile error the interpreter never saw:_</p> <ol> <li>_<strong>Reserved-name params.</strong> A parameter named after a C keyword</li> </ol> <p> (<code>index</code>) was declared raw but referenced via <code>c_safe_name</code> as <code>index_</code>. Fixed on both emission paths — lifted-fn params (<code>format_param</code>) and anonymous-closure adapters — where deeply curried inner functions land._</p> <ol> <li>_<strong>Cross-host capture confusion.</strong> A plain local variable <code>p</code> was</li> </ol> <p> dropped from its function's captures because a DIFFERENT function had an inner recursive helper also named <code>p</code>: the "exclude inner-lifted fn names from captures" filter used a global, last-write-wins source-name map. Now resolved per-host, so a local and an unrelated inner fn sharing a name stay distinct._</p> <ol> <li>_<strong>Container-typed match fallthrough.</strong> A <code>match</code> whose result type</li> </ol> <p> is a pointer container (<code>Vec</code>) emitted <code>(Vec___heap_int){0}</code> — an undeclared struct — for the non-exhaustive default arm, via <code>mono_variant_name</code> mangling. Pointer containers now zero to <code>NULL</code>._</p> <p>_With all three fixed, the inflater compiles and runs with no workarounds: it decompresses <code>gzip</code>-produced files (1 byte to 1 MB, stored / fixed / dynamic) byte-identically with a verified CRC-32. suite: 2188 passed / 0 failed (5 new regression tests)._</p> <hr> <h2 id="v0-1-50-2026-07-17">v0.1.50 — 2026-07-17</h2> <p>_The classics quartet (matmul, Game of Life, Sudoku, bignum): four textbook programs aimed at four suspected soft spots — nested <code>Vec[Vec[float]]</code> construction, read-current/write-next generation updates, mutate-and-undo backtracking over <code>vec_set</code>, and digit-vector arithmetic past the fixed-width int. <strong>All four ran correctly on interp and C with zero new bugs</strong>: the matrix product is exact, the glider translates (+2,+2) in 8 generations, the 9x9 puzzle solves (row0=534678912), and 30! comes out to all 33 digits (after 21! demonstrates the wrap — identically on both backends). After 26 releases of probe-driven fixes, that's a measurement of the suite's reach, and it's recorded as one. The single real pain was an ERROR MESSAGE: when the numeric overload defaults to int through a polymorphic helper (matmul's <code>mat_get</code>, whose element type is still a type variable at inference time), the eventual "expected <code>float</code>, got <code>int</code>" surfaces far from its cause. The unify hint now explains the defaulting and both escapes (annotate a parameter / ascribe an operand). All four programs join <code>examples/</code> (the Life one as <code>life_glider.mere</code> — <code>game_of_life.mere</code> already existed as the Phase 36 sugar showcase and stays untouched)._</p> <hr> <h2 id="v0-1-49-2026-07-17">v0.1.49 — 2026-07-17</h2> <p>_A pub/sub broker, and the bug it flushed out. The dogfood set out to force <code>select</code> (waiting on multiple channels at once) — and found it <strong>isn't needed</strong>: a broker that must react to publishes, subscriptions, and shutdown funnels everything through one command inbox as a <code>cmd</code> variant (the actor pattern), so it never waits on two channels simultaneously. The example also shows channels are first-class message payloads — a <code>Sub</code> command carries a subscriber's <code>Channel[int]</code> through the inbox. What the dogfood <strong>did</strong> force was a closure-lifting bug in the C backend: a recursive <code>loop</code> that calls a sibling helper whose own nested <code>rec go</code> closes over the helper's locals had those locals (<code>hn</code>, <code>hv</code>) leak into <code>loop</code>'s capture set. The transitive-capture fixpoint (which threads a callee's captures through its callers) added a callee's captures without skipping names already bound inside the caller, so <code>loop</code> was emitted as <code>__lifted_loop_N(bag, hn, hv, k)</code> — referencing <code>hn</code>/<code>hv</code> that aren't in its scope ("use of undeclared identifier"). Fixed by skipping any callee capture bound anywhere inside the caller's body. <code>examples/pubsub.mere</code> runs a two-topic broker with two subscribers on interp and C alike (<code>topic0=6 topic1=30</code>). E-1's last piece, <code>select</code>, stays deferred — not from lack of trying, but because the actor pattern subsumes it._</p> <hr> <h2 id="v0-1-48-2026-07-17">v0.1.48 — 2026-07-17</h2> <p>_Timed receive for supervisors (the second half of the concurrency arc): v0.1.47 let a worker pool shut down cleanly, but a <strong>supervisor still had no way to give up on a stuck worker</strong> — <code>channel_recv</code> on the results channel blocks forever if a job hangs. <strong>`channel_recv_timeout : Channel[a] -> int -> option[a]`</strong> blocks up to N milliseconds for a value and returns <code>None</code> on timeout (or once the channel is closed and drained), so a collector records the timeout and moves on instead of hanging the whole run. The C backend uses <code>pthread_cond_timedwait</code> against an absolute <code>CLOCK_REALTIME</code> deadline; the reference interpreter polls at 1 ms granularity (the stdlib has no timed condition wait). interp + C; Wasm and LLVM reject it with a pointed compile error. <code>examples/supervised_pool.mere</code> runs a pool where one job deliberately hangs and the supervisor collects the other five with a 300 ms budget (<code>results=5 timeouts=1</code>) on interp and C alike. Structured-concurrency cancellation is already expressible via <code>channel_close</code>; the one remaining E-1 piece is <code>select</code> over multiple channels, still waiting for a forcing program (a genuine multi-source wait)._</p> <hr> <h2 id="v0-1-47-2026-07-17">v0.1.47 — 2026-07-17</h2> <p>_Graceful shutdown for concurrency (found by a worker pool): the pool — main pushes N jobs, W workers pull and process, main collects — hit two walls at once. A worker's <code>channel_recv</code> loop blocks forever when the jobs run out, so <strong>there was no way to stop a worker and join it</strong>; and because the loop never returns, its type is bottom (<code>'a</code>), which the C backend can't emit ("unsupported C codegen type: 'a"). Both are the same missing primitive. <strong>`channel_close : Channel[a] -> unit`</strong> marks a channel done, and <strong>`channel_recv_opt : Channel[a] -> option[a]`</strong> blocks for a value but returns <code>None</code> once the channel is closed and drained — so a worker loops <code>match channel_recv_opt jobs with None -> ()</code></p> <table> <thead><tr><th>Some j -> ...; loop ()<code>, which terminates (returns unit, no longer</code></th></tr></thead> </thead></table> <p>bottom) and can be joined. <code>channel_recv</code> and <code>channel_send</code> on a closed channel now raise/abort instead of blocking or corrupting. interp + C (the native worker-pool / server target); Wasm and LLVM reject the two with a pointed compile error. <code>examples/worker_pool.mere</code> runs a 4-worker pool over 12 jobs and joins every worker cleanly. Closes the structured-concurrency gap (E-1) that had been waiting for a forcing program since the memory model landed._</p> <hr> <h2 id="v0-1-46-2026-07-16">v0.1.46 — 2026-07-16</h2> <p>_(Follow-up, no version bump) <code>examples/base64.mere</code>: a composition probe that confirms the day's three separately-shipped capabilities — the bitwise builtins, <code>read_file_bytes</code>, and <code>write_file_bytes</code> — compose in one program. RFC 4648 known-answer vectors pass, and passing a file path round-trips arbitrary binary byte-identically (<code>read_file_bytes → encode → decode → write_file_bytes</code>) on interp and C. No new bug surfaced — the value is the integration check itself._</p> <p>_Hex literals (a papercut two probes drove into the ground): both the SHA-256 round constants and the East Asian Width range table had to be written in decimal, because <code>0xFF</code> lexed as the int <code>0</code> followed by an identifier <code>xFF</code> ("unbound variable: xFF"). <code>0xFF</code> / <code>0Xff</code> now lex as ordinary ints — no separate type, same per-backend width — via <code>int_of_string</code>; a bare <code>0x</code> with no hex digit still reads as <code>0</code> then the identifier <code>x</code>. No octal / binary / digit-separator syntax (not yet forced). The lexer change is one branch; the value is that the next crypto or Unicode probe reads like the reference it's transcribed from._</p> <hr> <h2 id="v0-1-45-2026-07-16">v0.1.45 — 2026-07-16</h2> <p>_Columns, not codepoints (found by printing a table with Japanese cells): v0.1.38's codepoint view was the right first step and the wrong tool for alignment — <code>utf8_len</code> says 5 for こんにちは, a terminal draws it in 10 columns, and a product table with CJK rows comes out visibly ragged. <strong>`utf8_width`</strong> is the display width (East Asian Width, wcwidth-lite: CJK / fullwidth / emoji = 2 columns, combining marks = 0, halfwidth katakana = 1), and <strong>`pad_right` / `pad_left`</strong> pad on it. All three are prelude functions in pure Mere — UTF-8 decoded with plain div/mod arithmetic, the width table a dozen range checks in decimal (the lexer has no hex literals, which is now a recorded papercut) — so they landed on all four backends at once by construction. <code>examples/aligned_table.mere</code> renders a mixed ASCII / Japanese / emoji / halfwidth-katakana table with straight borders on interp and C alike._</p> <hr> <h2 id="v0-1-44-2026-07-16">v0.1.44 — 2026-07-16</h2> <p>_The picture that fixed the docs (found by a Mandelbrot renderer): the probe went in expecting to measure the "no float infix" tax the docs promised — and the docs were wrong in the language's favor. <strong>`+ - * /` and the comparisons had been numeric-overloaded for a while</strong> on interp, C, and Wasm; the reference still said prefix-only <code>f_add</code> style, and a docs-faithful reader would write nine needless prefix calls per formula. Three real gaps did surface around the stale entry, all fixed: <strong>unary minus was int-only</strong> (<code>-2.5</code> was a type error; negative float literals needed <code>f_neg</code>) — now overloaded like the binary operators on all four backends (fneg / f64.neg); <strong>the LLVM backend emitted `add i32` on double operands</strong> for float infix (invalid IR, and <code>icmp</code> for float comparisons) — now the fadd family and ordered fcmp; and <strong>the write half of the binary path was missing</strong> — <code>write_file_bytes : str -> Vec[R, int] -> unit</code> joins v0.1.43's reader, so PPM's raw P6 replaces the 2.6x-larger P3 ASCII escape. <code>examples/mandelbrot.mere</code> renders 400x300 in infix math and writes P6 that is pixel-identical to the P3 version. One honest wrinkle stays: the numeric overload resolves to float only on concretely-float operands, so unannotated fn params default to int — float-heavy code annotates its params. Docs corrected in both places._</p> <hr> <h2 id="v0-1-43-2026-07-16">v0.1.43 — 2026-07-16</h2> <p>_Bytes get in the door (found by a 30-line CRC-32 tool): the algorithm was trivial on the new bitwise builtins — the discovery was on the input side. <strong>`read_file` silently truncates binary data at the first 0x00 byte on the C backend</strong> (NUL-terminated <code>char*</code>), while the interpreter, whose strings carry NULs, read the same file correctly: a 25-byte file read as 2 bytes natively and produced a confidently wrong checksum. The str-is-bytes story was only true on interp. <strong>`read_file_bytes : str -> Vec[R, int]`</strong> is the binary-safe path — one int per byte, 0..255, the whole file, reusing the existing vec machinery instead of introducing a bytes type (8 bytes per byte is the honest cost until a program forces better). It gets the same construction-time region binding as <code>vec_new</code> (without it, the region tyvar stayed unresolved and functions taking the vec were silently never emitted by the C backend — the probe hit that too). interp + C for now; Wasm/LLVM reject it with a pointed compile error. <code>examples/crc32.mere</code> verifies against zlib on both text and NUL-bearing files; <code>read_file</code>'s docs now state the truncation divergence plainly._</p> <hr> <h2 id="v0-1-42-2026-07-16">v0.1.42 — 2026-07-16</h2> <p>_The real ALU (paying off the SHA-256 probe): <strong>bitwise builtins on all four backends</strong> — <code>bit_and</code> / <code>bit_or</code> / <code>bit_xor</code> / <code>bit_not</code> / <code>bit_shl</code> / <code>bit_shr</code>, on the backend's native int width, with <code>bit_shr</code> as the arithmetic shift. They lower to the machine operation everywhere: <code>&</code>-family operators on C, <code>i32.and</code>-family instructions on Wasm, <code>and i32</code>-family on LLVM, <code>land</code>-family on the interpreter. <code>examples/sha256.mere</code> dropped its div/mod fake ALU for them: one block went from ~29 ms (interpreted, bit-loop emulation) to <strong>6.7 µs native</strong> — about 4,300× — with all NIST vectors still passing on interp and C. Cleanups the rewrite surfaced: <code>abs</code>/<code>min</code>/<code>max</code>/<code>clamp</code> still used C <code>int</code> temporaries after v0.1.41 (silent truncation above 2^31, fixed); <code>str_of_int</code> on a variable under a top-level let referenced an undefined <code>show_int</code> on Wasm and LLVM (only <code>show</code> registered the helper, fixed on both); the LLVM backend now rejects out-of-range int literals at compile time like Wasm does — and the v0.1.41 changelog's claim that LLVM was i64 is corrected there: <strong>LLVM's int is i32</strong>, and widening it to 64-bit remains a deferred item with sha256 as the forcing program._</p> <hr> <h2 id="v0-1-41-2026-07-16">v0.1.41 — 2026-07-16</h2> <p>_One int, not four (found by writing SHA-256 in pure Mere): the probe aimed at the missing bitwise story and instead hit something under it — <strong>the C backend's int was C `int`, 32 bits</strong>, while the interpreter tested 63-bit semantics and the docs never said which. SHA-256's round constants (36 of them above 2^31) silently truncated and every digest came out wrong with zero diagnostics; the minimal repro is <code>2147483647 + 1</code>, which printed <code>-2147483648</code> natively and <code>2147483648</code> under the interpreter. <strong>The C backend's int is 64-bit (`long long`) now</strong>, with <code>LL</code>-suffixed literals so literal arithmetic doesn't wrap at 32 bits either, <code>%lld</code> show/json formats, and <code>atoll</code>/<code>strtoll</code> parsing. At the <code>extern fn</code> FFI boundary int deliberately stays C <code>int</code> — the functions users declare are libc/POSIX symbols whose ABI type IS the 32-bit int (declaring <code>getpid</code> as returning <code>long long</code> would read undefined upper register bits on arm64). The <strong>Wasm backend keeps its i32 int but now says so</strong>: an int literal outside <code>-2^31 .. 2^31-1</code> is a compile-time error with a source location instead of an <code>i32.const 4294967296</code> that only explodes later inside wat2wasm. The SHA-256 probe passes all NIST test vectors on interp and C; docs state each backend's width honestly. (This entry originally claimed LLVM was already i64 — measuring said otherwise: <strong>LLVM's int is i32</strong>, so it now gets the same out-of-range-literal compile error as Wasm, and the i64 widening is a known deferred item. The probe also uncovered an unrelated LLVM crash on this program, tracked separately.)_</p> <hr> <h2 id="v0-1-40-2026-07-16">v0.1.40 — 2026-07-16</h2> <p>_Error-handling ergonomics probe (an 8-step fallible config-loader written three ways): the verdict on the language was mostly good news — the <code>?</code> / <code>?!</code> early-return sugar from Phase 36 already turns a seven-level match pyramid into a flat sequence of bindings, and the prelude's <code>result_and_then</code> family covers combinator style. The probe found one genuine inconsistency: <strong>the `?` / `?!` lets were the only let form that rejected `;` as sugar for `in`</strong> — <code>let x = e?!; rest</code> was a parse error while every other <code>let x = e; rest</code> works. Fixed; both forms now accept both separators._</p> <hr> <h2 id="v0-1-39-2026-07-16">v0.1.39 — 2026-07-16</h2> <p>_Scale safety (found by sorting a million elements): <strong>`list_sort_by` is a stable merge sort now</strong>, and <strong>the prelude's list functions survive million-element lists</strong>. The insertion sort took ~2 s at 20k elements natively and O(n²) beyond — a million-element <code>list_sort</code> now runs in well under a second, still stable (ties keep input order; the merge is tail-recursive via a reversed accumulator, and the split avoids returning a tuple: a struct return compiles to an sret out-parameter in C, which quietly defeats clang's sibling-call optimization — that one cost an AddressSanitizer session to find). Ten more prelude functions were rewritten with accumulators after the probe showed the naive <code>Cons (f h, recurse)</code> shape overflowing the stack near a million elements: <code>list_len</code>, <code>list_map</code>, <code>list_filter</code>-adjacent take/zip, <code>list_append</code>, <code>list_concat</code>, <code>list_flat_map</code>, <code>range</code>, <code>list_max</code>, <code>list_min</code>. The derive family (<code>==</code> on a million-element list) was already safe. <code>list_sort_insert</code> remains for direct users._</p> <hr> <h2 id="v0-1-38-2026-07-16">v0.1.38 — 2026-07-16</h2> <p>_Unicode (found by ten minutes of typing Japanese at the language): <strong>the codepoint view of strings</strong>. A Mere <code>str</code> is — and stays — a byte string: <code>str_len "こんにちは"</code> is 15, <code>substring</code> can cut a character in half, and <code>str_rev</code> scrambles multibyte text; all documented rather than changed (byte indexing is what the FFI, the wire protocols, and the existing corpus rely on). What was missing was any way to work with <em>text</em>: two new builtins on all four backends — <code>utf8_len : str -> int</code> (codepoint count) and <code>utf8_chars : str -> str list</code> (split into codepoints; invalid bytes count as single units, so they never loop or throw) — plus prelude compositions <code>utf8_at</code>, <code>utf8_sub</code>, and <code>utf8_rev</code>, written in plain Mere on top of <code>utf8_chars</code> so every backend gets them for free. <code>utf8_rev "aあ😀b"</code> is <code>"b😀あa"</code> on interp, C, Wasm, and LLVM alike — the first new builtin family to land on all four backends at once (str_split's runtime scaffolding made LLVM cheap)._</p> <hr> <h2 id="v0-1-37-2026-07-15">v0.1.37 — 2026-07-15</h2> <p>_Memory model, ported to Wasm: <strong>`region R { }` reclaims on the Wasm backend</strong> — the sound version of the save/restore that Phase 16.4 removed as broken. Three parts make it sound where the old attempt was not: the block's result is <strong>deep-copied out</strong> (per-type <code>$__mcopy_<tag></code> fns, twice — once above the block's garbage, then down into the enclosing range after the bump restores; the ranges cannot overlap); <strong>escaping stores are compile errors</strong> (pushing a heap value into a container created outside the block, <code>map_set</code>, <code>strbuf_push</code> on an outer buffer, <code>channel_send</code>, <code>spawn</code>, and externs that register callbacks — a container created <em>inside</em> the block is free to mutate, it dies with the block); and <strong>escaping closures/containers/borrows are rejected via the result type</strong>. Wasm needs no thread-locals or heap blocks: a mark saved on the value stack and one scratch global do it._</p> <p>_Measured on the live 2048 with a per-move region around the key handler: the bump pointer stays at exactly 4,544 bytes across 30,000 moves — zero net allocation per move, zero traps. The same game previously burned ~8.4 KB per move and died at ~7,700. The remaining honest gap vs the C backend: no per-container storage (hence the escaping-store errors instead of C's copy-on-store), recorded in memory-model.md §3.5._</p> <hr> <h2 id="v0-1-36-2026-07-15">v0.1.36 — 2026-07-15</h2> <p>_Library hygiene, applied across contrib: <strong>importable libraries are main-free now</strong>. Five more libraries carried a demo main at the bottom of the file (the pattern v0.1.35 fixed for contrib/test), so importing them ran the demo — argparse, csv/writer, regex, regex/engine, and time. Each demo moved to <code>examples/<name>_demo.mere</code> and runs standalone. The self-host family (parser / typer / fmt / eval / codegen_wasm) keeps its inline demos deliberately: those are programs whose demo output is the cross-implementation test vector, not libraries._</p> <hr> <h2 id="v0-1-35-2026-07-15">v0.1.35 — 2026-07-15</h2> <p>_Test-framework dogfood (three small things it surfaced):_</p> <p>_<strong>Generic assertions confirmed working.</strong> <code>show</code> (like <code>==</code>, and like <code><</code> since v0.1.33) works through type variables — monomorphization plays the dictionary — so contrib/test's <code>assert_eq</code> is genuinely generic: a helper <code>fn s -> fn name -> fn x -> Test.assert_eq s name x x</code> asserts on ints, tuples, nested pairs, and prints failing values with no annotations. No language change was needed; the regression test pins it._</p> <p>_<strong>Library files must not carry a demo main.</strong> contrib/test's demo lived at the bottom of the library file, so every importer <em>ran</em> it (noise, an intentional FAIL, and the demo's exit status). The demo moved to <code>examples/test_framework_demo.mere</code>; the library is module-only now, like contrib/xml._</p> <p>_<strong>`-I` now works when running a file.</strong> The import search path flag was honored by <code>-c</code> / <code>-l</code> / <code>-w</code> but silently dropped by the interpreter path (<code>mere -I <dir> file.mere</code> failed to resolve imports that <code>mere -c -I <dir></code> accepted) — the run entry points now pass the search paths through, closing another CLI asymmetry (cousin of v0.1.29's)._</p> <hr> <h2 id="v0-1-34-2026-07-15">v0.1.34 — 2026-07-15</h2> <p>_Soundness (found by playing the live 2048 for ten thousand headless moves): <strong>`&&` and `||` now short-circuit on every backend</strong>. The interpreter and the C backend always short-circuited, but the Wasm backend emitted strict <code>i32.and</code> / <code>i32.or</code> and LLVM emitted eager <code>and i1</code> (behind a comment claiming the "MVP subset has no effects" — long obsolete: a trapping right-hand side IS an effect). The bounds-guard idiom <code>i < len && vec_get v i == x</code> therefore trapped on Wasm only — in production, <strong>97% of the live 2048's keypresses died silently</strong> in its stuck-detection (<code>r < 3 && bget b (i + 4) == v</code>), invisible because the DOM glue catches and logs closure exceptions. Both backends now lower <code>&&</code>/<code>||</code> to their If emission._</p> <p>_The same probe measured the Wasm page-lifetime allocation model (the memory-model work of v0.1.30–31 is C-only so far): the game burns ~8.4 KB of never-reclaimed bump per move and hits its 64 MB memory at move ~7,700 — a determined player kills the tab in under an hour. That number is now the forcing measurement for porting value reclamation to the Wasm backend._</p> <hr> <h2 id="v0-1-33-2026-07-15">v0.1.33 — 2026-07-15</h2> <p>_Polymorphic ordering: <strong>`<` / `<=` / `>` / `>=` now work through type variables</strong>, closing the gap derive-ord (v0.1.11) left open. The design is deliberately not a trait system: the scheme carries no constraint — instead <strong>monomorphization plays the dictionary's role</strong>. Every compiled instance of a polymorphic comparator compares at a concrete type, where the existing derive machinery (<code>cmp_<tag></code>) specializes; the interpreter compares structurally at runtime. This is exactly how <code>==</code> has worked through type variables all along — ordering simply joins it (the historical "unresolved comparand defaults to int" rule is gone; programs that used the default still typecheck, since instantiation covers them)._</p> <p>_Consequences for free: the prelude's <code>list_sort</code>, <code>list_max</code>, and <code>list_min</code> are now generic — <code>list_sort [(3, "c"), (1, "a")]</code> sorts tuples with no annotations and no comparator; a hand-written <code>fn a -> fn b -> a < b</code> instantiates at every use type (the generic pairing-heap example drops its annotated comparator). Instances are structural only — there is no way to override a type's ordering (the derive family's philosophy), <code>_by</code> variants remain for explicit control, and the parity scope is interp / C / Wasm, as with derive-ord._</p> <hr> <h2 id="v0-1-32-2026-07-15">v0.1.32 — 2026-07-15</h2> <p>_Cleanup release (three small fixes plus doc sync):_</p> <p>_<strong>Top-level / local name collision (invalid C).</strong> A local <code>let m = ...</code> inside any function that shared its name with a globalized top-level <code>let m</code> was emitted as an assignment to the file-scope global instead of declaring a shadowing local — the prelude's <code>list_max</code> (local <code>m</code>) plus a program-level <code>let m = map_new ()</code> produced C that didn't compile. The global-assignment form now fires only for the exact top-level spine bindings (matched by physical node identity), so same-named locals declare and shadow correctly._</p> <p>_<strong>Tuple exhaustiveness false positive.</strong> <code>match (h1, h2) with (HE, _) | (_, HE) | (HN _, HN _)</code> is exhaustive, but no single arm is total, so the checker warned "no wildcard arm for tuple" (found by the generic pairing heap's merge). Tuple scrutinees whose components all range over small finite spaces (bools / unit / registered variants) are now checked by enumerating the product; a genuinely missing combination is reported by example — <code>missing (Greenq, Greenq)</code> — instead of a generic complaint._</p> <p>_<strong>mem_to_str leak.</strong> It malloc'd and never freed; it now allocates in the thread's current region, so per-request region blocks reclaim byte-dialect strings too._</p> <p>_Also: <a href="memory-model.html">memory-model.md</a> gains §3.5 documenting the implemented v0.1.30-31 reclamation semantics (current region, copy-out, copy-on-store, per-message channel copies, backend notes)._</p> <hr> <h2 id="v0-1-31-2026-07-15">v0.1.31 — 2026-07-15</h2> <p>_Memory model (stage 2 — the payoff): <strong>`region R { }` now reclaims the values its body allocates</strong>. Value allocations (strings, cons cells, variant nodes) target a thread-local <strong>current region</strong> instead of hardcoding the never-freed default region; a region block makes itself current for its body, deep-copies its result out into the enclosing region (stage 1's <code>__mcopy</code> machinery), and releases. Closure envs and container structs deliberately stay in the default region (they carry identity), stores into containers are safe by stage 1's copy-on-store, <code>channel_send</code> deep-copies the payload into a per-message region (freed on <code>recv</code> after copying out into the receiver's current region — a sender's scratch can die while the message is in flight), a container cannot escape as a block result (the typer's region-escape check fires; a codegen guard backs it up), and <code>try_or</code> restores the current region when a <code>fail</code> longjmps past a block. Block regions are heap-acquired with a one-deep per-thread cache, so a per-iteration block costs a pointer swap and a bump reset — and, critically, no stack struct's address escapes, which is what lets clang keep tail-calling. The spawn trampoline frees a finished thread's cached region (<code>_Thread_local</code> has no destructor — a spawn-per-connection server leaked ~1 MB per closed connection without this) (<code>show</code>/<code>to_json</code>/float-formatting helpers are <code>noinline</code> for the same reason: their inlined <code>asprintf(&local)</code> silently broke sibling-call optimization and deep loops overflowed the stack)._</p> <p>_Measured: the idiomatic line-at-a-time counter — plain <code>read_line</code> + <code>str_len</code> in a per-line region — now runs at <strong>1.5 MB constant RSS over 8M lines</strong> (246 MB before; <code>wc -l</code> needs 2.5 MB). A 100k-iteration loop storing every 10,000th string into an outer map keeps exactly the stored data. Long-running servers can finally reclaim per-request memory in the string dialect, not just the byte dialect. Suite: 2093._</p> <hr> <h2 id="v0-1-30-2026-07-15">v0.1.30 — 2026-07-15</h2> <p>_Memory model (stage 1 of the per-request-reclamation plan): <strong>copy-on-store — containers own their contents</strong>. <code>map_set</code> deep-copies the key and value into the map's own region, and <code>vec_push</code> / <code>vec_set</code> copy the element, via per-type <code>__mcopy_<tag></code> functions specialized the same way the derive family (show / json / == / cmp) is: strings copy their bytes, tuples / records / variants copy structurally (cons cells and variant nodes re-allocate in the container's region), scalars and closures pass through, and nested containers copy as pointers (mutable identity and aliasing preserved — they own their own storage). Strings are immutable, so the copies are semantically unobservable; the point is lifetime: a stored value must not dangle when the storer's allocation scope is later reclaimed. This is the prerequisite for scoped string allocation (<code>region R { }</code> capturing str/cons allocations — the next stage), which is what finally makes long-running servers' per-request memory reclaimable. OwnedVec / StrBuf / Channel are deferred to that stage. Today's cost: one copy per store; today's benefit: none visible — by design._</p> <hr> <h2 id="v0-1-29-2026-07-15">v0.1.29 — 2026-07-15</h2> <p>_Soundness (mkv dogfood P2): <strong>sharing a mutable container across threads is now a compile error</strong>, and <strong>the compile path runs the same safety analyses as the run path</strong>. Two fixes:_</p> <p>_<strong>Send/Sync classification.</strong> Region-bound mutable containers (<code>Map</code> / <code>Vec</code> / <code>StrBuf</code>) are now explicitly <code>!Send && !Sync</code> — their runtimes are lock-free (linear-scan arrays / bump buffers), so a shared container across <code>spawn</code> is a data race. Previously the classifier fell through to "are all type args Send?", and the region-marker arg is a bare TyVar, judged optimistically — so a shared <code>Map</code> compiled fine and lost ~2% of concurrent writes in a real RESP-server stress test. <code>OwnedVec</code> stays Send/!Sync (drop type: single owner, movable). The blessed pattern is share-by-communicating: <code>Channel</code> remains Send+Sync, and the mkv actor model compiles unchanged._</p> <p>_<strong>The `-c` / `-l` / `-w` paths now run the safety analyses.</strong> The compile entry ran type inference only — channel-element Send obligations, borrow-conflict checking, and spawn-capture move analysis were silently skipped, so <code>mere file.mere</code> rejected programs that <code>mere -c file.mere</code> happily compiled (including capturing a region borrow in a spawned thread). All three checks now run before codegen on every backend._</p> <hr> <h2 id="v0-1-28-2026-07-15">v0.1.28 — 2026-07-15</h2> <p>_Fix (generic-PQ dogfood, two monomorphization bugs): a <strong>generic pairing heap</strong> (<code>type 'a heap = HEmpty | HNode of ('a * 'a heap list)</code> + comparator closures) ran correctly on the interpreter but failed to compile natively. Two independent root causes, both in the C backend's monomorphization:_</p> <p>_<strong>B-P2 — body-only tuple shapes were never collected.</strong> Tuple typedef collection walked main's AST and fn signatures, but not fn bodies — so a tuple that exists only as a body annotation (the <code>(h1, h2)</code> scrutinee of a poly fn's match, concrete only inside a monomorphized instance's cloned body) was referenced in the emitted C without ever being declared. Bodies are now walked too; the concreteness guard still skips unresolved polymorphic shapes._</p> <p>_<strong>B-P2b — no promotion to multi-instance.</strong> A poly fn's usage sites inside another poly fn's body only become scannable once that fn resolves. <code>hp_pop</code> was seen at one type (from main), single-resolved by unifying the original skeleton in place — destroying its polymorphism — and the later-discovered second usage (at int, inside <code>drain</code>) was emitted against the wrong instance's struct types. Every skeleton now keeps a pristine clone taken before any unification; single-resolved fns' bodies join the arrow-discovery scan; and a fn already resolved at one type is promoted to multi-instance when a second type shows up._</p> <p>_With both fixed, the generic heap and a Dijkstra built on it (new <code>examples/generic_heap_dijkstra.mere</code>) run natively, byte-identical to the interpreter. Suite: 2081._</p> <hr> <h2 id="v0-1-27-2026-07-14">v0.1.27 — 2026-07-14</h2> <p>_Optimization (mlog dogfood P4, the big one): <strong>saturated calls to curried top-level fns compile to a direct N-ary C call</strong>. Level-by-level application allocated a closure env in the default region <strong>per call</strong>, through the region lock — measured as O(iterations) permanent memory in every multi-argument hot loop: a byte-at-a-time line counter held 2.1 GB RSS over 8M lines. For each top-level <code>f = fn p1 -> .. -> fn pN -> body</code> (N ≥ 2, concrete types) the backend now also emits <code>f__direct(p1, .., pN)</code> and compiles exactly-saturated call sites straight to it — argument temporaries pin the interpreter's left-to-right evaluation order, and self-recursion becomes a C self tail call. Partial applications and first-class uses keep the curried chain. The same line counter is now <strong>1.5 MB RSS, constant across input size</strong> (below <code>wc -l</code>), and 300 MB of input streams in 0.13 s. Constant-memory streaming is genuinely expressible now; what still accumulates is the string dialect's per-line <code>str</code> values (the open type-level lifetime question)._</p> <hr> <h2 id="v0-1-26-2026-07-14">v0.1.26 — 2026-07-14</h2> <p>_Capability (mlog dogfood P1): <strong>`read_line` on the C backend</strong>. It was interpreter-only — the sixth member of that family (print_err / file_exists / print_no_nl / random_int / file_size) — so a native streaming line processor could not be written at all (<code>read_stdin</code> slurps the whole input by design). <code>__lang_read_line</code> reads one stdin line without the trailing newline, <code>""</code> on EOF, matching the interpreter. Found by measuring memory behaviour of line-at-a-time processing for the constant-memory streaming question._</p> <hr> <h2 id="v0-1-25-2026-07-14">v0.1.25 — 2026-07-14</h2> <p>_Fix (mkv dogfood, long-running processes): <strong>regions grow instead of aborting</strong>. The region allocator was a single fixed-cap bump block (default region: 4 MB) that aborted with <code>region OOM</code> on overflow — a long-running server's per-command allocations (reply strings, cons cells, tuples) exhausted it after a few thousand requests. A region is now a chain of bump blocks: on overflow a geometrically larger block is chained on. Blocks never move, so existing pointers stay valid, and <code>region R { }</code> frees the whole chain at scope exit. Also hardened the native byte arena: <code>mem_alloc</code> / <code>str_ptr</code> share one bump pointer across spawned threads — it is now mutex-guarded and bounds-checked (it previously raced and silently overflowed past the arena). Under a sustained 80k-command concurrent load the RESP server now runs clean where it previously aborted at ~8k. The honest remaining edge: growth is not reclamation — per-request memory still accumulates for the process lifetime (region-scoped strings need type-level lifetime tracking; see the memory-model open questions)._</p> <hr> <h2 id="v0-1-24-2026-07-14">v0.1.24 — 2026-07-14</h2> <p>_Capability (mkv dogfood, T4 wire-protocol server): native TCP <strong>server</strong> primitives. <code>tcp_listen : int -> int</code> (socket + <code>SO_REUSEADDR</code> + bind + listen, returns the listening fd) and <code>tcp_accept : int -> int</code> (blocking accept, returns the client fd) join the existing <code>native_ffi_names</code>, emitted as <code>static</code> impls against the same flat arena + POSIX sockets that back <code>tcp_connect</code>/<code>tcp_read</code>/<code>tcp_write</code>. A Mere program can now be a TCP server, not just a client — the server-side mirror of the pg/redis client FFI. <code>SIGPIPE</code> is ignored so a client disconnecting mid-write drops the connection rather than the whole process. This is the missing capability behind a Redis-wire (RESP) key-value server; the earlier <code>http_serve</code> was HTTP-specific and single-connection._</p> <hr> <h2 id="v0-1-23-2026-07-14">v0.1.23 — 2026-07-14</h2> <p>_Fix (docs site): the Mere SSG (<code>contrib/site/build.mere</code>) parsed its CLI args assuming <code>args()</code> still prepended the script path — the v0.1.12 <code>args()</code> consistency fix shifted that by one, so <code>input_dir</code> resolved to the output dir and the site built <strong>0 markdown pages</strong> (tour.html / tutorial.html etc. 404'd). Updated build.mere to the current <code>args()</code> contract (first positional = input dir). A dogfood consumer that relied on the old behaviour — exactly the interp/native <code>args()</code> mismatch N3 was about, biting a Mere program this time._</p> <p><strong>Fix: same-named inner functions no longer collide when lifted</strong> (2048 dogfood P3). Two inner fns sharing a source name within one top-level function — e.g. a <code>let rec go</code> in each branch of an <code>if</code> — both lifted to the top level, but each backend's inner-fn resolution map is keyed by the source name, so the second <code>go</code> overwrote the first and both call sites dispatched to the wrong one. <strong>Cross-backend</strong>: the C and Wasm backends both mis-executed (silent wrong results); the interpreter was correct. A new shared pre-pass (<code>Ast.uniquify_inner_fns_program</code>, run next to the par_map lowering) α-renames on collision — the first use of a name keeps it, a later reuse becomes <code><name>_uq<N></code> with its references rewritten — fixing every backend in one place. Collision-free inner names (the common case) are untouched, so nothing changes in ordinary code or its pretty-printing.</p> <p>2069 tests.</p> <hr> <h2 id="v0-1-22-2026-07-14">v0.1.22 — 2026-07-14</h2> <p><strong>Wasm backend: `spawn` / `join` / `channel_*` now respect shadowing</strong> (2048 dogfood P2). A user binding named <code>spawn</code> — a game's tile spawner — was dispatched to the <em>concurrency</em> builtin, silently turning the module into a threaded one (shared-memory import + <code>$mere_spawn</code>), which the plain browser host rejects. The same bug family the C backend fixed for <code>join</code> in the mk dogfood (dd17b8a): the dispatch matched the name without asking whether it was rebound. All five concurrency dispatches now check the local scope / top-level fns / inner-lifted fns first, so a shadowed name falls through to ordinary application while genuine <code>spawn</code> still lowers to <code>$mere_spawn</code>.</p> <p>Also in the frontend FFI (no compiler change): <code>contrib/dom</code> gained <code>dom_on_key : (str -> unit) -> unit</code> — a global keydown listener passing the key name to a Mere closure; the browser counterpart to native <code>read_key</code>.</p> <p>2067 tests.</p> <hr> <h2 id="v0-1-21-2026-07-14">v0.1.21 — 2026-07-14</h2> <p><strong>`file_size` — a binary file's true byte length</strong> (mwasm dogfood P1). <code>read_file</code> is binary-safe (the buffer holds every byte and <code>char_at</code> / <code>ord</code> index past NULs correctly, on interp <em>and</em> C native), but <code>str_len</code> is <code>strlen</code> on the C backend and stops at the leading NUL — so a <code>.wasm</code> (magic <code>\0asm</code>) reported length 0, and a binary walk couldn't bound its loop. Added <code>file_size : str -> int</code> (stat's <code>st_size</code>, next to <code>file_mtime</code>), on interp and C. With <code>(buffer, size)</code> carried explicitly, the NUL-safe <code>char_at</code> / <code>ord</code> / <code>substring</code> make binary parsing expressible — no dedicated bytes type needed yet. Driving app: <code>mwasm</code>, a WASM binary inspector that reads the compiler's own output.</p> <p>2065 tests.</p> <hr> <h2 id="v0-1-20-2026-07-14">v0.1.20 — 2026-07-14</h2> <p><strong>`random_int` now works on the C backend</strong> (mrog dogfood P3). The game's wandering ghost picks a random direction each turn; <code>random_int</code> existed only in the interpreter — the third interpreter-only builtin this dogfood family has flushed out (after <code>print_err</code>, <code>file_exists</code>, <code>print_no_nl</code>). Added <code>__lang_random_int</code> (seeded once from time^pid, uniform <code>[0, n)</code>, fails on <code>n <= 0</code> like the interpreter). mrog M3 — ghost + game over — now runs natively.</p> <p>2064 tests.</p> <hr> <h2 id="v0-1-19-2026-07-13">v0.1.19 — 2026-07-13</h2> <p><strong>`print_no_nl` now works on the C backend</strong> (mrog dogfood P2). A TUI's cursor-control sequences must be written without a newline and without line buffering; <code>print_no_nl</code> existed only in the interpreter (the same family as <code>print_err</code> / <code>file_exists</code> before it). Added the case (<code>fputs(s, stdout); fflush(stdout)</code>). With it, mrog's full redraw loop — ANSI clear+home, map with <code>@</code> overlay, hjkl movement, wall collision, gold pickup — runs natively, byte-identical to the interpreter.</p> <p>2063 tests.</p> <hr> <h2 id="v0-1-18-2026-07-13">v0.1.18 — 2026-07-13</h2> <p><strong>Interactive terminal: `tty_raw` / `tty_restore` / `read_key`</strong> (mrog dogfood P1). Mere had only line-buffered input (<code>read_line</code> waits for Enter, with echo), so an interactive TUI couldn't be expressed at all. Three new builtins — interpreter (Unix termios) and C native (<code>tcgetattr</code>/<code>tcsetattr</code>):</p> <ul> <li><code>tty_raw : unit -> unit</code> — raw mode on stdin (no echo, no canonical</li> </ul> <p> buffering; ISIG stays on so Ctrl-C works). No-op when stdin isn't a tty, so piped tests behave.</p> <ul> <li><code>tty_restore : unit -> unit</code> — put back the termios saved by the first</li> </ul> <p> <code>tty_raw</code>.</p> <ul> <li><code>read_key : unit -> str</code> — blocking single-byte read; <code>""</code> on EOF.</li> </ul> <p>ANSI <em>output</em> already worked (<code>chr 27 ++ "[2J"</code>), so with key input the interactive read → update → redraw loop is now expressible. Driving app: <code>mrog</code>, a tiny terminal roguelike.</p> <p>2062 tests.</p> <hr> <h2 id="v0-1-17-2026-07-13">v0.1.17 — 2026-07-13</h2> <p><strong>C backend: closures that call an inner-lifted fn now carry its captures</strong> (mk dogfood P5). An inline lambda passed to <code>par_map</code> that captures an enclosing function's parameter gets inner-lifted, and its call sites inject the captured variable as a leading argument. But when that call site sat inside <em>another</em> closure — the <code>par_map</code> lowering's spawn lambda — the spawn closure's env didn't include the injected variable, and the emitted C referenced an undeclared identifier. The anonymous-closure capture computation now unions in the captures of any inner-lifted fn the body calls (one level suffices — lifted captures are already transitively closed by the Phase 45 fixpoint). Found by <code>mk</code>'s parallel dependency groups (<code>name [a b c]&: cmd</code>), which now build and run natively: three parallel 0.3s deps complete in ~0.38s, and a failing parallel dep propagates its exit code.</p> <p>2058 tests.</p> <hr> <h2 id="v0-1-16-2026-07-13">v0.1.16 — 2026-07-13</h2> <p><strong>`run` is now truly parallel under `spawn` / `par_map`</strong> (mk dogfood P4). <code>run</code> was lowered to libc <code>system()</code> (and OCaml's <code>Sys.command</code>, which wraps it) — and on macOS, concurrent <code>system()</code> calls serialize behind a global lock, so <code>par_map (fn c -> run c) cmds</code> executed commands one at a time: three parallel 0.3s sleeps took ~1.0s (interp) / ~1.6s (native). Confirmed with a C probe (3 threads × <code>system("sleep 0.3")</code> = 1.01s; <code>posix_spawn</code> = 0.32s). Reimplemented without <code>system()</code>:</p> <ul> <li>interp: <code>Unix.create_process "/bin/sh" ["sh";"-c";cmd]</code> + <code>waitpid</code></li> <li>C native: <code>posix_spawn</code> + <code>waitpid</code> (<code>128 + signal</code> on signaled exit)</li> </ul> <p>Three parallel 0.3s commands now take ~0.36s on both backends. Exit-code propagation is unchanged. This is what a parallel task runner needs — the <code>mk</code> dogfood's M5.</p> <p>2057 tests.</p> <hr> <h2 id="v0-1-15-2026-07-13">v0.1.15 — 2026-07-13</h2> <p><strong>`file_exists` now works on the C backend</strong> (mk dogfood P3). Incremental builds skip a task when its output exists and is newer than its inputs; the "exists" check guards <code>file_mtime</code> (which raises on a missing path). <code>file_mtime</code> was already on C, but <code>file_exists</code> was interpreter-only, so the native build failed with <code>use of undeclared identifier 'file_exists'</code>. Added the case (<code>stat(path, &st) == 0</code>, next to <code>__lang_file_mtime</code>). With this, the <code>mk</code> task runner's incremental mode (<code>name (out: in1 in2): cmd</code>) builds and runs natively — and its float mtime comparison rides the v0.1.11 structural <code>></code>.</p> <p>2057 tests.</p> <hr> <h2 id="v0-1-14-2026-07-13">v0.1.14 — 2026-07-13</h2> <p><strong>`print_err` now works on the C backend</strong> (mk dogfood P2). The native backend lowered <code>print</code> to <code>puts</code> but had no <code>print_err</code>, so a compiled CLI couldn't write diagnostics to stderr — a native build using it failed with <code>use of undeclared identifier 'print_err'</code>. Added the case (<code>fprintf(stderr, "%s\n", …)</code>, mirroring <code>print</code> → <code>puts</code>); the docs' 3-backend claim for <code>print_err</code> is now actually true.</p> <p>2056 tests.</p> <hr> <h2 id="v0-1-13-2026-07-13">v0.1.13 — 2026-07-13</h2> <p><strong>`run` — Mere can start external programs.</strong> A new <code>run : str -> int</code> builtin executes a command line through the shell, inherits stdio, and returns the exit code (interpreter via <code>Sys.command</code>; C native via <code>system</code> + <code>WEXITSTATUS</code>). This is the capability the new <code>mk</code> task-runner dogfood needed on day one — a whole class of tools (build systems, task runners, anything that shells out) was previously inexpressible. Exit codes propagate identically under interp and native.</p> <p>2054 tests.</p> <hr> <h2 id="v0-1-12-2026-07-13">v0.1.12 — 2026-07-13</h2> <p>Papercut batch — small dogfood findings paid back.</p> <ul> <li><strong>`args()` is now consistent between the interpreter and native binaries</strong></li> </ul> <p> (mstat N3). Both return only the program's own arguments, dropping the interpreter's script path / the binary name; the CLI entry point hands the post-script args to the <code>args()</code> builtin instead of it reading <code>Sys.argv[1..]</code>. An argument-driven CLI now behaves the same under <code>mere app.mere a b c</code> and the compiled <code>./app a b c</code>.</p> <ul> <li><strong>`str_of_float` renders whole-valued floats as `550.0`, not `550.`</strong></li> </ul> <p> (mstat N4). Fixed identically across interp / C / Wasm (and the <code>show</code> path), so all backends still agree and the output round-trips through <code>float_of_str</code>.</p> <p>Deferred: bare <code>None</code> needing a type annotation is an inference matter, not a papercut, and stays open.</p> <p>2052 tests.</p> <hr> <h2 id="v0-1-11-2026-07-13">v0.1.11 — 2026-07-13</h2> <p><strong>derive-ord: structural ordering, the sibling of structural equality.</strong> <code>< <= > >=</code> now work on any concrete type, not just <code>int</code> / <code>float</code> / <code>str</code> — completing the compile-time-specialized "derive family" (<code>show</code> / <code>to_json</code> / <code>of_json</code> / <code>==</code> / <strong>`<`</strong>).</p> <ul> <li><strong>Structural comparison</strong> on tuples, records, lists, and variants, on</li> </ul> <p> <strong>interp / C / Wasm</strong>, all agreeing byte-for-byte. Lexicographic: tuples and records by declared field order, lists element-wise (shorter prefix is smaller), variants by <strong>declaration order</strong> then payload. Emitted as a <code>cmp_<tag></code> function per type (the ordering sibling of <code>eq_<tag></code>), and as <code>value_compare</code> in the interpreter, ordering variants by the same tag order the codegen assigns.</p> <ul> <li><code>list_sort_by</code> with an annotated comparator now sorts a list of any</li> </ul> <p> structural type (<code>float</code> / record / tuple / …), closing the mstat N5 finding's practical half.</p> <ul> <li>Backward compatible: an unresolved comparator type variable still</li> </ul> <p> defaults to <code>int</code>, so <code>fn a -> fn b -> a < b</code> and the bare <code>list_sort</code> stay <code>int</code>. A fully-polymorphic <code>list_sort</code> needs ad-hoc-polymorphism resolution and remains deferred (documented in the stdlib reference).</p> <p>2052 tests.</p> <hr> <h2 id="v0-1-10-2026-07-12">v0.1.10 — 2026-07-12</h2> <p><strong>Bootstrap fixpoint: Mere is truly self-hosting.</strong> The Mere-in-Mere compiler, compiled by itself and run as wasm, produces byte-identical output to the reference — and that output runs correctly.</p> <ul> <li><strong>Self-host TCO (Stage 55f)</strong>: the self-host codegen now emits</li> </ul> <p> <code>return_call_indirect</code> (guaranteed tail calls) for tail-position closure calls, tracked via a <code>tail</code> flag threaded through if / let / letrec / match. Deep tail recursion in self-compiled code stays stack-flat (a 200000-deep counter completes; it overflowed before).</p> <ul> <li><strong>Three latent self-compilation bugs fixed (Stage 55g)</strong> — found by</li> </ul> <p> trace-bisecting the self-compiled compiler until the bootstrap fixpoint held:</p> <ol> <ol> <li>Pattern checks: a <code>PConstr</code> payload sub-check ran eagerly even when</li> </ol> </ol> <p> the tag didn't match, dereferencing garbage (out-of-bounds traps). Payload checks now short-circuit.</p> <ol> <ol> <li>Var-vs-var string <code>==</code> lowers to pointer equality in the un-typed</li> </ol> </ol> <p> self-host codegen; <code>member_str</code> (and parser friends) switched to explicit <code>str_eq</code> — ghost closure captures are gone.</p> <ol> <ol> <li>The self-host lexer was missing the <code>\r</code> escape, corrupting the</li> </ol> </ol> <p> data-segment escaper's CR needle ("Err" emitted as "E\0d\0d").</p> <ul> <li><strong>Fixpoint regression test</strong>: the suite now compiles a program with the</li> </ul> <p> interpreter-run compiler AND the self-compiled compiler and asserts the WAT outputs are byte-identical.</p> <ul> <li>Also: <code>let rec</code> written directly in the main expression now lifts on</li> </ul> <p> C + Wasm (mstat N6) instead of erroring.</p> <p>2035 tests.</p> <hr> <h2 id="v0-1-9-2026-07-12">v0.1.9 — 2026-07-12</h2> <p>Float operator overloading + libm name collisions — driven by the <code>mstat</code> numeric-CLI dogfood.</p> <ul> <li><strong>Infix operators on float</strong>: <code>+ - * /</code> and <code>< <= > >=</code> now work on</li> </ul> <p> <code>float</code>, not just <code>int</code> / <code>str</code>. Dispatched on the operand type at codegen (the same compile-time specialization as <code>show</code> / <code>to_json</code> / <code>eq</code>; no trait machinery). <code>Mod</code> stays int-only. All four backends' arithmetic/ordering covered. Also fixes a latent C bug where a whole-valued float literal emitted as <code>7</code> (via <code>%.17g</code>), making <code>7.0 / 2.0</code> integer division. <em>Caveat:</em> operands must be concretely float-typed — an unannotated <code>fn a -> fn b -> a < b</code> still defaults to int, so the default <code>list_sort</code> stays int (sort floats with an annotated comparator).</p> <ul> <li><strong>libm / POSIX name collisions</strong>: a user fn named <code>fmin</code> / <code>fmax</code> / … now</li> </ul> <p> gets rehomed (<code>fmin_</code>) instead of clashing with <code><math.h></code> in the C backend (<code>conflicting types for 'fmin'</code>). Same treatment as <code>main</code>.</p> <p>2033 tests.</p> <hr> <h2 id="v0-1-8-2026-07-12">v0.1.8 — 2026-07-12</h2> <p><code>of_json</code> / <code>of_json_opt</code> on the Wasm backend — backend parity.</p> <ul> <li><strong>Wasm `of_json` / `of_json_opt`</strong>: ported the JSON deserializers to the</li> </ul> <p> Wasm backend, so all three shipping backends (interp / C / Wasm) have them — matching <code>to_json</code>'s coverage (LLVM excluded, it lacks <code>to_json</code> too). A WAT JSON-parser runtime builds a generic tree in linear memory; per-type <code>$__ojnode_<tag></code> decoders build the typed value; strict <code>of_json</code> traps on error, <code>of_json_opt</code> returns <code>None</code>. This un-blocks the mere-blog dogfood's <strong>wasm deploy path</strong> (native-only since it adopted <code>of_json_opt</code> in v0.1.7).</p> <p>2022 tests.</p> <hr> <h2 id="v0-1-7-2026-07-11">v0.1.7 — 2026-07-11</h2> <p><code>of_json</code> (derive-style JSON parsing) + docs push + ergonomics.</p> <ul> <li><strong>`of_json` / `of_json_opt`</strong>: the deserialization mirror of <code>to_json</code>.</li> </ul> <p> <code>of_json : str -> 'a</code> parses JSON into a typed value, driven by the result type at the call site (an annotation <code>(of_json s : T)</code>) — JSON object → record fields by name, array → list / tuple, <code>null</code>/value → option, string / <code>{"Ctor":…}</code> → variant. Same compile-time specialization as <code>show</code> / <code>to_json</code>; interp + C (native) backends. <code>of_json_opt : str -> 'a option</code> is the non-crashing sibling (returns <code>None</code> on any parse / shape error) — safe for untrusted input like HTTP request bodies. Closed the mere-blog dogfood's request-parsing gap (PAIN B5): its handlers now decode into typed request records instead of plucking string fields, verified end-to-end on the native binary.</p> <ul> <li><strong>`option` is a transparent JSON nullable</strong>: <code>to_json</code> now encodes</li> </ul> <p> <code>None</code> as <code>null</code> and <code>Some x</code> as <code>x</code> (was the tagged <code>{"Some":x}</code>) on all three backends, the idiomatic API encoding and symmetric with <code>of_json</code>.</p> <ul> <li><strong>Native `exit n`</strong>: the C backend emits libc <code>exit()</code>, so a native CLI</li> </ul> <p> can set its process exit code (closed mq PAIN P1's last item).</p> <ul> <li><strong>Trailing commas</strong>: allowed in list and tuple literals (<code>[1, 2, 3,]</code>,</li> </ul> <p> <code>(a, b,)</code>); records already allowed them.</p> <ul> <li><strong>Docs</strong>: a one-page <a href="tour.html">Tour of Mere</a> feature showcase, and the</li> </ul> <p> SSG's nav / index are now curated (Start here → tutorials → reference) with real page titles. Site live at merelang.org.</p> <p>2019 tests.</p> <hr> <h2 id="v0-1-6-2026-07-11">v0.1.6 — 2026-07-11</h2> <p><code>to_json</code> (derive-style JSON) + native password-auth Postgres.</p> <ul> <li><strong>`to_json`</strong>: a polymorphic builtin (<code>forall 'a. 'a -> str</code>, the JSON</li> </ul> <p> sibling of <code>show</code>) that serializes any value structurally — records become JSON objects (dropping the type name), lists/tuples arrays, nullary constructors <code>"Name"</code>, and payload constructors <code>{"Name": payload}</code>. Same compile-time-specialization approach as <code>show</code> (no trait machinery); works on interp / C / Wasm. Removes hand-written record→JSON writers (the mere-blog dogfood's PAIN B3).</p> <ul> <li><strong>Native SCRAM-SHA-256</strong>: real SHA-256 / HMAC / PBKDF2 / base64 in the C</li> </ul> <p> runtime, so a native binary authenticates to a password Postgres over plaintext (TLS still pending). Verified against a scram-sha-256 server.</p> <ul> <li><strong>Native redis/mysql</strong>: two arena↔hex helpers complete the byte-buffer</li> </ul> <p> FFI, so the whole <code>contrib/db</code> family — not just pg — compiles to native binaries. Verified driving a real redis.</p> <p>1992 tests.</p> <hr> <h2 id="v0-1-5-2026-07-10">v0.1.5 — 2026-07-10</h2> <p><strong>Native full-stack</strong>: a web + Postgres app now compiles to a single native binary. Driven by the mere-blog dogfood.</p> <ul> <li><strong>Native FFI runtime (C backend)</strong>: the <code>tcp_*</code> / <code>mem_*</code> / <code>str_ptr</code></li> </ul> <p> externs that <code>contrib/db</code> (pg / mysql / redis) speak — previously host-provided over the Wasm linear memory — get native implementations: a Wasm-style flat byte arena (32-bit offsets) plus POSIX sockets. So the pure-Mere wire-protocol drivers run in a native binary.</p> <ul> <li><strong>Native HTTP server</strong>: <code>http_serve</code> runs a POSIX accept loop with the</li> </ul> <p> same handler contract as the Node host (<code>"METHOD URL"</code> + <code>http_set_*</code> / <code>http_get_header</code> / <code>http_current_body</code>).</p> <ul> <li><strong>Native crypto/util</strong>: a real FIPS 180-4 <code>sha256_hex</code> and a</li> </ul> <p> <code>/dev/urandom</code>-backed <code>gen_request_id</code> (password hashing + session ids).</p> <ul> <li>Result: <code>mere -c app.mere | clang</code> yields a self-contained native web+DB</li> </ul> <p> server — no Node, no Wasm. (Postgres SSL / SCRAM auth on native are stubbed for now; use trust / plaintext.)</p> <ul> <li><strong>`let` main diagnostic</strong>: a top-level <code>let main = …</code> now warns on the</li> </ul> <p> compile paths (not just the interpreter) with a message pointing at the entry-point convention, instead of surfacing a cryptic downstream <code>wat2wasm</code> clash.</p> <ul> <li><strong>Fix</strong>: the C backend escaped <code>\n</code> / <code>\t</code> in string literals but not</li> </ul> <p> <code>\r</code>, so a carriage return broke the emitted C string (hit compiling pg's COPY unescape).</p> <p>1978 tests.</p> <hr> <h2 id="v0-1-4-2026-07-10">v0.1.4 — 2026-07-10</h2> <p>Driven by the mere-blog dogfood (a Rails-ish blog on <code>contrib/http</code> + <code>contrib/db/pg</code>).</p> <ul> <li><strong>`let` constructor/record patterns on all backends</strong>: <code>let Ctor (a, b)</code></li> </ul> <p> = e<code> and </code>let Rec { f = x } = e<code> now compile on the C, Wasm, and LLVM backends (previously only the interpreter accepted them; the compiled backends handled just </code>P_var<code> / tuple / wildcard). Each backend desugars the general case to a single-arm match.</code></p> <ul> <li><strong>`contrib/orm`</strong>: a small, DB-agnostic typed layer — row decoders</li> </ul> <p> (<code>Orm.dec_int</code> / <code>dec_str</code> / <code>dec_bool</code> / <code>dec_str_opt</code> + <code>decode_rows</code>) over the <code>str option list</code> rows the <code>contrib/db</code> drivers return, plus matching JSON encoders (<code>Orm.enc_int</code> / <code>enc_str</code> / <code>enc_bool</code> / <code>enc_str_opt</code> / <code>enc_obj</code> / <code>enc_arr</code>). The ML answer to reflection-based ORMs.</p> <p>1972 tests.</p> <hr> <h2 id="v0-1-3-2026-07-10">v0.1.3 — 2026-07-10</h2> <p>Closes the last dogfood finding from the mq CLI.</p> <ul> <li><strong>String ordering</strong>: <code><</code>, <code><=</code>, <code>></code>, <code>>=</code> now work directly on <code>str</code>,</li> </ul> <p> comparing lexicographically (in addition to <code>int</code>). Previously the typer forced both operands to <code>int</code>, so <code>"a" < "b"</code> failed to typecheck and callers had to route through <code>str_compare</code>/<code>ord</code>. Works across all four backends (interp / C / Wasm / LLVM); the <code>int</code> default for unresolved operands is preserved, so existing code is unaffected.</p> <ul> <li><strong>contrib/json fix</strong>: v0.1.2 claimed the serialiser had moved into</li> </ul> <p> <code>module Json</code>, but the functions were dropped rather than re-added, so the release actually shipped a parser-only <code>json.mere</code>. They are now restored inside the module — <code>Json.to_json_str (Json.parse_json s)</code> type-checks and round-trips as intended.</p> <p>1961 tests.</p> <hr> <h2 id="v0-1-2-2026-07-10">v0.1.2 — 2026-07-10</h2> <p>More dogfood-driven fixes (from the mq CLI).</p> <ul> <li><strong>`read_stdin`</strong>: reads all of stdin as a <code>str</code> (interp + C backend), so</li> </ul> <p> CLIs can filter piped input (<code>echo … | mq '.query'</code>).</p> <ul> <li><strong>contrib/json</strong>: the serialiser (<code>to_json_str</code> / <code>to_pretty_str</code>) moved</li> </ul> <p> into <code>module Json</code> and <code>writer.mere</code> was removed, so parser and writer share one <code>json</code> type — <code>to_pretty_str (parse_json s)</code> now composes.</p> <p>1947 tests.</p> <hr> <h2 id="v0-1-1-2026-07-10">v0.1.1 — 2026-07-10</h2> <p>Fixes surfaced by dogfooding two real apps on top of Mere: a realtime collaborative editor (mere-notes, Wasm) and a native <code>jq</code>-like CLI (mq, C backend). Mostly C-backend and contrib hardening.</p> <ul> <li><strong>Native CLI I/O</strong>: the C backend implements <code>args()</code> (argv → str list),</li> </ul> <p> so a compiled Mere program can read its arguments.</p> <ul> <li><strong>C backend correctness</strong>: respect shadowing of the <code>join</code> builtin (a</li> </ul> <p> local <code>join</code> no longer compiles to <code>pthread_join</code>); fix cross-host capture merging in inner-fn lifting (composing two modules that each have a same-named inner fn no longer corrupts captures); mask <code>chr</code>'s byte index so out-of-range input can't read past the char table.</p> <ul> <li><strong>C backend parity / ergonomics</strong>: <code>str_eq</code> works as a function (not</li> </ul> <p> just the <code>==</code> operator); <code>str_of_int</code> pulls in the <code>show_int</code> helper; type annotations accept qualified module types (<code>Module.t</code>).</p> <ul> <li><strong>contrib hygiene</strong>: <code>contrib/json</code> and <code>contrib/csv</code> no longer run</li> </ul> <p> self-test demos on import (library-clean, module-only).</p> <ul> <li><strong>Package system v0.2</strong> (from the mere-notes dogfood): <code>mere install</code></li> </ul> <p> (manifest + git/subdir deps + lockfile), a <code>[host]</code> entry + <code>mere serve</code> that vendor and run the Node host, and distribution via <code>release.yml</code> + <code>scripts/install.sh</code>.</p> <p>1945 tests.</p> <hr> <h2 id="v0-1-0-2026-07-09-first-tagged-release">v0.1.0 — 2026-07-09 (first tagged release)</h2> <p>First public tagged release of the Mere compiler. What it contains:</p> <ul> <li><strong>The language</strong>: HM inference + let-polymorphism, region / view /</li> </ul> <p> <code>Trivial[R]</code> memory model with refined borrow modes, capability-passing effects, and feature-parity codegen to <strong>C / LLVM IR / Wasm</strong> alongside the tree-walking interpreter. 1936 tests.</p> <ul> <li><strong>Self-host</strong>: lexer / parser / typer / eval / fmt / codegen are written</li> </ul> <p> in Mere and compile themselves through the Wasm pipeline.</p> <ul> <li><strong>Concurrency</strong>: <code>spawn</code> / <code>channel</code> / <code>join</code> + <code>par_map</code> on all four</li> </ul> <p> backends, with a <code>Send</code> / <code>Sync</code> type discipline.</p> <ul> <li><strong>Package system v0.2</strong>: <code>mere install</code> (manifest + git deps with</li> </ul> <p> monorepo <code>subdir</code>, transitive resolution, <code>mere.lock</code>) and a <code>[host]</code> entry + <code>mere serve</code> that vendor and run the Node runtime host — so an app builds and runs from just an installed <code>mere</code>, no source tree.</p> <ul> <li><strong>Distribution</strong>: <code>release.yml</code> builds prebuilt binaries for macOS</li> </ul> <p> (arm64 / x86_64) + Linux (x86_64) on each <code>v*</code> tag; <code>scripts/install.sh</code> installs one without an OCaml toolchain.</p> <p>Work since the entries below (2026-07-07…09): self-host frontier completion (module-import inlining fix; while / brace-block / vec / map builtins), the concurrency stack, and the package-system + distribution tooling above.</p> <hr> <h2 id="2026-07-06-tutorial-implement-type-inference-in-mere-roadmap-step-4-third-of-three-series-complete">2026-07-06 — Tutorial: implement type inference in Mere (roadmap step 4, third of three — series complete)</h2> <p>Third and final tutorial in the initial series (direction paper's educational thread). Builds the unification engine at the heart of Hindley-Milner over a tiny lambda calculus + <code>let</code>.</p> <ul> <li><code>docs/tutorial-type-inference.md</code> — auto-published. Builds</li> </ul> <p> bottom-up: the <code>expr</code> / <code>ty</code> ASTs (with <code>TVar</code> unification variables), fresh-var supply (single-slot vec), the substitution + <code>apply</code>, the occurs check, <code>unify</code> (tuple-match core), and <code>infer</code> (6 cases). Then the honest <strong>HM leap</strong> section: explains why the monomorphic <code>let</code> here rejects <code>let id = fn x -> x in id id</code>, and what let-generalization / instantiation add — pointing to the real <code>contrib/typer</code> (which runs in the browser playground).</p> <ul> <li><code>examples/tutorial_type_infer.mere</code> — the worked example. Verified</li> </ul> <p> end-to-end: <code>fn x -> x : t0 -> t0</code>, <code>fn f -> fn x -> f x : (t6 -> t7) -> t6 -> t7</code> (arrow domain parenthesized), <code>(fn x -> x) 5 : int</code>, <code>let id = fn x -> x in id true : bool</code>, <code>1 2 : TYPE ERROR</code> (int isn't a function), <code>id id : TYPE ERROR</code> (occurs check).</p> <p>The tutorial series now covers all three planned tracks:</p> <ol> <li>REST API (<code>contrib/http</code> — routing / path params / CRUD)</li> <li>Redis client (raw TCP externs — the RESP protocol)</li> <li>Type inference (the HM unification engine — self-host compiler</li> </ol> <p> internals)</p> <p>Together they span the three positioning directions: Wasm-first backend (1), the network/systems layer (2), and the educational PL-implementation angle (3).</p> <h2 id="2026-07-06-tutorial-build-a-redis-client-in-mere-roadmap-step-4-second-of-three">2026-07-06 — Tutorial: build a Redis client in Mere (roadmap step 4, second of three)</h2> <p>Second educational tutorial. Builds a minimal Redis client from the raw TCP + memory externs to teach the RESP wire protocol — the layer <code>contrib/db/redis</code> sits on top of.</p> <ul> <li><code>docs/tutorial-redis-client.md</code> — auto-published (nav + sitemap +</li> </ul> <p> search). Covers RESP in a table (<code>+</code> simple / <code>-</code> error / <code>:</code> int / <code>$</code> bulk / <code>*</code> array), then builds bottom-up: the <code>tcp_*</code> + <code>mem_*</code> externs, the reply variant, byte / line / exact-count readers, the first-byte dispatch parser, and command encoding (<code>*N\r\n$len\r\narg\r\n</code>). Ends pointing at the full <code>contrib/db/redis</code> (RESP3, pipelining, TLS, pub/sub) + queue / stream / lock modules + the pg driver (same <code>mem_*</code> pattern).</p> <ul> <li><code>examples/tutorial_redis_client.mere</code> — the worked example.</li> </ul> <p> Verified end-to-end against <code>redis:7</code>: PING → <code>+PONG</code>, SET → <code>+OK</code>, GET → bulk <code>"hello mere"</code>, GET missing → nil, DEL → <code>:1</code> — one reply type exercised per command.</p> <p>Teaching point emphasized: bulk strings use a length prefix (not line scanning) because payloads can contain <code>\r\n</code> / NUL — so <code>read_bulk</code> reads an exact byte count via <code>read_exact</code>, unlike the CRLF <code>read_line</code> used for status / length lines.</p> <p>Note: <code>tcp_*</code> externs need the Node runner's sync TCP worker; they are NOT available on Cloudflare Workers (no raw sockets) — called out in the tutorial.</p> <h2 id="2026-07-06-tutorial-build-a-rest-api-in-mere-roadmap-step-4-first-of-three">2026-07-06 — Tutorial: build a REST API in Mere (roadmap step 4, first of three)</h2> <p>First educational tutorial (direction paper's step 4). A guided walkthrough that builds a minimal notes REST API on the <code>contrib/http</code> stack — create / list / fetch / delete over JSON, storage in-memory (no DB to set up).</p> <ul> <li><code>docs/tutorial-rest-api.md</code> — the tutorial, auto-published to the</li> </ul> <p> docs site (nav + sitemap + search picked it up automatically). Builds the program up in 5 steps (route → store+create → list → path-param fetch → delete), each snippet grounded in real code, then points to next steps (Postgres persistence, ETag concurrency via <code>http_rest_notes</code>, auth, middleware).</p> <ul> <li><code>examples/tutorial_notes_api.mere</code> — the complete worked example</li> </ul> <p> the tutorial references. Verified end-to-end: create → 201, list → JSON array, fetch → full note, missing → 404, delete → <code>{"deleted":true}</code>, list-after-delete correctly skips the removed note (the list walk gates on <code>map_has</code>, so a deleted id left in the order vector drops out silently).</p> <p>Teaching points surfaced in the tutorial: the <code>\{</code> escape for JSON object literals (bare <code>{</code> starts string interpolation), top-level <code>let rec</code> for recursive helpers (Wasm backend disallows <code>let rec</code> nested in a fn body), and <code>route_pattern</code> <code>:id</code> captures working across GET and DELETE.</p> <p>README gains a pointer under Documentation.</p> <h2 id="2026-07-05-cloudflare-worker-package-registry-v0-1-json-api">2026-07-05 — Cloudflare Worker: package registry v0.1 (JSON API)</h2> <p>Second CF Worker sample from the direction paper. Read-only JSON API over a static-ish bundled package list — the foundation for <code>mere install</code> speaking a normalized endpoint instead of hitting GitHub directly.</p> <p><code>examples/cloudflare-worker-registry/</code>:</p> <ul> <li><code>main.mere</code> — routes + response builders + naive JSON scan/escape</li> <li><code>worker.js</code> — CF entry, exposes bundled <code>packages.json</code> to Mere via</li> </ul> <p> a <code>cf_registry_data ()</code> extern</p> <ul> <li><code>packages.json</code> — v0.1's source of truth (3 sample entries:</li> </ul> <p> mere-http / mere-db / mere-json). To add a package: edit + rebuild</p> <ul> <li><code>wrangler.toml</code>, <code>build.sh</code>, <code>local_test.js</code>, <code>README.md</code></li> </ul> <p>Endpoints:</p> <ul> <li><code>GET /</code> landing HTML</li> <li><code>GET /pkg</code> whole registry</li> <li><code>GET /pkg/:name</code> one package's metadata</li> <li><code>GET /pkg/:name/latest</code> latest version</li> <li><code>GET /pkg/:name/:version</code> specific version</li> </ul> <p>Verified via <code>node local_test.js</code> — <strong>21 assertions across 8 request scenarios</strong>, all pass:</p> <ul> <li>Landing 200 + HTML</li> <li><code>/pkg</code> lists all 3 packages</li> <li>Package metadata has owner / latest / versions</li> <li><code>/pkg/mere-http/latest</code> returns injected <code>{name, version, tarball, ...}</code></li> <li>Specific version endpoint works</li> <li>Unknown package → 404</li> <li>Unknown version → 404</li> <li>POST → 404 (only GET supported)</li> </ul> <p>Two landmines fixed during shipping:</p> <ul> <li><strong>Balanced-brace parser bug</strong>: earlier <code>while</code> loop set <code>i = n</code> to</li> </ul> <p> break out but then the "start >= n → empty" check false-negatived every extraction. Restructured with an explicit <code>done</code> flag.</p> <ul> <li><strong>Unescaped `\n` in 404 body</strong>: <code>resp_not_found</code> splices <code>msg</code> into</li> </ul> <p> response body JSON without escaping. Added a <code>json_esc</code> pass.</p> <p>Wasm size: 11 KB. v0.2 roadmap in the README (GitHub tag fetching, KV cache, publish endpoint, <code>mere install</code> CLI).</p> <h2 id="2026-07-05-cloudflare-worker-playground-snippet-share-kv-backed">2026-07-05 — Cloudflare Worker: playground snippet share (KV-backed)</h2> <p>Turned the CF Worker template from "hello, method+path echoed" into a real sample that motivates Workers over static hosting: a playground-snippet share service backed by Cloudflare KV.</p> <p>Endpoints:</p> <ul> <li><code>GET /</code> landing HTML</li> <li><code>POST /share</code> raw code → 8-hex id + <code>KV.put</code>, returns <code>{id, url}</code></li> <li><code>GET /s/:id</code> returns stored snippet, 404 if unknown</li> </ul> <p>The async KV binding on CF is bridged to sync Mere externs via two conventions:</p> <ul> <li><strong>Pre-fetch</strong> (read path): worker awaits <code>KV.get(id)</code> BEFORE</li> </ul> <p> calling Mere; the value lives in a module-scoped <code>currentKvLookup</code> and Mere reads it via <code>cf_kv_lookup ()</code>.</p> <ul> <li><strong>Outbox</strong> (write path): Mere emits <code>kv_put:{key,value}</code> in the</li> </ul> <p> response JSON; worker honours it AFTER the handler returns via <code>KV.put(key, value)</code>.</p> <p>Body handling uses the same "extern-not-JSON" convention: JS stashes the raw request body in a module scratch, Mere reads it via <code>cf_body ()</code>. This sidesteps a JSON-in-JSON double-escape bug where <code>\n</code> inside stored snippets turned into <code>\\n</code> after round-trip.</p> <p>Local smoke test (<code>local_test.js</code>) verifies six assertions with an in-memory KV mock:</p> <ul> <li>Landing page 200 + text/html</li> <li><code>POST /share</code> returns 201 + JSON id/url, KV was written</li> <li><code>GET /s/:id</code> returns 200 with the ORIGINAL code (newlines</li> </ul> <p> preserved byte-for-byte — regression for the double-escape bug)</p> <ul> <li>Unknown id → 404</li> <li>Empty body → 400</li> <li>Unknown route → 404</li> </ul> <p>Wasm size: 5.7 KB → <strong>8.1 KB</strong> (added routing + JSON escaper + KV outbox construction).</p> <h2 id="2026-07-05-cloudflare-worker-template-roadmap-step-2">2026-07-05 — Cloudflare Worker template (roadmap step 2)</h2> <p>Step 2 of the direction-paper roadmap. A minimal, self-contained template that runs a Mere program as a Cloudflare Worker — 5.7 KB compiled wasm, no npm runtime deps, V8-isolate compatible.</p> <p><code>examples/cloudflare-worker/</code>:</p> <ul> <li><code>main.mere</code> — 30-line handler. Registers a request handler via a</li> </ul> <p> new <code>cf_on_fetch: (str -> str) -> unit</code> extern. Handler receives JSON-encoded request, returns JSON-encoded response.</p> <ul> <li><code>worker.js</code> — CF Worker entry (ES module). Provides <code>cf_on_fetch</code></li> </ul> <p> + the standard prelude stubs, marshals <code>Request</code> ↔ JSON ↔ Mere closure via the existing <code>__lang_bump</code> + <code>__indirect_function_table</code> machinery.</p> <ul> <li><code>wrangler.toml</code> — CF Worker deploy config.</li> <li><code>build.sh</code> — <code>mere -w main.mere → main.wat → main.wasm</code>.</li> <li><code>local_test.js</code> — Node 22-based smoke test using native</li> </ul> <p> <code>Request</code>/<code>Response</code> (no wrangler/miniflare required for verification).</p> <ul> <li><code>README.md</code> — layout, build/deploy commands, request/response</li> </ul> <p> protocol, and an explicit "what doesn't work on CF" section (no TCP / subprocess / fs — those are Node-runtime-specific externs).</p> <p>Verified locally via <code>node local_test.js</code> — three requests round-trip:</p> <ul> <li><code>GET /</code> → <code>hello from Mere on Cloudflare — GET /</code></li> <li><code>GET /hello?name=world</code> → same shape, path echoed</li> <li><code>POST /submit</code> → method + path echoed</li> </ul> <p>Actual <code>wrangler deploy</code> requires a Cloudflare account and is left to the operator (<code>README.md</code> documents the commands).</p> <p>Deliberate non-goals for this template: KV / R2 / D1 bindings, Durable Objects, auto-rebuild watcher. All addable incrementally.</p> <h2 id="2026-07-05-package-system-v0-1-mere-modules-walk-up-resolution">2026-07-05 — package system v0.1: <code>.mere_modules/</code> walk-up resolution</h2> <p>First step of the direction-paper roadmap. Extends the import resolver in <code>lib/parser.ml</code> with Node.js-style <code>node_modules</code> walk- up semantics — a project puts vendored packages under <code>.mere_modules/</code>, and any file in the tree can <code>import "pkg/module.mere"</code> without relative <code>../</code> navigation or <code>-I</code> flags.</p> <p>Resolution order (relative paths only; absolute paths still resolve literally):</p> <ol> <li><code><importer_dir>/<path></code> — historical behaviour</li> <li><code><nearest .mere_modules up>/<path></code> — new (Node-style walk-up)</li> <li><code>-I</code> dirs + <code>MERE_PATH</code> env — historical, order preserved</li> </ol> <p>Deliberate v0.1 non-goals (documented in <code>docs/packages.md</code>):</p> <ul> <li>No <code>mere.toml</code> manifest yet (track deps by git URL / commit)</li> <li>No <code>mere install</code> command (git clone / submodule / tarball drop)</li> <li>No central registry (planned for v0.3+, design in internal notes)</li> <li>No version resolution (walk-up first-match-wins)</li> </ul> <p>Vendoring workflow — three equivalent options, all documented:</p> <p> git clone https://github.com/<owner>/<pkg> .mere_modules/<pkg> # or git submodule add https://github.com/<owner>/<pkg> .mere_modules/<pkg> # or curl -L https://example.com/<pkg>.tar.gz | tar xz -C .mere_modules/</p> <p>New docs page <code>docs/packages.md</code> with layout, semantics, precedence, and a self-contained demo pointer. Demo <code>examples/pkg_demo/</code>:</p> <ul> <li><code>main.mere</code> — 3 lines, <code>import "hello/greet.mere"; print (greet "world")</code></li> <li><code>.mere_modules/hello/greet.mere</code> — one-liner greeter package</li> <li>End-to-end verified: <code>mere -w examples/pkg_demo/main.mere</code> →</li> </ul> <p> <code>hello, world!</code></p> <p>Three new regression tests in <code>test/test_basic.ml</code>:</p> <ul> <li>Single-level walk-up (<code>.mere_modules/</code> alongside entry file)</li> <li>Deep walk-up (entry file in <code>app/handlers/</code>, modules dir above)</li> <li>Cross-package imports find the same <code>.mere_modules/</code> root</li> </ul> <p>All 7 spot-checked existing demos (<code>http_blog</code>, <code>http_admin_dash</code>, <code>http_router_demo</code>, <code>http_ws_chat</code>, <code>db_redis_pubsub</code>, <code>subprocess_demo</code>, <code>gh_stars</code>) recompile unchanged. Test suite: 1846 → 1849.</p> <h2 id="2026-07-05-contrib-db-redis-ratelimit-distributed-fixed-window-limiter">2026-07-05 — <code>contrib/db/redis_ratelimit</code>: distributed fixed-window limiter</h2> <p>Multi-instance version of <code>contrib/http/ratelimit</code> (which is in-process only — two Mere HTTP servers would each keep their own counter, so a caller can rotate through instances to bypass). This version puts the bucket counter in Redis so N instances share one budget per key.</p> <p>Standard <code>INCR</code> + <code>EXPIRE</code> pattern:</p> <ul> <li><code>redis_rate_over_limit fd key window_sec max</code> → bool</li> </ul> <p> Increments the counter for the current window and returns <code>true</code> if <code>count > max</code>. Attaches TTL on the first hit of a bucket via <code>EXPIRE</code>; subsequent hits are single-<code>INCR</code> calls. Fail-open on network error (returns <code>false</code>).</p> <ul> <li><code>redis_rate_count fd key window_sec</code> → int</li> </ul> <p> Peek without incrementing. Useful for <code>X-RateLimit-Remaining</code> headers.</p> <p>Bucket key layout: <code><key>:<epoch/window></code> — all instances at the same wall-clock second share the counter. Not sliding-window (a burst right at the boundary can spike to 2 x max); document for callers who need bursty tolerance.</p> <p>Demo <code>examples/db_redis_ratelimit.mere</code> — 3-per-2-sec policy: attempts 1-3 return <code>ok</code>, 4-5 return <code>BLOCKED</code>, then a <code>sleep_ms 2200</code> triggers a window roll and the next attempt returns <code>ok</code>.</p> <h2 id="2026-07-05-contrib-os-parallel-map-n-shell-commands-in-parallel">2026-07-05 — <code>contrib/os/parallel_map</code>: N shell commands in parallel</h2> <p>Sits on top of <code>contrib/os/subprocess</code>. No new externs. Uses shell backgrounding (<code>&</code>) + <code>wait</code> + tmpfiles to run N children concurrently under the OS scheduler, then reads their stdouts back in <strong>index order</strong> (not completion order).</p> <p>The "cheap dogfood" step between the sync <code>subprocess_run</code> primitive and a native <code>worker_spawn</code> / <code>worker_await</code> pair that a future worker_threads shipping will bring.</p> <p> parallel_map : str list -> str list</p> <p>Verified end-to-end:</p> <ul> <li>4 x <code>sleep 1 && echo <label></code> → <strong>1135 ms wallclock</strong> (~max of</li> </ul> <p> individual times, not sum of 4000), results <code>[A; B; C; D]</code> in submitted order</p> <ul> <li>Mixed timings (0 / 2 / 1 sec) → <strong>2099 ms wallclock</strong>, results</li> </ul> <p> <code>[instant; two-sec; one-sec]</code> — the slowest child at index 1 dictates wallclock; ordering follows input order, not completion</p> <p>Not suitable for streaming (all children must exit before return), very short-lived children (fork overhead dominates), or output containing the fixed sentinel <code>__MERE_PMAP_SEP_9c3d4f7a__</code>. Documented in the module.</p> <h2 id="2026-07-05-contrib-os-subprocess-sync-shell-out-q-012-path-a">2026-07-05 — <code>contrib/os/subprocess</code>: sync shell-out (Q-012 Path A)</h2> <p>First shipping toward the concurrency-primitive design (see design notes in the project's internal notes). Path A of the plan — the "no language change, immediate utility" step before a proper <code>spawn</code> / <code>channel</code> primitive.</p> <p>Three externs backed by Node's <code>child_process.spawnSync</code>:</p> <ul> <li><code>subprocess_run cmd stdin -> str</code> — shell-execute, feed stdin,</li> </ul> <p> return stdout. Timeout 30 s, buffer cap 16 MiB per stream.</p> <ul> <li><code>subprocess_status ()</code> -> int — exit code of the last run</li> </ul> <p> (0 = ok, nonzero = child, -1 = signal / timeout).</p> <ul> <li><code>subprocess_stderr ()</code> -> str — stderr of the last run.</li> </ul> <p>Blocking by design. <code>subprocess_run</code> holds the whole Wasm frame until the child exits — a Mere HTTP server MUST NOT call it inside a request handler.</p> <p>Deliberate scope: no async / parallel-collect primitive. For parallelism today, users can shell-background inside one call:</p> <p> subprocess_run "sh -c '(child1 > /tmp/r1) & (child2 > /tmp/r2) & wait; " ++ "cat /tmp/r1; echo ---; cat /tmp/r2'" ""</p> <p>The two children run concurrently under the OS scheduler; only collection is serial. A proper <code>worker_spawn</code> / <code>worker_await</code> pair is scheduled for Q-012 step 3 (post <code>worker_threads</code> restructure).</p> <p>Demo <code>examples/subprocess_demo.mere</code> verifies all four flows:</p> <ul> <li><code>date -u</code> → status 0, timestamp captured</li> <li>text piped into <code>wc -w</code> → 5</li> <li><code>false</code> → status 1, stderr captured</li> <li>two <code>sleep 1</code> in parallel via shell <code>&</code> → <strong>1055 ms wallclock</strong></li> </ul> <p> (not 2000+ ms — real OS-level parallelism)</p> <p>Wired into both <code>run_wasm.js</code> and <code>run_http_server.js</code> via the same factory pattern as <code>http_fetch_env</code>. 1846 tests pass.</p> <h2 id="2026-07-05-contrib-http-websocket-rfc-6455-hub">2026-07-05 — <code>contrib/http/websocket</code>: RFC 6455 hub</h2> <p>WebSocket support in the standard shape:</p> <ul> <li>Handshake — <code>GET /ws/<channel></code> with <code>Upgrade: websocket</code> and</li> </ul> <p> <code>Sec-WebSocket-Key</code> → 101 Switching Protocols with the standard <code>Sec-WebSocket-Accept: base64(sha1(key + magic))</code> computation.</p> <ul> <li>Text frame codec — encode server → client (unmasked), decode</li> </ul> <p> client → server (masked with per-frame XOR key). Both length forms (7-bit / 16-bit / 64-bit) supported.</p> <ul> <li>Channel pool — <code>/ws/<channel></code> sockets go into a per-channel Set;</li> </ul> <p> <code>ws_broadcast</code> writes to every socket, auto-relay writes to every socket EXCEPT the sender.</p> <ul> <li>Close + ping — client close → echo close + destroy socket. Ping</li> </ul> <p> → reply pong with same payload.</p> <p>Public API (<code>contrib/http/websocket.mere</code>):</p> <ul> <li><code>ws_broadcast channel payload -> unit</code> — server → all clients.</li> <li><code>ws_client_count channel -> int</code> — for a "0 listeners → skip</li> </ul> <p> work" fast-path.</p> <p><strong>Deliberate design choice</strong>: individual client frames are NOT delivered to Mere. The glue auto-relays them to peers on the same channel (hub pattern), covering chat / cursor-share / collaborative- edit demos without needing an in-Wasm callback per frame. Per-frame Mere handlers would require a callback-into-Wasm design and stay deferred.</p> <p>Not supported (documented):</p> <ul> <li>Binary opcodes (0x2) — silently dropped</li> <li>Fragmentation (FIN=0 continuation) — every frame treated as full</li> <li>Payloads > 2^32 bytes (unrealistic for browser peers)</li> </ul> <p>Demo <code>examples/http_ws_chat.mere</code> — auto-relay chat + admin <code>POST /announce</code> → <code>ws_broadcast</code>. Verified with a native <code>WebSocket</code> probe on Node 22:</p> <ul> <li>A sends "hello from A" → B receives it, A does NOT (hub excludes</li> </ul> <p> sender)</p> <ul> <li><code>POST /announce {"msg":"hello everyone"}</code> → <code>{"delivered_to":2}</code>,</li> </ul> <p> both A and B receive <code>[admin] hello everyone</code></p> <p>All 5 spot-checked existing HTTP demos (router / blog / chat / pubsub_chat / admin_dash) recompile and serve as before — the <code>Upgrade</code> hook is a new event handler on the same server, so non-upgrade requests are unaffected.</p> <p>1846-test OCaml suite passes.</p> <h2 id="2026-07-05-examples-http-admin-dash-integration-dogfood">2026-07-05 — <code>examples/http_admin_dash</code>: integration dogfood</h2> <p>One small admin console exercises six of the modules shipped over the last day in a single mere file (~200 lines):</p> <ul> <li><code>contrib/http/router</code> — <code>route_prefix "/admin"</code> + exact routes</li> <li><code>contrib/http/session</code> — cookie sessions (random 16-hex ids)</li> <li><code>contrib/http/csrf</code> — synchronizer-token on the "run job" POST</li> <li><code>contrib/http/basic_auth</code> — Prometheus scrape gate on <code>/metrics</code></li> <li><code>contrib/http/metrics</code> — <code>/metrics</code> + <code>with_metrics</code> middleware</li> <li><code>contrib/http/cache</code> — <code>cache_no_store</code> on admin pages</li> <li><code>contrib/db/redis_lock</code> — "only one instance runs the job" mutex</li> </ul> <p>Feature: press the dashboard's "run job" button. The server acquires a Redis lock, sleeps 500 ms (simulated work), releases. A second instance clicking during the sleep window hits <code>redis_lock_acquire</code> → <code>None</code> and returns 409 <code>"contended"</code>.</p> <p>Verified multi-instance end-to-end (two processes on <code>:8080</code> + <code>:8081</code> sharing one Redis at :15650):</p> <ul> <li>Login flow: admin/adminpw → session cookie → dashboard 200 with</li> </ul> <p> a CSRF token in the form's hidden input.</p> <ul> <li>Concurrent kick: instance A returns 200 <code>"job ran successfully</code></li> </ul> <p> (held lock for 500 ms)"<code>, instance B returns 409 </code>"contended: another instance is running the job"<code>.</code></p> <ul> <li>CSRF check: POST without the token → 403.</li> <li><code>/metrics</code>: without Basic Auth → 401, <code>-u scraper:s3cret</code> → 200</li> </ul> <p> with <code>jobs_run_total 1</code> in the scrape body.</p> <p>The demo also documents the multi-instance run recipe in the header comments so users can reproduce the race locally with two <code>PORT=…</code> invocations against the same Redis.</p> <h2 id="2026-07-05-contrib-db-redis-lock-distributed-mutex-gen-request-id-shared">2026-07-05 — <code>contrib/db/redis_lock</code>: distributed mutex + <code>gen_request_id</code> shared</h2> <p>Standard <code>SET key <token> NX PX <ttl_ms></code> acquire with compare-and- delete release via Lua EVAL. Enough for "at most one worker across N processes should be running this job right now"; not enough for critical-section-with-consequences workloads (RedLock, CP consensus).</p> <ul> <li><code>redis_lock_acquire fd key ttl_ms -> str option</code></li> </ul> <p> Some fencing token on success, None on contention.</p> <ul> <li><code>redis_lock_release fd key token -> bool</code></li> </ul> <p> Compare-and-delete Lua: only deletes if the key's current value matches the caller's token. Prevents "A's TTL expires, B acquires, A's stale Release blows away B's lock" bugs.</p> <p>Also hoisted <code>gen_request_id</code> (16-hex random) from <code>run_http_server.js</code> into <code>scripts/pg_env.js</code> so CLI Mere programs under <code>run_wasm.js</code> can use it too — the lock's fencing tokens were the immediate trigger, but any test harness minting session ids or correlation ids benefits. All 7 existing consumers recompile unchanged.</p> <p>Demo <code>examples/db_redis_lock.mere</code> walks the six-step race:</p> <ul> <li>A acquires (fresh token)</li> <li>B tries → None (contention)</li> <li>A releases → true (CAS matches)</li> <li>C acquires (fresh token)</li> <li>Impostor tries release with wrong token → false, lock intact</li> <li>E tries → None (C still holds), C releases → true</li> </ul> <h2 id="2026-07-05-contrib-http-cache-cache-control-postures-etag-304">2026-07-05 — <code>contrib/http/cache</code>: Cache-Control postures + ETag / 304</h2> <p>Rounds out the middleware family (session / basic_auth / csrf / metrics / cache). Three helpers for the three canonical cache postures plus an ETag + <code>If-None-Match</code> short-circuit:</p> <ul> <li><code>cache_immutable seconds</code></li> </ul> <p> Sets <code>Cache-Control: public, max-age=N, immutable</code>. For asset URLs with a content hash in the path.</p> <ul> <li><code>cache_private seconds</code></li> </ul> <p> Sets <code>Cache-Control: private, max-age=N</code>. For per-session pages that can be briefly re-used.</p> <ul> <li><code>cache_no_store ()</code></li> </ul> <p> Sets <code>Cache-Control: no-store, no-cache, must-revalidate</code> + <code>Pragma: no-cache</code>. For login / secrets / POST redirects.</p> <ul> <li><code>etag body</code> — quoted SHA-256 hex, strong.</li> <li><code>if_none_match tag</code> — reads <code>If-None-Match</code>, <code>str_eq</code> compare.</li> </ul> <p> Doesn't parse <code>*</code> wildcards or comma lists (documented).</p> <p>Demo <code>examples/http_cache_demo.mere</code> verifies all three postures + the 304 round-trip: matching <code>If-None-Match</code> → 304 with empty body, mismatching → 200 with fresh ETag.</p> <h2 id="2026-07-05-contrib-db-redis-stream-consumer-groups-xgroup-xreadgroup-xack-xpending">2026-07-05 — <code>contrib/db/redis_stream</code>: consumer groups (XGROUP / XREADGROUP / XACK / XPENDING)</h2> <p>Extends the stream module with the load-balanced worker pattern — Redis' Kafka-consumer-group equivalent.</p> <p>Added:</p> <ul> <li><code>stream_group_create fd key group start_id</code> — XGROUP CREATE with</li> </ul> <p> MKSTREAM so producer/consumer bootstrap order is irrelevant. <code>"0"</code> = read from beginning, <code>"$"</code> = only new arrivals.</p> <ul> <li><code>stream_group_read fd key group consumer count</code> — XREADGROUP</li> </ul> <p> GROUP … <code>></code> (un-delivered only). Server remembers per-consumer in-flight entries in the PEL.</p> <ul> <li><code>stream_ack fd key group ids</code> — XACK; returns n acked.</li> <li><code>stream_pending_len fd key group</code> — XPENDING summary → total</li> </ul> <p> un-acked count.</p> <p>XCLAIM / XAUTOCLAIM for reassigning stuck entries stays deferred.</p> <p>Demo <code>examples/db_redis_stream_groups.mere</code> walks the full cycle: one group <code>workers</code> with two consumers A + B share 4 XADD'd jobs. XREADGROUP delivers 1-2 to A and 3-4 to B (no overlap — Redis tracks what's been handed out). PEL sits at 4, then 2 after A ACKs its half, then 0 after B ACKs. A follow-up XREADGROUP <code>></code> returns empty since the group is drained.</p> <h2 id="2026-07-05-contrib-db-redis-stream-xadd-xread-xlen">2026-07-05 — <code>contrib/db/redis_stream</code>: XADD / XREAD / XLEN</h2> <p>Third leg of the Redis event story:</p> <p> redis_pubsub broadcast-and-forget, no history redis_queue exactly-one-worker-claims (BRPOP) redis_stream durable append-only log, replayable</p> <p>Streams are Redis' Kafka-lite — entries live in an append-only radix tree with server-generated <code><ms>-<seq></code> ids. Consumers either resume from a chosen id or use consumer groups (deferred here).</p> <p>Public API:</p> <ul> <li><code>stream_add fd key fields -> str option</code></li> </ul> <p> XADD with <code>*</code> id, returns the new entry id.</p> <ul> <li><code>stream_read fd key after_id N -> (id, fields) list</code></li> </ul> <p> XREAD COUNT N STREAMS key after_id. <code>after_id</code> is exclusive; use <code>"0"</code> for a full replay.</p> <ul> <li><code>stream_len fd key -> int</code></li> </ul> <p> XLEN, <code>-1</code> on error.</p> <p>Out of MVP scope: XREADGROUP / XACK / XPENDING consumer groups, MAXLEN caps, blocking reads (XREAD BLOCK N). Documented in the module header.</p> <p>Demo <code>examples/db_redis_stream.mere</code> verifies the full flow: 3 XADDs → XLEN=3 → full replay from <code>0</code> recovers all fields → resume from mid-stream id yields only the tail → past-the-tail returns empty.</p> <h2 id="2026-07-05-contrib-http-csrf-synchronizer-token-csrf-middleware">2026-07-05 — <code>contrib/http/csrf</code>: synchronizer-token CSRF middleware</h2> <p>Sits on top of <code>contrib/http/session</code>: the cookie session id is the store key, the token is a fresh 16-hex random via <code>gen_request_id ()</code> minted on first <code>csrf_token_for</code> per session and re-used for the lifetime of the session.</p> <p>Public API:</p> <ul> <li><code>csrf_new_store ()</code></li> <li><code>csrf_token_for store session_id</code> — idempotent per session</li> <li><code>csrf_validate store session_id tok</code> — bool</li> <li><code>csrf_hidden_input token</code> — <code><input type="hidden" name="_csrf" value="…"></code> snippet</li> </ul> <p>Design choice: kept as primitives rather than a <code>with_csrf</code> middleware because content-type detection (form vs JSON) and body re-parsing are handler-specific concerns; handlers already read the body via <code>form_field</code> / <code>body_field</code>, so passing the value into <code>csrf_validate</code> is a one-liner where the caller already is.</p> <p>Demo <code>examples/http_csrf_demo.mere</code> — a mutable-message form. Verified: missing <code>_csrf</code> → 403, wrong token → 403, correct token → 303 redirect with the message actually persisting.</p> <h2 id="2026-07-05-playground-wordcount-demo-build-tail-call-flag">2026-07-05 — playground: <code>wordcount</code> demo + build tail-call flag</h2> <p>New live-docs demo — a client-side text stats tool: char / word / line counters computed by a Mere function compiled to Wasm, wired into a textarea + three display slots via <code>contrib/dom</code>. Reuses the Phase 48 C2 frontend FFI (closure dispatch through the exported function table); no new externs.</p> <p>Files:</p> <ul> <li><code>contrib/site/playground/wordcount.mere</code> — <code>count_words</code> /</li> </ul> <p> <code>count_lines</code> implemented as manual character scans (folds runs of whitespace into one word boundary; treats <code>\n</code> as line separator so an N-line file reports N).</p> <ul> <li><code>contrib/site/playground/wordcount.html</code> — form + wire wasm,</li> </ul> <p> matches the styling of the counter / echo demos.</p> <ul> <li>Nav entry added to all sibling playground pages + the SSG's</li> </ul> <p> playground index.</p> <p>Build fix: <code>contrib/site/build_full.sh</code> now invokes <code>wat2wasm --enable-tail-call</code>. The wordcount demo emits <code>return_call</code> / <code>return_call_indirect</code> (Wasm tail-call proposal) via its <code>while</code> loop + inner-lifted closures, and the pre-flag site build rejected those opcodes. Enabled by default in Chrome / Safari / Firefox 129+ / Node 22+, so no runtime compatibility loss.</p> <p>Live path: <code>https://merelang.github.io/mere/playground/wordcount.html</code> after the next Pages deploy.</p> <h2 id="2026-07-05-contrib-db-redis-hll-hyperloglog-cardinality-estimators">2026-07-05 — <code>contrib/db/redis_hll</code>: HyperLogLog cardinality estimators</h2> <p>Thin wrappers on Redis's <code>PF*</code> family. Approximate distinct-count with fixed 12 KiB per key regardless of true cardinality (~0.81 % standard error). Complements the exact-set path (<code>SADD</code> / <code>SCARD</code>) for cases where the memory budget matters more than the exact number — unique visitors, distinct URLs, unique IPs per hour.</p> <ul> <li><code>hll_add fd key values</code> — PFADD; returns <code>1</code> if the estimate</li> </ul> <p> moved, <code>0</code> if all values were already there, <code>-1</code> on error.</p> <ul> <li><code>hll_count fd keys</code> — PFCOUNT; approximate cardinality. Single</li> </ul> <p> key = that key's count; multiple keys = the union cardinality (server-side merge into a temp HLL).</p> <ul> <li><code>hll_merge fd dest srcs</code> — PFMERGE; materializes the union of</li> </ul> <p> <code>srcs</code> into <code>dest</code>. Idempotent.</p> <p>Demo <code>examples/db_redis_hll.mere</code> verifies both the union-via- count and union-via-merge paths: 3 users on <code>shard-a</code>, 3 users on <code>shard-b</code> (one overlap), true distinct = 5 across both, and both merge paths report 5.</p> <h2 id="2026-07-05-contrib-log-level-filtering-field-taking-variants-log-level-env">2026-07-05 — <code>contrib/log</code>: level filtering + field-taking variants + <code>LOG_LEVEL</code> env</h2> <p>The base <code>log_debug</code> / <code>log_info</code> / <code>log_warn</code> / <code>log_error</code> functions were already there but always printed. Now:</p> <ul> <li><code>set_log_level "debug" | "info" | "warn" | "error" | "off"</code> sets</li> </ul> <p> the threshold at runtime. Default remains <code>info</code>.</p> <ul> <li><code>log_from_env ()</code> reads <code>LOG_LEVEL</code> from the process env. Unset</li> </ul> <p> or empty leaves the default in place — a demo without any explicit configuration still gets <code>info</code>-and-above.</p> <ul> <li><code>log_debug_f</code> / <code>log_info_f</code> / <code>log_warn_f</code> / <code>log_error_f</code> —</li> </ul> <p> field-taking variants. Same filter applies; structured <code>(str, str) list</code> fields become JSON keys next to <code>msg</code>.</p> <p>The threshold lives in a single-cell <code>vec_new ()</code> allocated once at module-load time (post import-flatten). Note for future contrib authors: module-level mutable state must use <code>;</code> (top-level decl) rather than <code>let ... in</code> — the latter turns the rest of the file into one expression that import discards. Learned the hard way here; documented in the module.</p> <p>Demo <code>examples/log_levels_demo.mere</code> exercises all levels + runtime switching. Verified:</p> <ul> <li>default: info + warn + error + info_f + error_f visible.</li> <li><code>LOG_LEVEL=debug</code>: debug included.</li> <li><code>LOG_LEVEL=warn</code>: only warn + error.</li> <li><code>LOG_LEVEL=off</code>: silent (until runtime <code>set_log_level</code> re-enables).</li> </ul> <p>All 8 existing log consumers (<code>http_users_db</code>, <code>http_jwt_api</code>, <code>http_ci_dashboard</code>, <code>http_feed_reader</code>, <code>http_csv_export</code>, <code>http_wiki</code>, <code>http_file_upload</code>, <code>http_webhook_receiver</code>) recompile unchanged. Test suite: 1846.</p> <h2 id="2026-07-05-contrib-http-basic-auth-rfc-7617-basic-auth-middleware">2026-07-05 — <code>contrib/http/basic_auth</code>: RFC 7617 Basic Auth middleware</h2> <p>Small addition to gate internal endpoints — <code>/metrics</code> scraping, <code>/admin</code> dashboards, cron-triggered endpoints. Two entry points:</p> <ul> <li><code>with_basic_auth realm user pass handler</code> — single credential pair</li> </ul> <p> (compile-time constant).</p> <ul> <li><code>with_basic_auth_pred realm predicate handler</code> — delegate the</li> </ul> <p> credential check to a <code>(user, pass) -> bool</code> predicate. Useful when the accepted set comes from an env var or in-process map.</p> <p>Missing / wrong credentials → 401 with <code>WWW-Authenticate: Basic realm="…", charset="UTF-8"</code>. Handler is NOT called on failure. Simple <code>str_eq</code> compare (not timing-safe) — documented as a gate, not a production auth layer.</p> <p>Added <code>base64_encode</code> / <code>base64_decode</code> externs to <code>scripts/pg_env.js</code> for utf8 <-> standard-alphabet base64 round-trip (the existing <code>_hex</code> variants take a hex detour that's overkill for Basic Auth's plain <code>user:pass</code> payload).</p> <p><code>examples/http_metrics_demo.mere</code> gained a Basic-Auth-gated <code>/metrics</code> route as the first consumer. Verified: no-auth → 401, wrong creds → 401, <code>-u scraper:s3cret</code> → 200 with metrics body. Ungated routes (<code>/</code>, <code>/work</code>) still return 200.</p> <h2 id="2026-07-05-blog-engine-papercuts-lexer-typer-polish">2026-07-05 — Blog-engine papercuts: lexer + typer polish</h2> <p>Two friction points surfaced during the http_blog dogfood get proper first-class fixes now (previously the demo worked around them).</p> <p><strong>String line-continuation.</strong> <code>"foo \<newline> bar"</code> now lexes as <code>"foo bar"</code> — the backslash-newline sequence eats the newline itself plus any leading spaces / tabs on the next line (Python / Rust convention). Long HTML snippets, SQL statements, and log messages can be broken across source lines without smuggling in a <code>\n</code> or indent characters, and without piecing them back with <code>++</code> string concatenation. All existing escapes (<code>\n</code>, <code>\t</code>, <code>\r</code>, <code>\"</code>, <code>\\</code>, <code>\{</code>) still work identically.</p> <p><strong>SCREAMING_SNAKE_CASE hint on `let`.</strong> <code>let DB_URL = "..."</code> used to fail with a bare <code>type error: unknown constructor in pattern: DB_URL</code> because Mere reserves uppercase-first identifiers for constructors. The typer now recognises the shape (starts uppercase, has no lowercase letters, either ≥ 3 chars OR contains <code>_</code>) and adds:</p> <p> help: Mere reserves uppercase-first identifiers for constructors. If you meant a value binding, rename to <code>db_url</code>.</p> <p>The heuristic explicitly excludes single-letter names like <code>let X = …</code> (too plausibly a one-shot constructor placeholder) and still yields to the standard did-you-mean suggestion when one exists (<code>let x = Cnos (…)</code> → <code>did you mean 'Cons'?</code>).</p> <p>Both changes come with regression tests. Full suite: 1838 → 1846.</p> <h2 id="2026-07-05-sse-bridge-from-redis-multi-instance-sse-fanout">2026-07-05 — <code>sse_bridge_from_redis</code>: multi-instance SSE fanout</h2> <p>New extern in <code>contrib/http/sse.mere</code>:</p> <p> sse_bridge_from_redis channel host port -> unit</p> <p>Spins up (or reuses — idempotent per channel) a persistent RESP2 subscriber in the Node runner. Every incoming <code>message</code>-shaped reply on <code>channel</code> is forwarded to the JS-side SSE broadcast for the same channel name. Result: N Mere HTTP instances behind a load balancer, all subscribed to the same Redis channel, deliver posted messages to every SSE client regardless of which instance holds the subscription.</p> <p>Two moving parts:</p> <ul> <li><code>scripts/sse_redis_bridge.js</code> — new. Async RESP2 subscriber</li> </ul> <p> (Node's <code>net.Socket</code>), auto-reconnect on error / close with a 1 s backoff. Parser handles arrays / bulks / simple strings / integers — enough for the SUBSCRIBE reply shape.</p> <ul> <li><code>contrib/http/http.glue.js</code> — factored the inner fanout code out</li> </ul> <p> of the Mere-facing <code>sse_broadcast</code> extern into a JS-callable <code>broadcast(channel, payload)</code> helper. <code>makeHttpGlue()</code> now returns <code>{ glue, attach, broadcast }</code>; the bridge factory receives <code>broadcast</code> and calls it directly (no Mere-heap ptr boundary crossing).</p> <p>Demo <code>examples/http_pubsub_chat.mere</code> verifies end-to-end:</p> <ul> <li>Two instances started on <code>:8080</code> + <code>:8081</code> against a shared</li> </ul> <p> Redis; both subscribe to <code>chat</code>.</p> <ul> <li>POST to <code>:8080</code> returns <code>{"delivered_to":2}</code> (Redis sees two</li> </ul> <p> subscribers) and the message appears on BOTH SSE streams.</p> <ul> <li>POST to <code>:8081</code> — same behaviour in reverse.</li> </ul> <p><code>http_serve</code> and the pubsub subscriber coexist because the subscribe socket lives entirely in JS (Node's event loop), avoiding Mere's single-threaded per-frame constraint.</p> <h2 id="2026-07-05-contrib-http-session-consolidate-cookie-session-pattern">2026-07-05 — <code>contrib/http/session</code>: consolidate cookie-session pattern</h2> <p>Seven demos (http_blog, http_todo_app, http_users_db, http_todo_pg, http_mini_blog, http_feed_reader, http_cookie_session) all hand-rolled the same five-line dance: <code>map_new ()</code>, read <code>session=</code> cookie, look up user, mint id on login, <code>Set-Cookie</code>. Consolidate:</p> <ul> <li><code>session_new_store ()</code> — opaque store handle (a <code>map</code> under the</li> </ul> <p> hood; pre-migration demos still compile against <code>map_has</code> etc.).</p> <ul> <li><code>session_current store</code> — current user id or <code>""</code>.</li> <li><code>session_login store user</code> — mints a random 16-hex id via</li> </ul> <p> <code>gen_request_id ()</code>, sets <code>Set-Cookie: session=…; Path=/; HttpOnly; SameSite=Lax</code>.</p> <ul> <li><code>session_logout store</code> — removes the entry + emits <code>Max-Age=0</code>.</li> <li><code>session_require store login_url</code> — returns <code>str option</code>; <code>None</code></li> </ul> <p> side-effects a 303 to <code>login_url</code>.</p> <p>Behavioural upgrade: sessions now use <code>gen_request_id ()</code> (crypto random) instead of the demos' old <code>"s-" ++ username</code> — non-guessable ids, plus <code>HttpOnly; SameSite=Lax</code> cookie attributes by default.</p> <p><code>examples/http_blog.mere</code> migrated as the first consumer. All six CRUD flows still work end-to-end (login → post → view → edit → delete). The other six demos continue to work unchanged and can migrate incrementally.</p> <h2 id="2026-07-05-contrib-http-metrics-prometheus-style-metrics-middleware">2026-07-05 — <code>contrib/http/metrics</code>: Prometheus-style metrics + middleware</h2> <p>A small registry of counters and gauges plus a text-format exporter and a <code>GET /metrics</code> handler suitable for direct mount in a route table. Ships an auto-counting middleware <code>with_metrics</code> that increments <code>http_requests_total{method, path}</code> and adds request duration into <code>http_request_duration_ms_sum</code> + <code>_count</code> for every request (Prom's "summary" idiom, no percentiles).</p> <p>Public API:</p> <ul> <li><code>metric_declare_counter name help</code> / <code>metric_declare_gauge name help</code></li> </ul> <p> — register + attach HELP/TYPE metadata (rendered once per name).</p> <ul> <li><code>metric_inc name labels</code> — counter += 1.</li> <li><code>metric_add name labels n</code> — counter += n.</li> <li><code>metric_set name labels v</code> — gauge = v.</li> <li><code>metrics_render ()</code> — Prometheus text-format string.</li> <li><code>metrics_handler req</code> — mount as <code>GET /metrics</code>.</li> <li><code>with_metrics handle</code> — middleware wrapper.</li> </ul> <p>Storage is a plain <code>map_new ()</code> keyed by <code>name</code> or <code>name{labels}</code>; values are <code>int</code> (millisecond durations, counts). Float values, configurable histogram buckets, and label-value escaping are out of MVP scope.</p> <p>Also added <code>now_ms</code> extern to <code>run_wasm.js</code> (previously only in <code>run_http_server.js</code>) so contrib modules that pull it work under either runner.</p> <p>Demo <code>examples/http_metrics_demo.mere</code> — four routes (<code>/</code>, <code>/work</code> with a 50 ms sleep, <code>POST /error</code>, <code>/metrics</code>) verify the auto- counters, business counters, and duration accumulation. <code>/work</code>'s <code>http_request_duration_ms_sum</code> sits at ~55 ms after one hit; <code>errors_total</code> increments only on <code>POST /error</code>.</p> <h2 id="2026-07-05-examples-gh-stars-first-cli-demo">2026-07-05 — <code>examples/gh_stars</code>: first CLI demo</h2> <p>First Mere program that runs under <code>run_wasm.js</code> (not <code>run_http_server.js</code>) and makes outbound HTTP calls. Fetches <code>https://api.github.com/repos/<owner>/<repo></code> and prints the star count, using:</p> <ul> <li><code>arg_get 0</code> for <code>owner/repo</code> argv.</li> <li><code>getenv "GITHUB_TOKEN"</code> for optional Bearer auth (60 → 5000</li> </ul> <p> req/hour when set).</p> <ul> <li><code>http_fetch_h</code> for the <code>Accept: application/vnd.github+json</code> +</li> </ul> <p> <code>User-Agent</code> headers.</p> <ul> <li><code>http_fetch_response_header "X-RateLimit-Remaining"</code> for the</li> </ul> <p> rate-limit metadata line.</p> <ul> <li>Naive <code>"stargazers_count":<n></code> scanner (avoids pulling in</li> </ul> <p> <code>contrib/json</code> which has a top-level self-test block that would execute on import).</p> <p>Verified against <code>merelang/mere</code> (0 stars, fresh repo), <code>rust-lang/rust</code> (114325), <code>sindresorhus/awesome</code> (481588), and a 404 path (<code>no-such-owner/no-such-repo-12345</code> → HTTP 404 with the response body printed).</p> <h2 id="2026-07-05-redis-pubsub-run-forever-sleep-ms-extern-tcp-worker-end-event-fix">2026-07-05 — <code>redis_pubsub_run_forever</code> + <code>sleep_ms</code> extern + tcp_worker <code>end</code>-event fix</h2> <p>Three related changes to make a real-world reconnecting subscribe loop possible in pure Mere.</p> <p><strong>`redis_pubsub_run_forever host port sub timeout_ms retry_ms handler`</strong> Opens its own sub fd, sends the <code>SUBSCRIBE</code> / <code>PSUBSCRIBE</code> commands from <code>sub</code>, dispatches messages via <code>handler</code>, and on <code>PSClosed</code> (or <code>redis_connect</code> failure) sleeps <code>retry_ms</code> then starts over. The handler receives <code>PSClosed</code> events too, so it can log / reset metrics / decide to bail (returning <code>false</code> from any invocation ends the loop cleanly). Non-draining <code>redis_pubsub_subscribe</code> variants are used so <code>PSSubscribed</code> events flow through the handler on every reconnect.</p> <p>Subscription state is captured in a new <code>PubsubSub</code> record — <code>{ channels; patterns }</code>.</p> <p><strong>`sleep_ms` extern</strong> — synchronous millisecond sleep via <code>Atomics.wait</code> on a private <code>SharedArrayBuffer</code>. Blocks the whole Wasm frame, so an HTTP server MUST NOT call this inside a request handler. Added to both <code>run_wasm.js</code> and <code>run_http_server.js</code> (both had a no-op <code>sleep</code>).</p> <p><strong>tcp_worker.js `end`-event handler</strong> — with <code>allowHalfOpen: true</code>, a peer FIN emitted <code>end</code> but not <code>close</code>, so a pending <code>tcp_read</code> hung indefinitely. Reproducible via <code>CLIENT KILL TYPE PUBSUB</code> on a subscribed connection. Added an <code>on('end', ...)</code> handler that marks the socket read-closed and wakes any pending read with EOF (<code>respond(0, 0)</code>), matching what the <code>close</code> branch already did.</p> <p>Demo <code>examples/db_redis_pubsub_reconnect.mere</code> stages the failure in one process: subscribe → publish 2 → 2 deliveries → send <code>CLIENT KILL TYPE PUBSUB</code> → sub fd closes → loop sleeps 500 ms → reconnects + resubscribes → publish 2 more → 2 deliveries → exit.</p> <p>Verified end-to-end against redis:7, plus the existing base pubsub + queue demos still work unchanged (regression check). 1838-test OCaml suite passes.</p> <h2 id="2026-07-05-contrib-db-redis-queue-list-backed-work-queue">2026-07-05 — <code>contrib/db/redis_queue</code>: list-backed work queue</h2> <p>Complements <code>redis_pubsub</code>. Pub/sub is broadcast-and-forget; work queues are exactly-one-worker-claims-each-job. Standard Redis reliable-queue pattern wrapped:</p> <ul> <li><code>redis_queue_push fd queue payload</code> — LPUSH, returns new length.</li> <li><code>redis_queue_pop fd queue timeout_s</code> — BRPOP with server-side</li> </ul> <p> block. <code>Some (queue, payload)</code> on delivery, <code>None</code> on timeout. Client-side socket timeout is set to <code>(timeout_s + 5) s</code> as a safety net; <code>timeout_s == 0</code> blocks forever on both sides.</p> <ul> <li><code>redis_queue_pop_many fd queues timeout_s</code> — priority multi-queue</li> </ul> <p> BRPOP. Earlier queues in the list win.</p> <ul> <li><code>redis_queue_len fd queue</code> — LLEN.</li> <li><code>redis_queue_run fd queues timeout_s handler</code> — event-loop helper</li> </ul> <p> that retries on timeout; handler returns <code>false</code> to break out.</p> <p>Explicitly out of scope for the MVP: ack / retry semantics (processing-list + RPOPLPUSH reconciliation), delayed jobs, and priorities beyond the multi-queue trick.</p> <p>Demo <code>examples/db_redis_queue.mere</code> verifies push (returns 1,2,3,4), LLEN=4, FIFO order across three BRPOPs, priority fall- through via <code>pop_many ["jobs.slow"; "jobs"]</code>, and the empty-queue timeout returning <code>None</code>.</p> <h2 id="2026-07-05-http-fetch-shared-across-both-runners">2026-07-05 — <code>http_fetch</code> shared across both runners</h2> <p><code>http_fetch</code> and friends now live in <code>scripts/http_fetch_env.js</code> and plug into both <code>run_http_server.js</code> (as before) and <code>run_wasm.js</code> (new). Any Mere CLI that declares <code>extern fn http_fetch: ...</code> can now make outbound calls under the plain runner — previously they had to boot the HTTP server runner just to get the extern env.</p> <p><code>examples/http_client_auth.mere</code> dropped its unused <code>extern fn http_serve</code> declaration and runs identically under both runners (verified against httpbin.org).</p> <p>Also refreshed <code>docs/http-demos.md</code>: added a "Router API" primer covering <code>route</code> / <code>route_pattern</code> / <code>route_prefix</code>, and catalog entries for the recent <code>blog</code> and <code>client_auth</code> demos.</p> <h2 id="2026-07-05-contrib-http-client-request-response-headers-per-call-timeout">2026-07-05 — <code>contrib/http/client</code>: request + response headers, per-call timeout</h2> <p>The outbound <code>http_fetch</code> was fixed to a bare <code>(method, url, body)</code> shape — no way to attach an <code>Authorization: Bearer …</code> header, no way to read a <code>Retry-After</code> back off a 429, no way to shorten the 10 s default timeout for a cheap probe. Three new externs close that gap without breaking the existing 3-arg call:</p> <ul> <li><code>http_fetch_add_header name value</code> — attaches a header to the</li> </ul> <p> NEXT fetch (host-side accumulator is cleared once the fetch fires, so a set-and-fetch pair is self-contained).</p> <ul> <li><code>http_fetch_response_header name</code> — case-insensitive lookup on</li> </ul> <p> the LAST response. Only the final response block is exposed — redirect chains and 100-continue trailers are discarded.</p> <ul> <li><code>http_fetch_set_timeout ms</code> — one-shot override; 0 restores the</li> </ul> <p> 10 s default.</p> <p>Ergonomic wrappers in <code>contrib/http/client.mere</code>:</p> <ul> <li><code>http_fetch_h method url body headers</code> — headers as <code>(str * str) list</code>.</li> <li><code>http_get_bearer url token</code> — sugar over the common auth-header case.</li> </ul> <p><code>scripts/run_http_server.js</code> runs curl with <code>-i</code> and parses the final response header block (handling redirect / 100-continue prefaces by taking the LAST <code>HTTP/…</code> block) so the host doesn't need a temp file for header capture.</p> <p>Demo <code>examples/http_client_auth.mere</code> verifies all four features end-to-end against httpbin.org: custom header round-trip, response header read, Bearer token, per-call timeout enforcement.</p> <h2 id="2026-07-04-contrib-db-redis-pubsub-dispatch-layer">2026-07-04 — <code>contrib/db/redis_pubsub</code>: dispatch layer</h2> <p><code>redis.mere</code> already carried the raw <code>SUBSCRIBE</code> / <code>PSUBSCRIBE</code> / <code>PUBLISH</code> primitives, but callers had to destructure the resulting RRArr replies by hand to tell a <code>message</code> from a <code>pmessage</code> from a <code>subscribe</code> confirmation. A separate module now does the classification once and returns a small variant:</p> <pre><code> type pubsub_msg = | PSMessage of str * str — (channel, payload) | PSPMessage of str * str * str — (pattern, channel, payload) | PSSubscribed of str * int | PSUnsubscribed of str * int | PSPong of str | PSTimeout | PSClosed | PSOther of redis_reply </code></pre> <ul> <li><code>redis_pubsub_next fd timeout_ms</code> — read + classify one reply.</li> </ul> <p> Uses the caller's <code>timeout_ms</code> to disambiguate the "short read" case: > 0 → <code>PSTimeout</code>, else <code>PSClosed</code>.</p> <ul> <li><code>redis_pubsub_run fd timeout_ms handler</code> — event-loop helper;</li> </ul> <p> handler returns <code>false</code> to break out, loop also exits on <code>PSClosed</code>.</p> <ul> <li><code>redis_pubsub_subscribe</code> / <code>redis_pubsub_psubscribe</code> — non-draining</li> </ul> <p> variants that leave the confirmation reply on the wire, so the dispatch loop sees each as a <code>PSSubscribed</code> event.</p> <ul> <li><code>redis_pubsub_open host port</code> — two-fd <code>PubsubClient</code> record</li> </ul> <p> (publisher + subscriber connections) encapsulating Redis's "PUBLISH needs its own fd" rule.</p> <ul> <li><code>redis_pubsub_show msg</code> — one-line pretty-printer for access logs.</li> </ul> <p><code>examples/db_redis_pubsub.mere</code> rewritten to demonstrate the whole API, including PSUBSCRIBE with a matched-pattern delivery and a <code>PSTimeout</code> tick. Full RESP3 push (<code>RRPush</code>) is also routed through the classifier by recursing into the inner list.</p> <h2 id="2026-07-04-contrib-http-router-route-prefix-mount-points">2026-07-04 — <code>contrib/http/router</code>: <code>route_prefix</code> mount points</h2> <p>Third arm of <code>route_entry</code>: <code>REPrefix of str * route_entry list</code>. Declared via <code>route_prefix "/mount" inner_routes</code>, it nests a whole route table at a common URL prefix. Inner entries are stated relative to the mount point (<code>"/"</code> is the mount root, <code>"/login"</code> is <code>"/mount/login"</code>, etc.), and if no inner entry matches the request falls through to the next outer entry (rather than the prefix "claiming" the URL).</p> <p>Made the fall-through work cleanly by refactoring internal <code>_try</code> to return <code>str option</code> — <code>Some body</code> on match, <code>None</code> on no-match — with the top-level <code>router</code> invoking the fallback only if <code>_try</code> returns <code>None</code>. No behavioural change for pure-exact / pure-pattern route tables.</p> <p>Dogfood in <code>examples/http_blog.mere</code>:</p> <ul> <li>All 9 <code>/admin/*</code> routes now live under <code>route_prefix "/admin"</code> —</li> </ul> <p> the admin subtree is declared as a self-contained table and reused as one entry.</p> <ul> <li>Edit / delete moved to <code>/admin/edit/:id</code> and <code>/admin/delete/:id</code></li> </ul> <p> pattern routes — the hand-rolled query-string parse in <code>edit_form_h</code> (that reached into the raw request line because the router had already stripped the query) is gone. Cleaner URLs and one fewer papercut for the next demo author.</p> <h2 id="2026-07-04-contrib-http-router-capture-path-params">2026-07-04 — <code>contrib/http/router</code>: <code>:capture</code> path params</h2> <p>Extended <code>route_entry</code> from a bare tuple to a two-arm variant so the router can dispatch on patterns without breaking the existing exact-match API.</p> <ul> <li><code>route</code> (backwards-compatible) — exact-path entry, unchanged</li> </ul> <p> signature. Existing 15 demos recompile with zero source changes.</p> <ul> <li><code>route_pattern method path handler</code> — new. Path segments starting</li> </ul> <p> with <code>:</code> capture one URL segment each. Handler is <code>str list -> str -> str</code> (captures in source order, then req).</p> <ul> <li>Segment matching splits on <code>/</code>, ignores leading and trailing</li> </ul> <p> slashes, and requires arity to match exactly (no <code>*</code> glob).</p> <p>Wired into <code>examples/http_blog.mere</code> — the previous <code>not_found</code> + <code>str_starts_with "/post/"</code> workaround is gone; blog now routes <code>/post/:slug</code> declaratively. <code>examples/http_router_demo</code> gained two-capture <code>/user/:name/pet/:pet</code> for reference.</p> <hr> <h2 id="2026-07-02-phase-54-36-runtime-codegen-bootstrap-unblocked">2026-07-02 — Phase 54.36 runtime codegen bootstrap unblocked</h2> <p>Root-caused the "runtime OOB" that had been the last unresolved self-host gap since Phase 54.20 — turned out not to be a codegen bug but plain memory exhaustion.</p> <p><strong>Root cause</strong>: OCaml-side wasm codegen defaulted to <code>(memory (export "memory") 64)</code> — 64 pages = 4 MiB. Self-host <code>parse_and_emit "42"</code> allocates ~30 MiB at peak (prelude tokens + parsed AST + emit strbuf). The bump allocator has no <code>memory.grow</code>, so writes past 4 MiB trap.</p> <p>Phase 54.20's 5/6-char boundary observation was a red herring: the allocation crossed the 4 MiB line at a specific input-dependent point that happened to correlate with name length in the isolation harness. Phase 54.23's higher-order-list_map hypothesis was similarly incidental.</p> <p><strong>Fix</strong>:</p> <ul> <li><code>lib/codegen_wasm.ml</code> — default memory 64 → 1024 pages (64 MiB)</li> <li><code>contrib/codegen/codegen_wasm.mere</code> — same bump for the self-host</li> </ul> <p> codegen's own memory-line emission (16 → 1024)</p> <ul> <li><code>test/test_basic.ml</code> — updated the "wasm: memory declared + exported"</li> </ul> <p> snapshot to expect 1024. <code>run_wasm</code> also now passes <code>node --stack-size=65500</code> because self-host workloads recurse thousands of frames before returning (default Node stack ~500 KB).</p> <p><strong>Verified</strong>: <code>examples/oneshot_codegen.mere</code> (imports the self-host codegen and calls <code>parse_and_emit "42"</code>) now runs end-to-end under Node, emits 80,744 bytes of WAT, exits cleanly. Previously trapped with either "call stack size exceeded" or "memory access out of bounds" depending on which limit hit first.</p> <p><strong>Deferred</strong>:</p> <ul> <li><code>memory.grow</code> in the bump allocator. Bumping the default fixes the</li> </ul> <p> common case but doesn't help workloads > 64 MiB. Growth-on-demand needs instrumentation at every bump-alloc site — invasive rewrite in <code>lib/codegen_wasm.ml</code>.</p> <p><strong>Follow-up (same day)</strong>: <code>codegen_runtime_bootstrap</code> CI helper added in <code>test/test_basic.ml</code>. Compiles <code>examples/oneshot_codegen.mere</code> via the pre-built <code>_build/default/bin/mere.exe</code> (avoiding nested <code>dune exec</code> inside <code>dune runtest</code>), runs the wasm under Node with a puts hook that captures the auto-printed main result, and asserts the expected value (80746 bytes for <code>parse_and_emit "42"</code>). This closes the previously-deferred CI gap — regressions in the runtime self-host path now fail CI immediately.</p> <p>dune runtest: 1778 → <strong>1779 passing</strong>.</p> <hr> <h2 id="2026-07-02-phase-54-35-web-backend-stage-a-contrib-http">2026-07-02 — Phase 54.35 web backend Stage A (contrib/http)</h2> <p>First Node-hosted HTTP server bindings for Mere. Answers the question "can I write a real web backend in Mere today?" — yes.</p> <p><strong>Added</strong>:</p> <ul> <li><code>contrib/http/http.mere</code> — five extern fns: <ul> <li><code>http_serve: int -> (str -> str) -> unit</code> — register handler, start server</li> <li><code>http_current_body: unit -> str</code> — read POST/PUT body</li> <li><code>http_set_status: int -> unit</code> — override response status</li> <li><code>http_set_content_type: str -> unit</code> — override <code>Content-Type</code></li> <li><code>http_set_header: str -> str -> unit</code> — add arbitrary response header</li> </ul> </li> <li><code>contrib/http/http.glue.js</code> — Node glue with per-request slots for</li> </ul> <p> body / status / content-type / headers. Uses the same closure ABI as <code>contrib/dom</code> (Phase 48 C2 MVP): DataView-based <code>{env, fn_idx}</code> dispatch through the exported <code>__indirect_function_table</code>.</p> <ul> <li><code>scripts/run_http_server.js</code> — reference host that merges standard</li> </ul> <p> env imports (<code>puts</code>, libc stubs, math) with the http glue.</p> <ul> <li>Four examples exercising the stack: <ul> <li><code>examples/http_echo_server.mere</code> — minimal echo (~30 LoC)</li> <li><code>examples/http_echo_body.mere</code> — POST body via <code>http_current_body</code></li> <li><code>examples/http_json_api.mere</code> ⭐ — six-endpoint JSON REST API with</li> </ul> </li> </ul> <p> CORS via <code>http_set_header</code>, 404s via <code>http_set_status</code></p> <ul> <ul> <li><code>examples/http_todo_api.mere</code> ⭐ — in-memory TODO CRUD with</li> </ul> </ul> <p> routing, top-level mutable <code>Map[str, str]</code> state, POST / GET / PUT / DELETE + 404s on missing ids</p> <ul> <li>README entries in <code>contrib/README.md</code> and <code>examples/README.md</code></li> <li>Detailed <code>contrib/http/README.md</code> with API table, integration</li> </ul> <p> recipe, and MVP limitations</p> <p><strong>Non-obvious gotcha caught in testing</strong>: <code>http_current_body ()</code> returns a pointer into a per-request scratch buffer that gets overwritten at the start of the next request. Storing that pointer directly in a <code>Map</code> for later reads returns garbage. Fix: copy the bytes into the stable bump arena via <code>strbuf</code> before storing —</p> <pre><code class="language-mere"> let buf = strbuf_new () in let _ = strbuf_push buf (http_current_body ()) in let text = strbuf_to_str buf in map_set store id text </code></pre> <p>Documented in <code>contrib/http/README.md</code>.</p> <p><strong>MVP limitations (documented)</strong>: Node-only host, no streaming / binary payloads, no custom request-header access, single scratch buffer shared across servers.</p> <p><strong>Position</strong>: Stage 2 contrib (incubation), sibling of <code>contrib/dom</code> on the server side. Graduation target is <code>mere-http</code> (separate repo) once the package manager lands. A future lower-level <code>contrib/net</code> (raw sockets over a C runtime) will slot in below this one.</p> <hr> <h2 id="2026-06-30-2026-07-01-phase-54-self-host-bootstrap-loop-closes">2026-06-30 → 2026-07-01 — Phase 54 self-host bootstrap loop closes</h2> <p>Over 32 incremental slices (Phase 54.1 → 54.32) the Mere source of the compiler pipeline was made to compile itself. <strong>1622 → 1771 tests</strong>. 17 contrib libraries are now self-host-compilable and go end-to-end through <code>parse_and_emit_file → wat2wasm → node</code>.</p> <p><strong>Milestones achieved</strong>:</p> <ul> <li><strong>Compile-time self-compile loop closes</strong>: <code>codegen_wasm.mere</code> (~2800</li> </ul> <p> lines) compiles itself through <code>parse_and_emit_file</code> to 1,560,495 bytes of valid WAT; <code>wat2wasm</code> accepts the output. CI-verified.</p> <ul> <li><strong>Runtime self-host of 5 major components</strong>: <code>lexer</code>, <code>parser</code>,</li> </ul> <p> <code>evaluator</code>, <code>type inferencer</code>, and <code>formatter</code> all compile via the self-host pipeline AND run correctly under wasm. Ten bootstrap harness tests exercise real workloads:</p> <ul> <ul> <li><code>tokenize "let x = 1 in x"</code> → 7 tokens</li> <li><code>parse_decls (tokenize "let x = 1; let y = 2; let z = 3;")</code> → 3 decls</li> <li><code>parse_and_eval "let rec fact = fn n -> if n < 1 then 1 else n * fact (n - 1) in fact 5"</code> → 120</li> <li><code>parse_and_infer "let x = 5 in x + 1"</code> → "int"</li> <li><code>format_program (parse "1 + 2 * 3")</code> → "1 + 2 <em> 3\n"</em></li> </ul> <li><strong>17 contribs self-host-compilable</strong>: <code>ast</code> / <code>lexer</code> / <code>parser</code> /</li> </ul> <p> <code>typer</code> / <code>eval</code> / <code>fmt</code> / <code>json</code> / <code>path</code> / <code>option</code> / <code>regex</code> / <code>regex.engine</code> / <code>argparse</code> / <code>test</code> / <code>toml</code> / <code>markdown/to_html</code> / <code>markdown/to_text</code> / <code>markdown/toc</code>. <code>time.mere</code> still needs float codegen. 10 of the 17 have <code>bootstrap_wat_ok</code> CI checks.</p> <p><strong>Key infrastructure added</strong>:</p> <ul> <li><code>parse_and_emit_file path</code> (Phase 54.10): recursive <code>import "..."</code> inline</li> </ul> <p> with cycle detection + column-0 marker scan.</p> <ul> <li><code>selfhost_prelude</code> (Phase 54.9 + 54.11 + 54.27): auto-prepended Mere</li> </ul> <p> source with <code>list_map</code> / <code>list_rev</code> / <code>list_fold</code> / <code>list_len</code> / <code>list_append</code> / <code>list_mapi</code> / <code>list_filter</code> / <code>list_iter</code> / <code>list_any</code> / <code>list_all</code> / <code>str_join</code> / <code>str_split</code> / <code>str_trim</code> / <code>str_replace</code>, plus <code>type __list_t = Nil | Cons of int;</code> / option / result so tags register deterministically.</p> <ul> <li>Constructor-arity rewrite (Phase 54.13): parser post-pass that walks</li> </ul> <p> <code>TopType</code> decls, builds an arity map, and rewrites <code>EApp(EConstr name None, x)</code> → <code>EConstr(name, Some x)</code> when arity is 1 — fixes the <code>Some x</code> bare-app trap the atom-level parser can't disambiguate.</p> <ul> <li>Stdlib builtins in <code>codegen_wasm.mere</code>: <code>ord</code> / <code>chr</code> / <code>is_digit</code> /</li> </ul> <p> <code>is_alpha</code> / <code>is_space</code> / <code>str_len</code> / <code>char_at</code> / <code>str_starts_with</code> / <code>substring</code> / <code>str_index_of</code> / <code>str_repeat</code> / <code>int_of_str</code> / <code>str_unescape</code> / <code>str_eq</code> / <code>strbuf_new</code> / <code>strbuf_push</code> / <code>strbuf_to_str</code> / <code>strbuf_len</code> / <code>map_new</code> / <code>map_set</code> / <code>map_get</code> / <code>map_has</code> / <code>read_file</code> / <code>not</code> / <code>fail</code>; every one gets a WAT helper.</p> <ul> <li>Semantic fixes: <code>$char_at</code> returns a 1-byte str (matching OCaml</li> </ul> <p> <code>V_str</code>), <code>==</code>/<code>!=</code> on any <code>EStr</code> literal lower to <code>$__lang_streq</code>, and <code>str_eq</code> provides explicit content equality for two runtime strings.</p> <ul> <li>Parser extensions: <code>module M { }</code> / <code>extern fn</code> / <code>fn _</code> / <code>fn (a: t)</code> /</li> </ul> <p> cons-tail <code>[h, ...t]</code> / <code>'a</code> tyvar / char literal / <code>'X'</code> / tuple destructure shorthand / <code>Module.Ctor</code> in patterns and expressions / float literal skip (integer part only) / <code>region R { <expr> }</code> permissive.</p> <p><strong>Outstanding</strong>: runtime self-compile of the codegen itself (<code>parse_and_emit</code> running inside the compiled wasm) traps in an isolated 8-line region — a wasm-level bug that shows up specifically with 6+ character identifier names. Documented reproduction; needs interactive wasm memory inspection to close. Time.mere waits on proper float codegen.</p> <hr> <h2 id="2026-06-22-cont-phase-38-g-1-ownedvec-auto-scope-bound-drop">2026-06-22 (cont. — Phase 38.G-1 OwnedVec auto scope-bound Drop)</h2> <p>After Phase 38.C finished, during the public-release prep session we consumed <strong>Level 1</strong> of DEFERRED §1.3. <strong>1515 → 1526 tests</strong>. Implements N1 of the N1/N2/N3 decomposition that was paper-validated in the design doc (<code>39_nll_linear_design.md</code>).</p> <ul> <li><strong>Behavior</strong>: for <code>let v = owned_vec_new () in body</code>, if static analysis</li> </ul> <p> can confirm that <code>body</code> does <strong>not lexically escape</strong> <code>v</code>, we auto-emit <code>free(v->data)</code> at scope end (same shape as Phase 15.13 <code>with</code>).</p> <ul> <li><strong>Static analysis</strong> (new helpers in codegen_c.ml): <ul> <li><code>no_value_leak v body</code>: checks that <code>Var v</code> does not appear in value</li> </ul> </li> </ul> <p> position of Tuple / Constr payload / Record_lit / Record_update / Fun body.</p> <ul> <ul> <li><code>tail_does_not_return_v v body</code>: checks that the tail expression's type</li> </ul> </ul> <p> does not transitively contain OwnedVec.</p> <ul> <ul> <li>Both pass → auto-Drop; either fails → fall back to existing registry +</li> </ul> </ul> <p> main-end sweep (safe-by-default, conservative).</p> <ul> <li><strong>Supported backends</strong>: C + LLVM. Wasm uses bump-arena and has no</li> </ul> <p> per-allocation free, so Phase 38.G-1 is a no-op there (will enable if GC / linear-memory free arrives).</p> <ul> <li><strong>Escape patterns (no auto-Drop)</strong>: tail of body returns <code>v</code> / <code>v</code></li> </ul> <p> stashed in a tuple / closure captures <code>v</code> / tail type contains OwnedVec.</p> <ul> <li><strong>Auto-Drop patterns</strong>: build → query → return scalar / each <code>if</code> arm is</li> </ul> <p> scalar / nested let chains whose tail is scalar / compatible with Phase 38.C partial application.</p> <ul> <li><strong>Levels 2/3 (N2 NLL Light, N3 Full Linear, ~5–15 slices) remain</strong></li> </ul> <p> deferred<strong> — held back until dogfood actually hurts.</strong></p> <ul> <li><strong>Relevant commit</strong>: <code>76f00f8</code></li> </ul> <hr> <h2 id="2026-06-22-cont-phase-38-c-multi-arg-curried-builtin-first-class">2026-06-22 (cont. — Phase 38.C multi-arg curried builtin first-class)</h2> <p>After Phase 37 finished, the public-release sprint <strong>consumed DEFERRED §1.2 A2</strong>. Multi-arg curried builtins now work in value / partial-app position on all 3 backends. <strong>1511 → 1515 tests</strong>.</p> <ul> <li><strong>Design call</strong>: the originally envisioned per-builtin × per-arity closure</li> </ul> <p> adapter template (extension of Phase 35.1 nullary) was <strong>scrapped</strong> — boilerplate would explode as builtin × arity × backend. Instead each codegen got an <strong>AST-local synthesize</strong> helper (<code>synthesize_curried_eta</code> / <code>_llvm</code> / <code>_wasm</code>); the Var handler detects a multi-arg curried builtin in value position and synthesizes a fully eta-expanded <code>fn __arg0 -> fn __arg1 -> ... -> builtin __arg0 ... __argN</code> Fun chain on the spot, then re-feeds it to <code>emit_expr</code>. The existing anonymous-Fun adapter machinery (Phase 5.7-b) builds the closure; the nested inner App hits each builtin's direct-call fast path.</p> <ul> <li><strong>Supported builtins (9)</strong>: <code>owned_vec_push</code> / <code>owned_vec_get</code> /</li> </ul> <p> <code>vec_push</code> / <code>vec_get</code> / <code>vec_set</code> / <code>strbuf_push</code> / <code>map_get</code> / <code>map_has</code> / <code>map_set</code>.</p> <ul> <li><strong>Examples</strong>:</li> </ul> <p> <code></code><code> let push_v = owned_vec_push v in let _ = push_v 1 in let _ = push_v 2 in ...</code></p> <p> let set_in_m = map_set m in // 1-arg partial of a 3-arg let _ = set_in_m "a" 1 in ... <code></code><code></code></p> <ul> <li><strong>Limitation</strong>: fully unapplied (<code>let push = owned_vec_push</code>) becomes</li> </ul> <p> polymorphic after let-poly, so the use site must pin the type with <code>Annot</code> or a concrete argument (same constraint as Phase 35 nullary).</p> <ul> <li><strong>Slice layout</strong>: <code>46b2704</code> Phase 38.C-1 spike (C / owned_vec_push) /</li> </ul> <p> <code>24ff513</code> 38.C-2 (C / remaining 2-arg) / <code>a6fb4bf</code> 38.C-3 (C / 3-arg) / <code>8265992</code> 38.C-4/5 (LLVM + Wasm port).</p> <hr> <h2 id="2026-06-22-cont-phase-37-public-release-prep">2026-06-22 (cont. — Phase 37 public-release prep)</h2> <p>A prep sprint to public-ize mere after Phase 36 syntactic sugar. <strong>LICENSE adopted + CI set up + B/A implementation polish complete</strong>. 1488 → <strong>1498 tests</strong>.</p> <ul> <li><strong>LICENSE (MIT alone)</strong>: <code>LICENSE</code> (MIT) + <code>CONTRIBUTING.md</code>, with a</li> </ul> <p> contributor heads-up that we may go MIT OR Apache-2.0 dual in the future. Matches the mainstream license of OCaml-family languages (Lua / Zig / Julia / Nim / F#). Strategy notes are in <code>internal design notes</code> Section F.</p> <ul> <li><strong>GitHub Actions CI</strong>: ubuntu + macos × OCaml 5.1/5.4 running <code>dune build</code></li> </ul> <p> + <code>dune runtest</code>. CI / License badges added to README.</p> <ul> <li><strong>Phase 37.B exhaustiveness Phase 2</strong>: <code>is_total_pattern</code> recurses into</li> </ul> <p> tuple / record (<code>(a, b)</code> and <code>{ x = a, y = b }</code> count as total), type hints attached to wildcard warnings for int / str / float / tuple / record (<code>"no wildcard arm for int"</code> etc.). 1488 → 1494 tests.</p> <ul> <li><strong>Phase 37.A `while` at top-level (3 backends)</strong>: extended C / LLVM / Wasm</li> </ul> <p> <code>lift_fn_skels</code> so <code>let _ = while cond do body;</code> works directly under <code>main</code>. When <code>Let (P_*, Let_rec (bs, lr_body), rest)</code> is seen, <code>bs</code> is lifted to a top-level fn skel and the value is replaced with <code>lr_body</code>. 1494 → 1498 tests.</p> <ul> <li><strong>Phase 37.C multi-arg curried builtin first-class</strong>: the remainder of</li> </ul> <p> DEFERRED §1.2 A2. Re-estimated implementation size and <strong>deferred to Phase 38.C</strong> (closure-form for 2-arg curried builtins requires outer/inner adapter generation in two stages, with boilerplate piling up across 10+ builtins like vec_push / map_set × 3 backends).</p> <ul> <li><strong>`.gitignore` / `.gitattributes`</strong>: ignore editor / OS / codegen output;</li> </ul> <p> <code>*.mere linguist-language=OCaml</code> as interim highlighting until Linguist registration.</p> <ul> <li><strong>CLI ergonomics polish</strong>: <code>--version</code> / <code>-v</code> flag, explicit error for</li> </ul> <p> unknown flags, help text updated to reflect 4-backend feature parity (dropped legacy "Phase N prep, int subset" wording), added pointer to docs / examples at the end of help.</p> <ul> <li><strong>opam packaging</strong>: <code>(package mere)</code> in <code>dune-project</code> + <code>(public_name</code></li> </ul> <p> mere)<code> in </code>bin/dune<code>. </code>generate_opam_files true<code> auto-generates </code>mere.opam<code>. </code>opam install .<code> works.</code></p> <hr> <h2 id="2026-06-22-cont-phase-36-syntactic-sugar-dogfood-examples">2026-06-22 (cont. — Phase 36 syntactic sugar + dogfood examples)</h2> <p>After Phase 32 (FFI), ran straight through Phase 33 (dogfood example batch + did-you-mean expansion), Phase 34 (float on 3 backends + libm dispatch), Phase 35 (DEFERRED §1.2 A1: nullary factory builtin first-class value), and Phase 36 (13 syntactic sugars + 16 prelude entries + 47 examples + 8 DEFERRED fixes). <strong>1486 → 1488 tests</strong>, examples 61 → 118 (47 new), the syntactic surface reached practical territory for an ML-family language.</p> <ul> <li><strong>Phase 36 sugars (13 kinds)</strong>: range <code>a..b</code> / operator section <code>(+ 1)</code> /</li> </ul> <p> cons <code>1 :: xs</code> / reverse pipe <code>f <| x</code> / apply <code>f @@ x</code> / lambda shorthand <code>\x -> ...</code> / string interpolation <code>"x = {show n}"</code> (lexer re-tokenizes recursively, <code>\{</code> to escape, nested strings rejected) / <code>?</code> (Option early-return) / <code>?!</code> (Result early-return) / list comprehension multi-gen <code>[f x | x <- xs, p x]</code> / <code>if let pat = e then ... else ...</code> / <code>for x in xs do body</code> (→ <code>list_iter</code>) / <code>while cond do body</code> (→ <code>let rec __while_N = fn () -> if cond then body; __while_N () in __while_N ()</code>).</p> <ul> <li><strong>Phase 36 prelude (16 entries)</strong>: <code>range</code> / <code>list_filter</code> / <code>list_take</code> /</li> </ul> <p> <code>list_drop</code> / <code>list_find</code> / <code>list_append</code> / <code>list_concat</code> / <code>list_flat_map</code> / <code>list_zip</code> / <code>list_for_all</code> / <code>list_any</code> / <code>list_member</code> / <code>list_sum</code> / <code>list_product</code> / <code>list_max</code> / <code>list_min</code> (cumulative 34 entries). <code>sum</code> / <code>product</code> / <code>max</code> / <code>min</code> are defined with <code>let rec</code> (looks complex because the test helper <code>codegen_with_decls</code> skips <code>Top_let_rec</code>).</p> <ul> <li><strong>Phase 36 DEFERRED fixes (8)</strong>: §1.13 narrowed value restriction (do</li> </ul> <p> not generalize types containing mutable containers) / §1.14 lifted closure capture goes through <code>load</code> / <code>global.get</code> for globals / §1.15 C codegen O(2^N) slowdown on deep list literals (double <code>emit_expr arg</code> inside Constr → cache once) / §1.16 <code>strbuf_to_str</code> inside a region had dangling pointer on region escape (C/LLVM switched to <code>__lang_default_region</code> alloc) / §1.17 C codegen <code>type result</code> shadow blew up <code>List.combine</code> (remove from <code>polymorphic_variants</code> + dedupe variant_decls last-wins) / §1.18 Phase 30.2 top-level global init order (source-order inline init) / §1.19 nested lambda unbound on top-level fn reference (added <code>closure_wrapper_forward_decls</code> in C/LLVM/Wasm; Wasm populates <code>fn_closure_table_idx</code> before <code>emit_fn_def</code>) / §1.20 C codegen forward decl for user record inside polymorphic variant (include mono variant/record bodies in the unified topo sort).</p> <ul> <li><strong>Phase 36 examples (47)</strong>: basic dogfood (histogram / traffic_light /</li> </ul> <p> event_counter / html_builder / fallible_lookup / config_loader / csv_writer / markdown_to_text / calendar_lite / matrix_2d / borrow_chain / cache_sim / simple_query / caesar_cipher / fraction / roman_numerals / password_strength / brackets_balance / morse_code / luhn_check / tic_tac_toe / palindrome / anagram / base_conv / rps_game / scoreboard / eight_queens / collatz / bin_tree_traversal / knapsack / factory_value) + sugar showcase (range_demo / sections / cons_pipe_demo / sugar_demo / question_demo / sugar_showcase / comprehension / statistics / if_let_demo / for_loop_demo / while_loop_demo) + 4 big ones (csv_summary / game_of_life / sudoku_check / calc 138 lines / maze_solver BFS).</p> <ul> <li><strong>Phase 35</strong>: extended DEFERRED §1.2 A1 (first-class factory builtin</li> </ul> <p> eta-wrap) to all 3 backends. Added eta_adapters to C/LLVM/Wasm so that unapplied builtins like <code>let mk = map_new</code> work correctly as values.</p> <ul> <li><strong>Phase 34</strong>: float MVP rolled out to 3 backends. Phase 34.1 = C,</li> </ul> <p> Phase 34.2 = LLVM (<code>fadd</code> / <code>fsub</code> / <code>fcmp</code> + <code>@llvm.fabs.f64</code> + <code>__lang_str_of_float</code>), Phase 34.3 = Wasm (i32 ptr to heap-alloc f64 slot + host import for formatting), Phase 34.4/34.5 = libm dispatch (sqrt/sin/cos/tan/f_pow/atan2) on 3 backends + <code>math_demo</code> example.</p> <ul> <li><strong>Phase 33</strong>: dogfood example batch + did-you-mean expansion. Phase</li> </ul> <p> 33.0 expanded did-you-mean to multi-candidate top-3 listing (partially closes DEFERRED §5.1). Phases 33.1–33.7 added D3 option_pipeline / H1 prime_sieve / G5 rate_limiter / C4 stack_calc / G6 markdown_toc / G4 bank_account / H3 graph_bfs working with diff = 0 on 4 backends.</p> <hr> <h2 id="2026-06-22-cont-phase-32-c1-ffi">2026-06-22 (cont. — Phase 32 C1 FFI)</h2> <p>Right after Phase 31, ran Outlook §C1 (FFI = calling external C functions) through 5 slices + 1 polish back-to-back. <strong>1480 → 1486 tests</strong>, the <code>extern fn <name>: <ty>;</code> syntax lets libc functions be called directly from all 4 backends. A step that takes Mere from "an experimental language that runs by itself" to "a practical language that can talk to the outside world".</p> <ul> <li><strong>Phase 32.6</strong>: multi-arg curried extern (<code>extern fn setenv: str -> str</code></li> </ul> <p> -> int -> int;<code>) working on 3 backends. The </code>collect_extern<code> helper walks the App chain to gather all args. Added default JS impls for getenv / setenv / system to </code>scripts/run_wasm.js<code>. Added a 3-arg setenv example in </code>examples/ffi_demo.mere<code>; diff = 0 on 4 backends.</code></p> <ul> <li><strong>Phase 32.5</strong>: added 4 + 2 tests for §32.1–32.4 + §32.6 (1484 → 1486),</li> </ul> <p> created <code>examples/ffi_demo.mere</code>.</p> <ul> <li><strong>Phase 32.4</strong>: Wasm codegen emits <code>(import "env" <name> ...)</code> host</li> </ul> <p> import + <code>call $<name></code>; default JS impls for getpid/getppid etc. injected into <code>scripts/run_wasm.js</code>.</p> <ul> <li><strong>Phase 32.3</strong>: LLVM codegen emits <code>declare <ret> @<name>(<args>)</code> + call.</li> <li><strong>Phase 32.2</strong>: C codegen emits <code>extern <ret> <name>(<args>);</code> decl +</li> </ul> <p> direct call. unit arg → <code>()</code>; unit return → <code>(call, 0)</code> for int-ification.</p> <ul> <li><strong>Phase 32.1</strong>: lexer (T_extern) + AST (Top_extern) + parser + typer +</li> </ul> <p> pipeline + repl + bin + 9 mocks via <code>lookup_extern</code> in eval.ml (getpid / getppid / getenv / setenv / system / sleep / srand / rand / unix_time).</p> <ul> <li><strong>Phase 32.0</strong>: FFI design — fixed syntax / typing /</li> </ul> <p> ABI / per-backend strategy. MVP type range is int / bool / str / unit only; float / tuple / record / variant / callback deferred.</p> <h2 id="2026-06-22">2026-06-22</h2> <p>Ran 11 slices of Phase 29-31 across the night. Starting from <strong>16 examples PERFECT on 4 backends</strong>, finished dogfood (toy_sql 1165 LoC) → bug hunt → all fixes → README polish in one day. 1469 → 1480 tests; DEFERRED §1.10 / §1.11 / §1.12 fully resolved; mere reached a state presentable to outsiders.</p> <ul> <li><strong>Phase 31.1</strong>: README updated to reflect Phase 22-31 (1268 → 1480 tests;</li> </ul> <p> 3 → 4 backend feature parity; toy_sql 1165 LoC; signature spread / Result helpers / inner-fn lifting / top-level globalization / Wasm runtime execution / str_compare on 3 backends).</p> <ul> <li><strong>Phase 31.0</strong>: ported <code>str_compare</code> to 3 backends (C / LLVM / Wasm).</li> </ul> <p> Sign-normalized to match interp's OCaml <code>compare s t</code> (-1/0/1) exactly. C uses inline strcmp, LLVM uses strcmp + select, Wasm uses a dedicated runtime helper.</p> <ul> <li><strong>Phase 30.2c</strong> ⭐: Wasm codegen declares non-fn top-level lets as</li> </ul> <p> <code>(global $name (mut i32))</code>, initializes them with <code>global.set $name</code> at main entry. Var emits <code>global.get $name</code>. Works uniformly since all values are i32.</p> <ul> <li><strong>Phase 30.2b</strong>: LLVM codegen declares them as <code>@<name> = internal</code></li> </ul> <p> global <ll_type> zeroinitializer<code>, stores init at main entry, Var reference is </code>load<code>.</code></p> <ul> <li><strong>Phase 30.2a</strong>: C codegen declares non-fn top-level lets as file-scope</li> </ul> <p> <code>static <type> <name>;</code>, initializes at main entry. The heuristic <strong>only globalizes lets whose name shows up in skels' free_vars</strong>, protecting existing tests. <strong>DEFERRED §1.10 fully resolved on all 3 backends</strong>.</p> <ul> <li><strong>Phase 30.1</strong> ⭐: when a captured name in a closure was shadowed by</li> </ul> <p> let, body emission now temporarily removes the shadowed name from <code>current_env_subst</code>. Root cause was not specific to P_tuple — it was <strong>env_subst not respecting shadowing</strong>. Applied to both Let P_var and Let P_tuple. <strong>DEFERRED §1.11 fully resolved</strong>.</p> <ul> <li><strong>Phase 30.0</strong> ⭐: added <code>when not (Hashtbl.mem toplevel_fn_names ...)</code></li> </ul> <p> guard to the hardcoded dispatch of builtins (<code>is_alpha</code> / <code>is_digit</code> / <code>is_space</code>). If a user-defined fn shadows them, builtin dispatch is skipped. Same pattern applied to C / LLVM / Wasm. <strong>DEFERRED §1.12 fully resolved</strong>.</p> <ul> <li><strong>Phase 29.3</strong> ⭐: implemented nested-loop JOIN in toy_sql + qualify_row</li> </ul> <p> + project_join + 7 JOIN tests. <strong>toy_sql total 1165 LoC, diff = 0 PERFECT on 4 backends, 59 tests</strong> (tokenizer 22 + parser 13 + executor 17 + JOIN 7). Final assessment of N1/N2/N3 dogfood: at 1165 LoC the demand never materialized; pain concentrated in codegen plumbing (DEFERRED §1.10–§1.12).</p> <ul> <li><strong>Phase 29.2</strong>: toy_sql executor (Catalog Map[str, table_meta] +</li> </ul> <p> Storage OwnedVec[tagged_row] + WHERE filter + project + 17 tests). Map[K, V=variant] and OwnedVec[variant] codegen worked first try (symmetric to Phase 15.16).</p> <ul> <li><strong>Phase 29.1</strong>: toy_sql SQL parser (AST + continuation flow + 13 tests).</li> </ul> <p> <strong>Dogfood findings</strong>: C codegen tuple destructure rebind bug (DEFERRED §1.11), Wasm memory expanded from 1 page (64KB) to 16 pages (1MB) for string-heavy apps.</p> <ul> <li><strong>Phase 29.0</strong>: toy_sql foundation (Value variant + Token variant +</li> </ul> <p> hand-written tokenizer + 22 self-tests). <strong>Dogfood findings</strong>: C codegen record-field × nested-lambda capture bug (DEFERRED §1.10), C codegen shadowing user-defined fn with builtin (DEFERRED §1.12).</p> <hr> <h2 id="2026-06-21">2026-06-21</h2> <p>After closing one deferred item in Phase 21, ran Phase 22 → 23 → <strong>Phase 24-27 (29 slices straight)</strong> to complete 4-backend feature parity, then added 4 dogfood examples in Phase 28. <strong>1268 → 1469 tests passing</strong>, DEFERRED §1.7 / §1.8 / §1.9 resolved, 16 examples match diff = 0 PERFECT on all 4 backends.</p> <ul> <li><strong>Phase 28.1</strong>: fix deep nested lambda capture bug in C codegen</li> </ul> <p> (DEFERRED §1.9). Added <code>pattern_vars_with_types</code> helper; Match emit_arms wraps arm body / guard in with_pat scope and prepends pattern bindings to current_var_types. Nested closures in arm bodies now pick up pattern-bound names in free_vars filter and write them into closure env. Same shape as LLVM Phase 25.3 (second N+1 → N backport).</p> <ul> <li><strong>Phase 28.0</strong>: 4 new examples verified on 4 backends: <ul> <li>D2 <code>chained_parse.mere</code>: Result chain idiom (result_and_then /</li> </ul> </li> </ul> <p> result_map / result_or_else)</p> <ul> <ul> <li>C1 <code>state_machine.mere</code>: variant + match transitions</li> <li>I1 <code>ini_parser.mere</code>: line parser + Map (Phase 27.1 insertion-order</li> </ul> </ul> <p> dogfood)</p> <ul> <ul> <li>C5 <code>regex_lite.mere</code>: recursive AST + backtracking matcher</li> </ul> </ul> <p> <strong>12 → 16 examples PERFECT-matching on 4 backends</strong>. chained_parse surfaced C codegen <code>undeclared identifier 'rest'</code> (DEFERRED §1.9).</p> <ul> <li><strong>Phase 27.3</strong> ⭐: Wasm ty_tag accepts StrBuf (releases blocker where</li> </ul> <p> Phase 15.9-implemented <code>mere_strbuf_*</code> runtime couldn't be used with StrBuf inside tuple/variant payload). <strong>json_writer matches PERFECT on Wasm runtime → 12/12 PERFECT on Wasm → full 4-backend feature parity achieved</strong>.</p> <ul> <li><strong>Phase 27.2</strong> ⭐: Wasm runtime execution verification. Added</li> </ul> <p> <code>scripts/run_wasm.js</code> (Node.js host harness with puts / read_file / write_file imports). Wasm main tail emits <code>show_<main_ty> + puts</code>; <code>add_show_type main_ty</code> forces show emission for main_ty. <strong>11/11 examples match PERFECT vs interp on Wasm runtime</strong>.</p> <ul> <li><strong>Phase 27.1</strong> ⭐: pinned interp Map iter order to insertion order.</li> </ul> <p> V_map changed to <code>(Hashtbl, value list ref)</code>; map_set appends new keys; map_iter iterates via the list. <strong>All 3 backends now 12/12 PERFECT</strong> (C/LLVM 10 → 12; word_freq + mini_shell Map-order cosmetic diff gone).</p> <ul> <li><strong>Phase 27.0</strong>: C codegen prints <code>"()"</code> for unit main_ty (backport of</li> </ul> <p> LLVM Phase 25.11). template_engine / json_writer / inventory / cap_handler no longer trail <code>()</code> on C; C PERFECT 6 → 10.</p> <ul> <li><strong>Phase 26 (7 slices)</strong>: 11/12 examples EMIT + wat2wasm successful on</li> </ul> <p> Wasm codegen. Ported the cumulative Phase 22-25 features (variant boxed payload / stdlib builtins / try_or / inner let-rec lifting / multi-instantiation specialization / str_split / str_join / read_file / write_file / lift_fn_skels non-Fun walk / various polishing) to Wasm one slice at a time.</p> <ul> <li><strong>Phase 25 (13 slices)</strong>: LLVM codegen runs 12/12 examples (PERFECT 10).</li> </ul> <p> In parallel with Phase 24.x C features, implemented boxed payload / stdlib / try_or / inner let-rec lifting / multi-instantiation specialization / show_str escape / fn dedup / nested P_constr / missing builtins / various polishing on LLVM side.</p> <ul> <li><strong>Phase 24 (5 slices)</strong>: 12/12 examples working on C codegen</li> </ul> <p> (template_engine / json_writer / inventory / cap_handler / word_freq / mini_shell). Variant payload switched to <code>{ tag, payload_ptr }</code> boxed representation, unifying polymorphic variant containers across all 3 backends.</p> <ul> <li><strong>Phase 23 (5 slices)</strong>: json_parser matches interp 100% on C codegen</li> </ul> <p> (Phase 23.2 added result_map / result_and_then / result_or_else to prelude; Phase 23.3 per-instantiation specialization of polymorphic user let-rec; Phase 23.5 show_str escape — <strong>DEFERRED §1.7 fully resolved</strong>).</p> <ul> <li><strong>Phase 22 (5 slices)</strong>: try_or + str ops (str_split / str_join /</li> </ul> <p> str_count / str_index_of) working on all backends.</p> <ul> <li><strong>Phase 21 (1 slice)</strong>: partial resolution of DEFERRED §1.7 (first</li> </ul> <p> stage of polymorphic user let-rec monomorphization on C codegen).</p> <hr> <h2 id="2026-06-20">2026-06-20</h2> <p>Started from Phase 15.16, then sprinted through Phase 16 / 17 / 18 in one day. 1268 → 1304 tests, resolved 6 items: DEFERRED §1.4 / §1.5 / §1.6 / §2.1 / §2.5 / §4.1. Reached a state with <strong>4 backends matching exactly on a non-trivial program (todo_app), full coverage of the 10-pair borrow checker conflict matrix, and proper module scoping (M.Red qualified + open A.B; nested paths)</strong>.</p> <ul> <li><strong>Phase 18.2: `open A.B;` (open on nested module path)</strong> — DEFERRED</li> </ul> <p> §4.1 fully closed. <code>module_bindings</code> registers under both short-name key and full-path key (<code>A.B</code>); parser's <code>T_open</code> refactored to a path parser. Existing <code>open M;</code> follows the same code path (1304 passing).</p> <ul> <li><strong>Phase 18.1: M-prefix scoping for ctors / records inside modules</strong> —</li> </ul> <p> remainder of DEFERRED §4.1. After <code>module M { type T = Red | Blue; }</code>, qualified access <code>M.Red</code>, qualified record literal <code>M.Pt { ... }</code>, and qualified patterns <code>match v with | M.Red -> ...</code> all work. Same-named ctors across two modules can be disambiguated by qualified form. Loose coupling: new AST decls <code>Top_ctor_alias</code> / <code>Top_record_alias</code> + shared alias table (<code>Ast.ctor_aliases</code>) + typer.alias_ctor + eval normalizes to canonical name when constructing V_constr. Bare names still work for backward compat (1301 passing).</p> <ul> <li><strong>Phase 17.2: full 10-pair borrow conflict matrix + intra-tuple</strong></li> </ul> <p> conflict<strong> — resolves DEFERRED §2.5. Of the 4×4=10 conflict pairs, added tests for the 4 untested ones (SW×ER, SW×EW, ER×ER, ER×EW); changed `check_borrows` Tuple branch to sequential threading; added a "Conflict matrix and extension history" section to design doc 08 (1295 passing).</strong></p> <ul> <li><strong>Phase 17.1: track function-return borrow by let-bound name</strong> —</li> </ul> <p> DEFERRED §2.1 fully resolved. For <code>let r = f x in let r2 = &mut R r</code> where <code>f</code> returns <code>&R T</code>, the let-bound name is used as a place and a synthetic borrow is added to active for conflict detection (1287 passing).</p> <ul> <li><strong>Phase 16 polish</strong>: reflected friction points #1/#2/#3/#4 in tutorial</li> </ul> <p> / patterns (<code>{ t | f = v }</code> partial update, same-name rebinding, type annotation idiom for closure parameters). Phase 16 retrospective document created.</p> <ul> <li><strong>Phase 16.4: Wasm Region_block bump restore removed</strong> — DEFERRED</li> </ul> <p> §1.6. Fixed bug where <code>let v = region R { vec_to_owned ... } in ...</code> allocates inside a region and escapes, but the region exit rewinds bump so subsequent allocations overwrite the escaped value. Aligned Wasm region semantics with arena-leak (1283 passing).</p> <ul> <li><strong>Phase 16.3: mk_logger / mk_metrics codegen on 3 backends</strong> —</li> </ul> <p> DEFERRED §1.5. Brought interpreter-only Logger / Metrics cap builtins to C / LLVM / Wasm parity. Logger = <code>{ closure_str_unit info / warn / error }</code>; Metrics = <code>{ inc, record (curried str→int→unit) }</code>. Side change: <code>collect_arrow_types</code> (C/LLVM) recursively traverses known record field types → closure typedefs used only via Logger are also auto-emitted (1281 passing).</p> <ul> <li><strong>Phase 16.2: fix C codegen `let x = f x` same-name rebinding bug</strong> —</li> </ul> <p> DEFERRED §1.4. <code>__auto_type x = ...x...</code> hits the C rule "a variable may not reference itself in its initializer" and triggers a clang error. <code>codegen_c.ml</code> Let uniformly expanded to 2-step form <code>({ __auto_type __let_tmp_<name> = <value>; __auto_type <name> = __let_tmp_<name>; <body>; })</code>; at rhs evaluation the new binding is not yet declared so the old binding is visible (1269 passing).</p> <ul> <li><strong>Phase 16.1: surface 6 friction points via practical example</strong></li> </ul> <p> todo_app.mere<strong> — 110-line TODO app combining OwnedVec[Task] + Logger + vec_map + region. Documented 2 by-design (#1/#2 immutable record update), 2 HM limits (#3/#4 field access inference), 2 real bugs (#5 rebinding, #6 mk_logger codegen), 1 Wasm bug (§1.6) (1268 passing).</strong></p> <ul> <li><strong>Phase 15 #16</strong>: extended Map[R, K, V] K to payload-bearing variants</li> </ul> <p> across 3 backends (Mere's full concrete type set is now usable as a Map key).</p> <hr> <h2 id="2026-06-19">2026-06-19</h2> <ul> <li><strong>Phase 15 #16: extended Map[R, K, V] K to payload-bearing variants on 3</strong></li> </ul> <p> backends<strong> — extends Phase 15.15 nullary-variant K to also accept ctors carrying payloads. Now Mere's full concrete type set works as Map key. </strong>(a) C codegen<strong>: extended the variant branch of `key_eq_for` — `(a.tag == b.tag) && (a.tag == TAG_X ? eq_payload_X : a.tag == TAG_Y ? eq_payload_Y : ... : 0)` nested ternaries for per-tag dispatch; nullary ctors short-circuit to `1` (true). Payload recursively calls `key_eq_for`. C codegen accepts different payload types across ctors (leveraging variant's union representation). </strong>(b) LLVM IR<strong>: extended `emit_map_key_eq_helper_llvm` variant branch — extract tag with `extractvalue`, 0 if tags differ, otherwise extract payload and compare. </strong>LLVM MVP restriction<strong>: ctors must share the same payload type (MVP variant codegen requires a single payload type). Layered OR of "tag-in-nullary-set" checks for nullary ctors, combined with payload eq. </strong>(c) Wasm<strong>: extended `emit_map_key_eq_wasm` variant branch — load tag with `i32.load offset=0`, then a nested if/else chain `if (tag == TAG_X) then eq_payload_X else ...`. Last else is `1` (nullary or covered). Wasm also assumes uniform payload type under MVP, like LLVM. `is_key_supported` accepts payload variants on each of the 3 backends, recursively checking payload types. Added 5 tests (1268 passing) — C accepts mixed payload (A int / B str), LLVM/Wasm accept uniform payload (A int / B int / C nullary) + interpreter parity (1502, 603). </strong>Side test-helper refactor<strong>: changed `vec_codegen_c` / `_llvm` / `_wasm` test helpers to go through `typed_prog` and `Pipeline.process_decls` so Top_type etc. are registered first (programs with type decls used to typer-error in test helpers). Mere's Map key support now covers </strong>all concrete types<strong> (int / bool / str / tuple / record / nullary variant / payload variant). Remaining: first-class value usage; auto-Drop.</strong></p> <ul> <li><strong>Phase 15 #15: extended Map[R, K, V] K to record / nullary variant on 3</strong></li> </ul> <p> backends<strong> — extends Phase 15.14 (tuple) so records and nullary variants also work as K. Enables meaningful maps with compound keys (e.g. `Pt { x, y } → value`, `Color = Red | Green | Blue → value`). Payload-bearing variants out of scope (per-tag union access is complex, candidate for separate slice). </strong>(a) C codegen<strong>: extended `key_eq_for` — records use `(a).field_name` for direct field access and recursive compare; nullary variants compare tags only with `(a).tag == (b).tag`. `is_key_supported` allows record / variant in both spots (Map type registration and `map_kv_tags_of`); judgment via `Typer.records` / `Exhaustive.type_variants`. </strong>(b) LLVM IR<strong>: inside `emit_map_key_eq_helper_llvm` `go` function, records get field via `extractvalue %RecName %r, i`; nullary variants get tag via `extractvalue %VarName %v, 0` + `icmp eq i32`. </strong>(c) Wasm<strong>: in `emit_map_key_eq_wasm` `build`, records get field via `i32.load offset=4*i` (memory-offset based); nullary variants get tag via `i32.load offset=0` + `i32.eq`. Error messages updated to "int / bool / str / tuple / record / nullary variant". Added 8 tests (1263 passing) — 3 backends × (variant key Color: 9, record key Pt: 1000) accept + interpreter parity. Payload-bearing variants still rejected (DEFERRED §1.1 separately).</strong></p> <ul> <li><strong>Phase 15 #14: extended Map[R, K, V] K to bool / tuple on 3</strong></li> </ul> <p> backends<strong> — extends Phase 15.10 (which had int / str only) to also accept bool / tuple (recursively). Enables compound keys (e.g. coordinates `(x, y) → ...`) with tuples. Key equality expands recursively per K structure. </strong>(a) C codegen<strong>: refactored `key_eq_expr` into recursive `key_eq_for k a b` — int/bool via `==`, str via `strcmp`, tuples access each field via `(a).f0, (a).f1, ...` and AND them. Tuples are C value types (struct), so direct field access works. </strong>(b) LLVM IR<strong>: emit one `@mere_map_key_eq_<K>` helper per K (called from `map_set / get / has`). Tuples are decomposed via `extractvalue` and recursively combined with `icmp eq + and i1`. `map_instances` is iterated for unique K and a helper is emitted per unique K in emit_program. </strong>(c) Wasm<strong>: all values are i32 but tuples access fields via memory offset. Added new `emit_map_runtime_wasm k_ty` function that generates 5 helpers per K (new/set/get/has/len) + `$mere_map_key_eq_<K>`. Phase 15.10 hardcoded `map_int_runtime_wasm` / `map_str_runtime_wasm` removed; `map_key_types : (string, Ast.ty) Hashtbl.t` registers K types → emit_program iterates. Tuple key equality in WAT uses block-scoped local.set + i32.load offset=4*i + recursive call_eq. Added 8 tests (1255 passing) — 3 backends × (bool, tuple key) accept + interpreter parity (bool: 302, tuple: 121). Remaining: extending Map K to record / variant (per-K eq logic is generic so extension is easy, but a separate slice is cleaner).</strong></p> <ul> <li><strong>Phase 15 #13: scope-bound OwnedVec Drop via `with v = owned_vec_new</strong></li> </ul> <p> () in body<code>** — complements Phase 15.8 process-wide registry (</code>__mere_owned_vec_free_all<code> at main end) by wiring OwnedVec into the </code>with<code> syntax. When written explicitly as </code>with v = owned_vec_new () in body<code>, after body evaluation v->data is freed and the struct's data field is rewritten to NULL. The registry's </code>free_all<code> (at main end) tolerates </code>free(NULL)<code> (C standard no-op) while finally freeing the struct itself. Fits Mere's **"explicit > concise" philosophy** — the user opts into scope-Drop only when needed, safe without Rust-like move semantics or ownership analysis (creating an alias inside </code>with<code> and using it outside is still UB, but typer's Drop-type rule suppresses some of it). **(a) C codegen**: added branch to </code>Ast.With (name, value, body)<code> emission for </code>value.ty = OwnedVec<code>, inserting </code>free(((__mere_owned_vec_base<em>)name)->data); ((__mere_owned_vec_base</em>)name)->data = NULL;<code> after body. The </code>__mere_owned_vec_base<code> is the existing registry </code>{ void<em> data; int len; int cap; }` struct — generic free leveraging that all `mere_owned_vec_<T>` share the same leading layout. </em><em>(b) LLVM IR</em><em>: emit `getelementptr {ptr, i32, i32}, ptr v, i32 0, i32 0` to access struct field 0 (data ptr), then `load → @free → store null`. LLVM's opaque pointers + shared leading layout means it works without type tags. </em><em>(c) Wasm</em><em>: no malloc/free, just a linear-memory bump allocator, so </em><em>structurally a no-op</em><em> (process exit collects). No code change, but extended `resolve_vec_let_types` pre-pass to also walk With so typer type info flows correctly (shared across 3 backends). Added 3 tests (1247 passing) — C/LLVM scope-end free emission + interpreter parity (30). Remaining: scope-bound Drop is </em><em>only on explicit `with`</em><em>; default `let` still relies on main-end registry sweep. Rust-style auto-Drop requires NLL + move semantics (DEFERRED §1.1).</em></p> <ul> <li><strong>Phase 15 #12: added `vec_to_list` + `len` on list to 3 backends</strong> —</li> </ul> <p> added the remaining recursive-variant (Nil/Cons chain) construction + traversal in codegen. Parallel to Phase 15.7 <code>vec_to_owned</code>, <code>vec_to_list v</code> converts region Vec to <code>T list</code> (builds Cons chain bottom-up — start from Nil and prepend in reverse). <code>len</code> on list added; other types covered in Phase 15.11. <strong>(a) C codegen</strong>: vec_to_list inline-expanded in GCC stmt expression, calling <code>mere_vec_<T>_get(v, i)</code> in reverse and writing each into Cons payload <code>tuple_<T>_list_<T></code> (<code>.f0 = elem, .f1 = acc</code>); new nodes allocated from default region. Cons/Nil tag values resolved at codegen time from <code>variant_tags</code>. Len on list inlined similarly (while loop with <code>__l->tag == cons_tag</code> condition, <code>__l->payload.Cons.f1</code> for next). <strong>(b) LLVM IR</strong>: per-T helpers <code>@mere_vec_to_list_<T></code> and <code>@mere_list_<T>_len</code>, with phi for loop counter (i / acc) and list cursor. Assumes <code>%list_<T>_node = type { i32, %tuple_<T>_list_<T> }</code> exists and accesses payload via <code>getelementptr</code>. <code>vec_to_list_instances : (string, Ast.ty * Ast.ty) Hashtbl.t</code> tracks per-T, deduped in emit_program. <strong>(c) Wasm</strong>: shared <code>$mere_vec_to_list</code> and <code>$mere_list_len</code> helpers (Wasm values are all i32 and list structure is uniform). Tag values pulled from <code>variant_tags</code> at codegen time and baked into runtime; <code>vec_to_list_used</code> / <code>list_len_used</code> flags for lazy emit. Added 7 example tests (1244 passing) — 3 backends × (vec_to_list / len-on-list) + interpreter parity. <code>v2l_src</code> program: <code>type 'a list = Nil | Cons of 'a * 'a list; ... vec_to_list v ...</code> computing <code>len l + head</code>; 13 on 3 backends + interp. Remaining: Map K extension (tuple / record / variant key); first-class value usage (<code>let f = vec_new in ...</code>); OwnedVec scope-bound Drop.</p> <ul> <li><strong>Phase 15 #11: 3 backends got `len` ad-hoc polymorphic builtin</strong></li> </ul> <p> codegen<strong> — `len : 'a -> int` had runtime dispatch in the interpreter; codegen now uses compile-time dispatch (statically routes to the corresponding `_len` helper based on arg.ty). </strong>(a) C codegen<strong>: in the `Ast.Var "len"` App handler, walk `arg.ty` for dispatch — `Vec[_, T]` → `mere_vec_<T>_len`, `OwnedVec[T]` → `mere_owned_vec_<T>_len`, `StrBuf` → `mere_strbuf_len`, `Map[_, K, V]` → `mere_map_<K>_<V>_len`, `str` → `((int)strlen(...))`, `TyTuple ts` → static arity constant (`({ (void)(arg); N; })` evaluates side effects). </strong>(b) LLVM IR<strong>: same pattern; emit `call i32 @mere_vec_<T>_len(ptr %a)` etc. via fresh_reg; str via `@strlen → trunc i64 to i32`; tuple evaluates side effects via emit_expr then returns as constant register via string_of_int. </strong>(c) Wasm<strong>: Vec / OwnedVec share `$mere_vec_len` (same struct layout in Wasm); StrBuf / Map use their helpers; str via `$__lang_strlen`; tuple via emit_expr + `drop` + `i32.const N`. On each backend, `len` is removed from Var rejection — only first-class value usage is rejected. `len` dispatch depends on arg's </strong>static type<strong>; if arg is polymorphic like `Vec[__heap, 'a]` the existing `resolve_vec_let_types` pre-pass concretizes it (collection-type support since Phase 15.2). Added 5 tests (1237 passing) — 3 backends × (Vec / str / tuple) dispatch + interpreter parity (vec[3] + "hello"[5] + (1,2,3,4)[4] = 12). Remaining: `vec_to_list` (recursive variant codegen); Map K extension; first-class value usage.</strong></p> <ul> <li><strong>Phase 15 #10: 3 backends got `Map[R, K, V]` codegen</strong> — brought</li> </ul> <p> the region-aware mutable hashmap to 3 backend parity. Scope: <strong>K = int / str + V = any concrete type</strong>, linear scan (O(n) lookup), on cap-hit allocate new array in region (arena semantics). Brings the 5 interpreter builtins from Phase 12.8 (<code>map_new</code> / <code>map_set</code> / <code>map_get</code> / <code>map_has</code> / <code>map_len</code>) to codegen. <strong>(a) C codegen</strong>: per-(K, V) <code>mere_map_<K>_<V></code> struct <code>{ K* keys; V* values; int len; int cap; __lang_region* region; }</code> + 5 helpers. Key compare via <code>==</code> (int) or <code>strcmp(...) == 0</code> (str); set linear-scans for existing key and overwrites value, else appends to tail (on cap-hit, doubles array, memcpy to new region area). <strong>(b) LLVM IR</strong>: per-(K, V) <code>%mere_map_<K>_<V> = type { ptr, ptr, i32, i32, ptr }</code> + 5 helpers. SSA phi for scan loop; key compare via <code>icmp eq i32</code> (int) or <code>@strcmp</code> (str). Grow path uses <code>getelementptr ... null, i32 1 → ptrtoint</code> for sizeof(K) / sizeof(V), then @memcpy to migrate parallel arrays. get/has return <code>abort</code> / <code>ret i1 0</code> from <code>not_found</code> label. <strong>(c) Wasm</strong>: all values are i32, so <strong>per-K only</strong> (per-V not needed). 2 sets <code>$mere_map_int_*</code> and <code>$mere_map_str_*</code> (5 fns each); key compare via <code>i32.eq</code> or <code>$__lang_streq</code>. <code>map_int_used</code> / <code>map_str_used</code> flags for lazy emit — only one runtime is emitted if only one K is used. On each backend the App handler unwraps curried Apps; <code>map_new</code>'s region pulled from <code>e.ty</code> TyRef marker (same pattern as Vec / StrBuf). Rewrote existing "map: codegen rejection (C)" test to accept; added 3 backends × (str/int) accept + interpreter parity, 8 tests total (1232 passing). Added <code>examples/map_codegen.mere</code> (str→int / int→str / Map inside region combined to return 640; interpreter + 3 backends all 640). Remaining: <code>vec_to_list</code> / <code>len</code> / first-class value usage.</p> <ul> <li><strong>Phase 15 #9: 3 backends got `StrBuf[R]` codegen</strong> — brought the</li> </ul> <p> region-internal mutable string buffer to 3-backend parity. StrBuf is a single non-polymorphic type (no element-type parameter), so per-T monomorphization is not needed; a single runtime helper set (<code>new</code> / <code>push</code> / <code>to_str</code> / <code>len</code>) suffices. <strong>(a) C codegen</strong>: <code>mere_strbuf</code> struct <code>{ char* data; int len; int cap; __lang_region* region; }</code> + 4 helpers; push's realloc within same region (arena semantics); to_str copies null-terminated to region. <code>strbuf_used : bool ref</code> flag for lazy emit (zero overhead in programs that don't use it); added forward typedef. <strong>(b) LLVM IR</strong>: <code>%mere_strbuf = type { ptr, i32, i32, ptr }</code> + 4 helpers; push calls <code>@__lang_region_alloc</code> + <code>@memcpy</code>; push's resize loop is br-back form (double cap until enough capacity); to_str allocates <code>len+1</code> bytes + memcpy + null terminator. <strong>(c) Wasm</strong>: <code>$mere_strbuf_new / push / to_str / len</code> added as an independent runtime block (no closure dispatch, separated from vec_higher_order). <code>$__lang_bump</code> shared; strings copied byte by byte with i8 store/load; resize-time memcpy also hand-written loop. On each backend, App handler unwraps curried form <code>App ({ Var "strbuf_push" }, sb)</code>; <code>strbuf_new</code>'s region pulled from <code>e.ty</code> TyRef marker (same pattern as Vec). Rewrote "strbuf: codegen rejection (C)" to accept; added 3 backends × accept + interpreter parity, 4 tests total (1225 passing). Added <code>examples/strbuf_codegen.mere</code> (interpreter + 3 backends return 48: len of <code>"hello, world!"</code> + len of string built in another region + sb1 len). Remaining: <code>Map[R, K, V]</code> / <code>vec_to_list</code> / <code>len</code> / first-class value usage.</p> <ul> <li><strong>Phase 15 #8: main-end batch free for OwnedVec (naive Drop)</strong> —</li> </ul> <p> replaces the "leave it to process exit" approach of Phase 15.7 with explicit "batch free at end of main" for heap-allocated OwnedVec. Clean under valgrind / leak sanitizer. <strong>Design</strong>: all <code>mere_owned_vec_<T></code> structs share the leading layout <code>{ T* data; int len; int cap; }</code>, so generic free works by casting the first field as <code>void* data</code> (<code>free(v->data); free(v);</code>). A process-wide registry (<code>void** items; int count; int cap;</code>) is a file-scope global; each <code>_new</code> helper registers the struct ptr, then <code>main</code> end's <code>__mere_owned_vec_free_all</code> iterates and frees all. <strong>(a) C codegen</strong>: added <code>owned_vec_registry_runtime</code> block (<code>__mere_owned_vec_register</code> / <code>__mere_owned_vec_free_all</code> + 3 file-scope globals); <code>emit_owned_vec_runtime_for</code> calls <code>__mere_owned_vec_register(v)</code> at end of <code>_new</code>; <code>main</code> end calls <code>__mere_owned_vec_free_all()</code> (only when ≥1 OwnedVec is present). <strong>(b) LLVM IR</strong>: emit <code>owned_vec_registry_runtime_llvm</code> equivalently; registry expressed via global ptr / i32; <code>@realloc</code> to grow; free_all iterates via phi loop. Each <code>@mere_owned_vec_<T>_new</code> end calls <code>@__mere_owned_vec_register</code>; <code>@main</code> end calls <code>@__mere_owned_vec_free_all</code>. <strong>(c) Wasm</strong>: no malloc, allocation via <code>$__lang_bump</code> (linear memory); process exit hands the entire WebAssembly instance back to OS, so <strong>explicit free is unnecessary / impossible</strong> — registry / free_all not emitted (preserves current behavior). <strong>Remaining limit</strong>: process-wide, not scope-bound, so memory grows monotonically for long-running programs that create many OwnedVecs. Real scope-Drop with NLL / move semantics is future work. Added 4 tests (1222 passing) — C / LLVM assertContains for registry + free_all calls; Wasm negative test confirms no registry emitted.</p> <ul> <li><strong>Phase 15 #7: 3 backends got `OwnedVec[T]` + `vec_to_owned` /</strong></li> </ul> <p> <code>owned_vec_to_vec</code><strong> — brought interpreter-only heap-allocated OwnedVec to 3-backend parity, including round-trip (deep copy) with region Vec. Drop processing omitted in this minimum scope (process exit collects). </strong>(a) C codegen<strong>: generates per-T `mere_owned_vec_<tag>` struct + 4 helpers (new/push/get/len) via `emit_owned_vec_runtime_for`; allocates with `malloc / realloc`. vec_to_owned / owned_vec_to_vec inlined in GCC stmt expression; the latter extracts the target region from e.ty TyRef marker (active region). `c_type_of` walks `OwnedVec[T]` → `mere_owned_vec_<tag>*` in parallel with Vec; forward typedefs added. </strong>(b) LLVM IR<strong>: per-T `%mere_owned_vec_<tag> = type { ptr, i32, i32 }` + 4 helpers; `getelementptr ... null, i32 1 → ptrtoint` for sizeof(T); push's realloc uses declared `@realloc(ptr, i64)`. Conversion helpers per-T `@mere_vec_to_owned_<tag>` / `@mere_owned_vec_to_vec_<tag>` implemented with SSA phi loops. </strong>(c) Wasm<strong>: values are all i32 and `$__lang_bump` is shared, so </strong>OwnedVec runtime is physically the same as Vec<strong> — owned_vec_new / push / get / len thin-alias-routed to `$mere_vec_*`; conversions use newly added `$mere_vec_clone` helper for deep copy (allocate new vec, loop element-push). Wasm owned_vec only retains drop_types' region-placement rejection; runtime representation distinction not needed. Extended `resolve_vec_let_types` pre-pass to also handle `Ast.TyCon ("OwnedVec", _)` on C / LLVM. Added `examples/owned_vec_codegen.mere` — vec → owned → vec round trip + fold returning 67 (interpreter + 3 backends all 67). Added 12 tests (1218 passing) — 3 backends × (owned_vec / vec_to_owned / owned_vec_to_vec) codegen-symbol emit + 3 interpreter parity. Remaining: real Drop (per-instance free); `vec_to_list` (recursive variant construction); `StrBuf` / `Map` / `len` / first-class value usage.</strong></p> <ul> <li><strong>Phase 15 #6: 3 backends got `vec_map` / `vec_filter` — all 5 main</strong></li> </ul> <p> Vec higher-order APIs are present<strong> — follows Phase 15.5 (vec_set / iter / fold) with the two region-preserving ones. Both APIs build a new Vec in the same region as the input (vec_map converts element type T → U; vec_filter keeps only elements where predicate is true). </strong>(a) C codegen<strong>: GCC/Clang stmt expression inlining; pull the original Vec's region from `__vc->region` to create new Vec via `mere_vec_<U>_new(__vc->region)`; expand closure dispatch in-line into a loop. vec_filter uses `__auto_type __x = mere_vec_<T>_get(...)` (compiler infers C type) and conditionally pushes via `mere_vec_<T>_push` based on predicate's if branch. </strong>(b) LLVM IR<strong>: vec_map per-(T, U) helper (`@mere_vec_<T>_map_<U>`); vec_filter per-T helper (`@mere_vec_<T>_filter`). Both pull the input Vec's region field (offset 12 = idx 3) via `getelementptr + load` and call corresponding `@mere_vec_<U>_new` / `@mere_vec_<T>_new` to make new Vec. phi manages loop counter; vec_filter conditional-pushes via `br i1` on predicate's i1. `vec_map_instances` / `vec_filter_instances` tables dedupe. </strong>(c) Wasm<strong>: all values are i32, so `$mere_vec_map` / `$mere_vec_filter` added to `vec_higher_order_runtime`. Both call `$mere_vec_new` (no region parameter in Wasm); apply closure to elements via `call_indirect`; push to new Vec via `call $mere_vec_push`. Added `examples/vec_map_filter_codegen.mere` (interpreter + 3 backends return 226). Added 9 tests (1206 passing) — 3 backends × (vec_map / vec_filter) codegen-symbol emit + LLVM's per-(T, U) per-T branch confirmation + interpreter parity. </strong>Now all 5 main Vec higher-order APIs (set / iter / fold / map / filter) work on 3 backends<strong>, with almost no gap to the interpreter. Remaining: `vec_to_list` / `vec_to_owned` / `OwnedVec` / `StrBuf` / `Map` / first-class value usage.</strong></p> <ul> <li><strong>Phase 15 #5: 3 backends got Vec higher-order APIs (`vec_set` /</strong></li> </ul> <p> <code>vec_iter</code> / <code>vec_fold</code>)<strong> — Vec[R, T] working on 3 backends since Phase 15.2 / 15.3 / 15.4; this slice brings interpreter-only main higher-order APIs to parity. </strong>(a) C codegen<strong>: vec_set is a per-T runtime helper (`mere_vec_<T>_set`); vec_iter / vec_fold are inlined at call site (GCC/Clang stmt expression `({ ... })` writes local + for loop + closure dispatch directly). </strong>Side bug fix: anonymous Fun in main_body wasn't draining closure adapter<strong> — added `drain ()` after `let main_body = emit_expr body_expr in` in emit_program to re-collect `pending_closures`. </strong>(b) LLVM IR<strong>: vec_set is per-T helper; vec_iter is per-T helper (`@mere_vec_<T>_iter`); vec_fold is per-(T, U) helper (`@mere_vec_<T>_fold_<U>`). Hand-written SSA with basic blocks managing loop state (i, acc) via phi. </strong>(c) Wasm<strong>: all values are i32, so all 3 helpers shared single runtime (`$mere_vec_set / $mere_vec_iter / $mere_vec_fold`). `vec_iter / vec_fold` helpers reference `(type $cl)` + `call_indirect`, so even programs whose closure values aren't in the table need `(table 0 funcref)` empty-declared; isolated via `vec_higher_order_used : bool ref` flag + separate runtime block. On each backend, App handler unwraps curried Apps (vec_set / vec_fold are 3-arg = 2-stage unwrap; vec_iter is 2-arg = 1-stage). Added `examples/vec_higher_order_codegen.mere` (interpreter + 3 backends return 1234 demo). Added 12 tests (1197 passing) — 3 backends × (vec_set / vec_iter / vec_fold) codegen + interpreter parity. Remaining: `vec_map` (region-preserving new Vec creation) / `vec_filter` (dynamic size calc) / `vec_to_list` / `vec_to_owned` / `OwnedVec` / `StrBuf` / `Map` / first-class value usage.</strong></p> <ul> <li><strong>Phase 15 #4: Wasm codegen supports `Vec[R, T]` — full 3-backend</strong></li> </ul> <p> feature parity<strong> — followed Phase 15.2 (C) / 15.3 (LLVM) and ported Vec to Wasm. In Wasm Mere values are all 4-byte i32 (scalar direct for primitives; structured types are linear-memory offsets), so per-T monomorphization (as in C / LLVM) is not needed. Design call: single `$mere_vec_new / $mere_vec_push / $mere_vec_get / $mere_vec_len` runtime handles all element types. lib/codegen_wasm.ml: (1) added `vec_used : bool ref`, emit_expr sets true when going through vec_*; (2) 4 fns + struct layout `{data:i32, len:i32, cap:i32, _pad:i32}` (16 bytes) written into `vec_runtime` literal in WAT; push's realloc allocates from single `__lang_bump` = arena semantics; (3) `ty_tag` catch-all relaxed to allow TyRef _ R TyUnit (region marker); explicit Vec rejection removed; (4) Var handler's vec_* rejection retained only for first-class value usage; (5) 4 special-cases added to emit_expr — `App (App (Var "vec_push", v), x)` unwrapped to runtime call; `vec_new`'s region argument ignored (Wasm bump is global); (6) introduced `resolve_vec_let_types` pre-pass same as Phase 15.2 / 15.3 (concretizing binding type doesn't directly affect Wasm code but maintained for consistency). Added `examples/vec_codegen_wasm_typed.mere` (int / str / tuple / variant 4 types = 252). Added 4 tests + rewrote existing Wasm rejection test (1185 passing). Now `Vec[R, T]` works on all 3 backends (C / LLVM IR / Wasm) — the constraint "Vec / OwnedVec / StrBuf / Map are interpreter-only" is fully gone for Vec[R, T]. Remaining: higher-order APIs / first-class value usage / OwnedVec / StrBuf / Map codegen remain interpreter-only (see DEFERRED §1.1).</strong></p> <ul> <li><strong>Phase 15 #3: LLVM IR codegen supports `Vec[R, T]` (C feature</strong></li> </ul> <p> parity)<strong> — ported the same monomorphization pattern as Phase 15.2 (C version) to LLVM IR. lib/codegen_llvm.ml: (1) added `vec_instances : (string, Ast.ty) Hashtbl.t`; (2) `emit_vec_runtime_for_llvm` emits one set per element type of `%mere_vec_<tag> = type { ptr, i32, i32, ptr }` + 4 helpers (`_new` / `_push` / `_get` / `_len`) (using LLVM's `getelementptr ... null, i32 1 → ptrtoint` idiom for sizeof(T), allocates via region; push's realloc within same region = arena semantics); (3) `llvm_ty_of` walks `TyCon ("Vec", args)`, returns Vec value as LLVM opaque ptr (`ptr`) and registers element type in `vec_instances`; (4) `ty_tag` catch-all relaxed to allow `TyRef _ R TyUnit` (region marker); (5) Var handler's vec_* rejection retained only for first-class value usage; (6) 4 special-cases (`vec_new` / `vec_push` / `vec_get` / `vec_len`) in emit_expr — `vec_elem_tag_of` reads element type; unwrap curried App (`App(App(Var "vec_push", v), x)`) and call `@mere_vec_<tag>_*`; `vec_new` pulls active region from `current_regions` and passes `@__lang_default_region` or `%__region_R`; (7) introduced `resolve_vec_let_types` pre-pass same as Phase 15.2 — connect let-poly generalized binding and use tyvars with `Typer.unify`; once any use site resolves, chain-propagates to all sites. Added `examples/vec_codegen_llvm_typed.mere` (mixes int / str / tuple / variant 4 types in one program; total 252). Added 5 tests (1182 passing) — confirms emit of mere_vec_T_new runtime for 4 patterns Vec[R, int] / str / tuple / region R inside. Remaining: Wasm backend Vec[R, T] (Phase 15.4 candidate) / higher-order APIs / first-class value / OwnedVec / StrBuf / Map.</strong></p> <ul> <li><strong>Phase 15 #2: C codegen generalizes element type T of `Vec[R, T]`</strong></li> </ul> <p> — extends Phase 15.1 (<code>Vec[R, int]</code> only) to support any concrete element type supported by codegen: int / bool / str / tuple / record / variant. Monomorphize emits <code>mere_vec_<tag></code> runtime struct + 4 helpers (<code>_new</code> / <code>_push</code> / <code>_get</code> / <code>_len</code>) per element type (e.g. <code>mere_vec_int</code> / <code>mere_vec_str</code> / <code>mere_vec_tuple_int_int</code> / <code>mere_vec_Tag</code>). lib/codegen_c.ml: (1) added <code>vec_instances</code> table; c_type_of / emit_expr register T encountered in Vec[_, T] sanitized via <code>ty_tag</code>; (2) <code>emit_vec_runtime_for : Ast.ty -> string</code> generates C runtime block per element type; (3) emit_expr's 4 special-cases (<code>vec_new</code> / <code>vec_push</code> / <code>vec_get</code> / <code>vec_len</code>) routed to <code>mere_vec_<tag>_*</code> helper names via <code>vec_elem_tag_of</code> helper; (4) <code>let v = vec_new () in body</code> generalized binding (Mere has no value restriction; generalized to <code>forall T. Vec[..., T]</code>) leaves App's own .ty TyVar unresolved; added <code>resolve_vec_let_types</code> pre-pass — for each <code>Let(P_var name, value, body)</code> where value.ty is Vec, connect all <code>Var name</code> in body to binding side via <code>Typer.unify</code>; once any use site (e.g. <code>vec_push v 10</code>) resolves, chain-propagates to all sites; (5) element type's C struct may be forward-referenced by later closure typedef etc.; insert <code>typedef struct mere_vec_<tag> mere_vec_<tag>;</code> forward typedef after tuple/record/variant bodies. Added <code>examples/vec_codegen_c_typed.mere</code> (mixes int / str / tuple / variant in one program; total 252). Added 2 tests + rewrote existing "Vec[R, <non-int>] reject" test to "str / tuple accept" (1178 passing). Remaining Vec codegen listed in §1.1 (higher-order APIs / first-class value / LLVM/Wasm / OwnedVec / StrBuf / Map).</p> <ul> <li><strong>Phase 15 #1: C codegen for `Vec[R, int]` (DEFERRED §1.1 partial</strong></li> </ul> <p> resolution)<strong> — first step toward native-izing interpreter-only Vec in the smallest scope (element type int / C backend only). Added `mere_vec_int` struct + `mere_vec_int_new / push / get / len` helpers to `lib/codegen_c.ml` runtime (region-allocated; push's realloc allocates new buffer in same region; old buffer reclaimed at region free = arena semantics). Fixed `c_type_of` to walk `Ast.walk` TyCon args, then map `TyCon ("Vec", [_; TyInt])` to `"mere_vec_int*"`. Added 4 special-cases to `emit_expr` `App` handler (`vec_new` / `vec_push v x` / `vec_get v i` / `vec_len v`) — vec_new reads active region binding via `Ast.walk e.ty` (outside → `__heap` = `__lang_default_region`; inside region R → `__region_R`) and expands to `mere_vec_int_new(&...)`. Remaining 3 unwrap curried form (`App (App (Var "vec_push", v), x)`) via inner/outer combo to runtime helper calls. Relaxed `ty_tag` catch-all rejection to pass only `TyRef` (region marker). Var handler's vec_* rejection kept only for first-class value usage (`let f = vec_new in ...`); direct application changed to pass. Added `examples/vec_codegen_c.mere`: returns 95 computing `vec_new () + push×5 + get / len` in outside-region (verified working via `clang` native binary). Added 6 tests (1177 passing): C codegen accepts Vec[R, int]; runtime helpers emitted; binds to `__lang_default_region` outside / to `__region_R` inside; non-int like Vec[R, str] still rejected; LLVM / Wasm continue rejecting all Vec. Remaining Vec codegen listed in §1.1 (higher-order APIs / first-class value / LLVM·Wasm support / OwnedVec / StrBuf / Map / element types other than int).</strong></p> <ul> <li><strong>Phase 14 #2: rename codebase from working name lang-ml → Mere</strong> —</li> </ul> <p> followed Phase 14.1 name fixation (internal design notes) and changed code body / extensions / docs to Mere across the board. dune library <code>lang_ml</code> → <code>mere</code> (lib/dune); executable <code>main</code> → <code>mere</code> (bin/dune); <code>bin/main.ml</code> → <code>bin/mere.ml</code> (git mv); <code>Lang_ml.*</code> → <code>Mere.*</code> (bin/mere.ml / lib/codegen_llvm.ml / lib/repl.ml / test/test_basic.ml); examples/<em>.lang → </em>.mere (37 files, git mv); updated internal <code>.lang</code> references to <code>.mere</code> (comments in examples / <code>import "..."</code> paths / docs / repl_session.md); CLI usage <code>lang-ml</code> → <code>mere</code>; REPL startup message updated. Updated all Lang / lang-ml / <code>.lang</code> notation in docs / README / CLAUDE.md. Lang in sentences ("Lang program", "of Lang", etc.) also changed to Mere. Intentionally left design context directory <code>internal design notes</code> as-is (historical record). All 1171 tests pass. DEFERRED §7.1 (rename work) moved to fully resolved. Remaining GitHub repo rename (<code>lang-ml</code> → <code>mere</code>) is a user manual operation.</p> <ul> <li><strong>Phase 12 #10: reverse `owned_vec_to_vec` (DEFERRED §3.6 fully</strong></li> </ul> <p> resolved)<strong> — follows Phase 12.11 one-way (`vec_to_owned`) with reverse `owned_vec_to_vec : OwnedVec[T] -> Vec[R, T]`. Region R injected from `active_regions` at call site as App-handler special-case same as `vec_new` / `strbuf_new` / `map_new` (outside → `__heap` default). Eval is `Array.copy` for deep copy (V_vec shared, copy alone yields independence). 3-backend codegen interpreter-only stub. Verified: outside → `Vec[__heap, T]`; `region R { owned_vec_to_vec o }` → `Vec[R, T]` (escape check works); deep copy means subsequent owned-side push doesn't affect vec. Added 5 tests (1171 passing). DEFERRED §3.6 fully resolved.</strong></p> <ul> <li><strong>Phase 13 #1: type error UX continued — did-you-mean for record</strong></li> </ul> <p> field / view field / qualified name<strong> — partially consumes DEFERRED §5.1. Switched `Field_get` family errors (view / record) and `Record_update` field mismatch errors in `lib/typer.ml` to go through `raise_with_suggestion`: passes the corresponding record / view's declared field name list as candidates and adds nearby names by Levenshtein distance as `did you mean \`X\`?` in help: message. </strong>Qualified name typo<strong> (e.g. `Math.factrial` → `Math.factorial`) needs no implementation change — when env lookup for `Var "Math.factrial"` fails, existing `Var` branch uses entire env (including M-prefixed bindings inside Module) as candidates and calls suggest_name, which works naturally. Verified: `Pt { name, value }` then `p.namee` → `did you mean \`name\`?`; same for view fields; same for `{ p | namee = ... }` record update; `Math.factrial 5` → `did you mean \`Math.factorial\`?`. Added 4 tests (1166 passing). Remaining DEFERRED §5.1 (type variable rename hint / N-best candidate display) in separate slice.</strong></p> <ul> <li><strong>Phase 12 #9: `vec_filter` / `vec_to_list` / `vec_to_owned`</strong> —</li> </ul> <p> consumes DEFERRED §3.5 remainder and §3.6. Added 3 builtins: <code>vec_filter : Vec[R, T] -> (T -> bool) -> Vec[R, T]</code> (region-preserving, keeps only elements where predicate is true); <code>vec_to_list : Vec[R, T] -> T list</code> (converts to <code>'a list = Nil | Cons of 'a * 'a list</code>, builds Cons chain via <code>Array.fold_right</code>); <code>vec_to_owned : Vec[R, T] -> T OwnedVec</code> (<code>Array.copy</code> deep copy, returns OwnedVec independent of source — a way to extract region-internal Vec to heap). All schemes region-polymorphic; <code>vec_to_owned</code> result is drop_types-registered <code>OwnedVec</code> type so cannot be placed in region (<code>region R { ... vec_to_owned v ... &R ... }</code> auto-rejected as Trivial[R] violation). 3 backend codegen interpreter-only stubs for all 3 builtins. Added 10 tests (1162 passing): 3-scheme type inference; filter behavior / empty result; list conversion + empty Vec → [] display; deep copy to OwnedVec + independence from source mutations; region escape rejection. DEFERRED §3.5 fully resolved; §3.6 updated to one-way (Vec→Owned) resolved (reverse Owned→Vec needs region context, separate slice).</p> <ul> <li><strong>Phase 9 #5: precise import paths (importer-relative +</strong></li> </ul> <p> canonicalisation)<strong> — consumes DEFERRED §4.2. Phase 9.2 introduced cwd-relative `import "path";`; changed to </strong>importer-relative<strong> (resolved from the file containing the import statement). Added `Parser.current_base_dir : string ref`; `parse_program ?(base_dir = Sys.getcwd ())` for initial value. `import` branch: relative path via `Filename.concat !current_base_dir path`; canonicalized via `Unix.realpath`; during recursive parse swap `current_base_dir := Filename.dirname canonical` (restored on exception). Added `?base_dir` to Pipeline.process; CLI (bin/main.ml) passes `~base_dir:(Filename.dirname path)` in file mode. Canonicalisation makes different relative forms (e.g. `/tmp/foo.mere` vs `./foo.mere`) refer to same file → accurate cycle guard. Verified: `import "./sub/inner.mere"` resolves from main.mere's dir; nested imports (main → middle → sub/inner) work from each step's dir; same file via different relative forms loaded once. Added 3 tests (1152 passing). DEFERRED §4.2 updated to resolved.</strong></p> <ul> <li><strong>Phase 9 #4: `type` / `record` declaration inside modules</strong> —</li> </ul> <p> consumes last 1/3 of DEFERRED §4.1. Extracted T_type branch logic inside <code>parse_decls</code> (including record / variant / alias disambiguation) into helper <code>parse_type_decl_after_keyword</code>; added T_type branch to <code>parse_module_body</code> calling same helper. As a slice-1 limitation, <strong>type / record / constructor names are not M-prefixed and enter global registry</strong> — declaring same-named type in different modules conflicts (proper scoping in subsequent slice). Verified: <code>module M { type Pt = { x: int, y: int }; let mk = fn p -> Pt { ... } };</code> compute <code>p.x + p.y</code> from <code>M.mk (3, 4)</code>; <code>module M { type 'a opt = ... }; M.unwrap (S 42)</code> dispatches via variant; type and let mix OK. Added 3 tests (1149 passing). DEFERRED §4.1 fully resolved (3/3).</p> <ul> <li><strong>Phase 9 #3: nested modules + `open M;`</strong> — consumes 2/3 of</li> </ul> <p> DEFERRED §4.1 (remaining: type / record inside module). Refactored <code>parse_module_body</code> to take <code>cur_path</code> parameter; handles <code>T_module T_ident inner T_lbrace</code> recursively. Registers both short name (<code>inner</code>) and full path (<code>outer.inner</code>) to <code>module_names</code>; qualified access from both inside and outside works. Newly added <code>module_bindings : (string, string list) Hashtbl.t</code> registry — inside <code>prefix_module_decls</code>, records direct binding names (only names without dots); used to expand <code>open M;</code>. Added <code>open</code> keyword + T_open token to lexer; added <code>T_open T_ident name T_semi</code> branch to parser's <code>parse_decls</code>: extract <code>module_bindings[m_name]</code> and expand to chain of <code>Top_let (P_var n, Var "M.n")</code> aliases; unregistered module is parse error. Nested module direct binding names containing dots are excluded from <code>open</code> expansion (e.g. <code>module M { module N { ... }; let g = ... }</code> with <code>open M;</code> brings in only <code>g</code>; N exports referenced as <code>M.N.foo</code>). Verified: <code>module M { module N { let f = ... }; let g = N.f + 1 }; M.N.f + M.g</code> works; shortcut access after <code>open M;</code> coexists with <code>M.foo</code> qualified access. Added <code>examples/module_nested.mere</code>. Tutorial 10.5 updated: nested + <code>open</code> usage + constraints. Added 7 tests (1146 passing). DEFERRED §4.1 updated to "2/3 resolved" (type / record inside module is future work).</p> <ul> <li><strong>Phase 11 #7: borrow checker refinement (3) — borrow propagation</strong></li> </ul> <p> from match arms<strong> — continues DEFERRED §2.2 (match patterns). Added Match case to `extract_borrows`: union of `extract_borrows` from each arm body (which arm runs is runtime-dependent, so conservatively treat all arms as active). Guards are side conditions so not subject to extraction. While we're at it, extended `Let_rec` / `With` / `Region_block` bodies to also traverse recursively (these values can leak borrows when let-bound). Verified: `let r = match v with | N -> &R x | S _ -> &R x in let m = &mut R x in 0` → conflict; `let r = match v with | N -> &R x | S _ -> &R y in let m = &mut R y in 0` → conflict (else branch equivalent &R y also active); unrelated `&mut R z` OK. Added 3 tests (1139 passing). Remaining borrow checker DEFERRED: §2.3 NLL only.</strong></p> <ul> <li><strong>Phase 11 #6: borrow checker refinement (2) — borrow propagation</strong></li> </ul> <p> through if branches<strong> — consumes DEFERRED §2.2. Up through Phase 11.5, only `Let (P_var _, Ref ..., body)` patterns added borrow to active set; couldn't detect cases where </strong>if expression result leaks the borrow<strong>, like `let r = if cond then &R x else &R y in ...`. Added helper `extract_borrows : Ast.expr -> (region * place * mode * loc) list`: Ref to single-element list; If(cond, t, e) to </strong>union<strong> of extracts from t/e; Let(_, _, body) recurse from body; Annot recurse from inner; otherwise empty list. Refactored `check_borrows` `Let` branch: pass value through `extract_borrows` to get borrows propagating up; conflict-check each one and add to active set; pass union to body. Verified: `let r = if c then &R x else &R y in let m = &mut R y in 0` → conflict (else branch from y also active); `let r = if c then &R x else &R y in let m = &mut R z in 0` → OK (z unrelated); nested let-in-if recurses properly. Added 5 tests (1136 passing). Next stage is §2.3 NLL (Non-Lexical Lifetimes) — releasing borrow at "the moment it stops being used", equivalent to liveness analysis.</strong></p> <ul> <li><strong>Phase 11 #5: borrow checker refinement (1) — tracking complex</strong></li> </ul> <p> expressions (field chain)<strong> — consumes DEFERRED §2.1. Phase 11.4 only tracked simple Var for `x` in `&[mode] R x`; extended to identify field chains like `p.field` / `p.q.r`. Added `place_id : Ast.expr -> string option` helper (Var → Some name, Field_get inner f → Some "<inner>.<f>", otherwise None). Replaced Var-only checks in `check_borrows` `Ref` / `Let` branches with place_id based. Non-place expressions (function call results, literals etc.) continue to be skipped (None). Error messages also display dotted paths like `&R p.x`. Verified: `&R p.x + &mut R p.x` → conflict; `&R p.x + &mut R p.y` → OK; `&R p.x + &R p.x` → OK (shared read each other); `&R o.inner.v + &mut R o.inner.v` → conflict (nested chain); `&R p + &mut R p.x` → OK (whole p and p.x are separate places). Added 6 tests (1131 passing). Remaining borrow checker DEFERRED: §2.2 control flow analysis (separate borrow sets per if branch) and §2.3 NLL in separate slices.</strong></p> <ul> <li><strong>Phase 12 #8: `Map[R, K, V]` (region-aware mutable map)</strong> —</li> </ul> <p> Minimum harness for design doc 13_region_std_types.md §5 <code>Map[R, K, V]</code>. Same construction-time binding pattern as Vec[R, T] / StrBuf[R]. Type is 3-arg <code>TyCon ("Map", [TyRef BorrowedRead R TyUnit; K; V])</code>. Eval has <code>V_map of (value, value) Hashtbl.t</code> (OCaml polymorphic hash/eq) + 5 builtins (<code>map_new</code> / <code>map_set</code> / <code>map_get</code> / <code>map_has</code> / <code>map_len</code>). <code>map_get</code> on missing key is eval error; <code>map_has</code> for safe check. Typer has 5 schemes (region / K / V each as TyVar for polymorphism); <code>types["Map"] = 3</code>; <code>App (Var "map_new", _)</code> special-cased pulls region binding from active_regions (empty → __heap). Ast.pp_ty has 3-arg <code>Map[R, K, V]</code> bracket display (TyRef-of-unit / polymorphic both handled). Added <code>V_map</code> case to Phase 12.6 <code>len</code> builtin for polymorphic len. All 3 backend codegen interpreter-only stubs for Map type / 5 builtin names. Added <code>examples/map_basics.mere</code>: simple str→int, has-safe lookup, int→str (type reversal), short-lived inside region — 4 patterns demo. Tutorial 10.6 added Map API table + caveats (closure / ref as key identified per-ref). Added 10 tests (1125 passing): 5-scheme type inference; basic set/get; has branch; len with duplicate key; polymorphic type (int → str); eval error on missing key; region escape rejection; outside-region default; polymorphic len integration; codegen rejection. Now Q-010 main collections (Vec / OwnedVec / StrBuf / Map) all work in interpreter. Remaining: trait system proper (§3.1), unified Allocator trait API (§3.4), <code>OwnedVec</code> / <code>Vec</code> round-trip (§3.6), 3-backend codegen (§1.1).</p> <ul> <li><strong>Phase 12 #7: Vec higher-order APIs (iter / map / fold / set)</strong> —</li> </ul> <p> Implemented higher-order functions intended for Vec API in design doc 13_region_std_types.md §3. All region-polymorphic + element type polymorphic. <code>vec_map</code> result Vec bound to same region as source (region-preserving). Schemes: <code>vec_iter : Vec[R, T] -> (T -> unit) -> unit</code>; <code>vec_map : Vec[R, T] -> (T -> U) -> Vec[R, U]</code>; <code>vec_fold : Vec[R, T] -> U -> (U -> T -> U) -> U</code>; <code>vec_set : Vec[R, T] -> int -> T -> unit</code>. Eval calls user functions (V_closure / V_builtin) via <code>apply_value_ref</code> pattern (same as <code>flip</code> / <code>try_or</code> / <code>iter_n</code> etc.); placement after apply_value_ref definition. <code>vec_set</code> is in-place mutation; out-of-range index is eval error. 3 backend codegen interpreter-only stubs for all 4 names. Added <code>examples/vec_higher_order.mere</code>: int→int map / int→str map / fold for sum and max / set + iter / chain inside region — 5 patterns demo. Tutorial 10.6 section added higher-order API table + usage examples. Added 12 tests (1115 passing): 4-scheme type inference; map (incl. element type conversion); fold (sum); set + out-of-range; iter side effects via separate Vec; region-preserving behavior; codegen rejection. Remaining Q-010: Map[R, K, V]; Allocator trait; Vec / OwnedVec / StrBuf codegen support.</p> <ul> <li><strong>Phase 12 #6: `StrBuf[R]` (Q-010 narrowed — region-internal mutable</strong></li> </ul> <p> string buffer)<strong> — Minimum harness for design doc 13_region_std_types.md §4 `StrBuf[R]`. Same construction-time binding pattern as `Vec[R, T]` (Phase 12.3); type is 1-arg `TyCon ("StrBuf", [TyRef BorrowedRead R TyUnit])` (region marker only, same convention as view types). Added `V_strbuf of Buffer.t` to eval (internal storage in OCaml Buffer); `to_string` formats as `StrBuf["..."]`. Builtins: `strbuf_new : unit -> StrBuf[R]`, `strbuf_push : StrBuf[R] -> str -> unit`, `strbuf_to_str : StrBuf[R] -> str`, `strbuf_len : StrBuf[R] -> int`. Added 4 schemes to typer in polymorphic-region form (TyVar in region position); pre-register `types["StrBuf"] = 1`; `App (Var "strbuf_new", _)` special-cased same as vec_new pulls region binding from active_regions (empty → __heap). Added polymorphic `StrBuf[a]` bracket display to `Ast.pp_ty`. Added `V_strbuf` case to Phase 12.6 `len` builtin for length via polymorphic. 3 backend codegen rejects both type / builtin as interpreter-only. Added `examples/strbuf_basics.mere`: outside-region (default `__heap`) / inside region (auto-bound to `StrBuf[R]`) / polymorphic `len` — 3 patterns demo. Tutorial 10.6 updated: StrBuf[R] explanation + constraints. Added 9 tests (1103 passing): type inference; push/to_str round-trip; empty len; inside region binding; escape rejection; polymorphic len integration; codegen rejection. Remaining Q-010: `Map[R, K, V]`; Allocator trait; Vec/OwnedVec/StrBuf codegen support.</strong></p> <ul> <li><strong>Phase 12 #5: ad-hoc polymorphic `len` (Q-010 narrowed / lightweight</strong></li> </ul> <p> unified trait-style API)<strong> — Minimum practical alternative to a full trait system planned for `trait Collection { fn len(self) -> usize }` in design doc 13_region_std_types.md §6. Instead of introducing a full trait system (~500 LoC), added `len : 'a -> int` as an ad-hoc polymorphic builtin in the same frame as `show : 'a -> str`. Single scheme in typer (`'a -> int`); eval dispatches based on runtime value variant: `V_vec` (shared by Vec[R, T] and OwnedVec[T]) → array length; `V_str` → byte length; `V_tuple` → arity; `V_constr (Nil/Cons chain)` → list traversal counts elements; otherwise eval error. </strong>Single API<strong> for Vec[R, T] / OwnedVec[T] / `'a list` / `str` / `tuple`. 3 backend codegen reject `len` as interpreter-only stub. Added 8 tests (1094 passing): type inference; behavior for str / Vec / OwnedVec / tuple / list; eval error for unsupported value (int); codegen rejection. Full trait system introduction in future slice — whether trait's implicitness fully aligns with Mere's design philosophy (explicit > concise) is on hold.</strong></p> <ul> <li><strong>Phase 12 #4: `OwnedVec[T]` (Q-010 narrowed (b) separate type)</strong> —</li> </ul> <p> Implemented "separate type" portion of design doc 13_region_std_types.md §9 "(b) separate type + trait for unified API". Added <code>OwnedVec[T]</code> (heap-allocated, has Drop) in contrast to <code>Vec[R, T]</code> (region-internal, Trivial). Added <code>owned_vec_new / push / get / len</code> schemes (1-arg, <code>'a OwnedVec</code> form) to typer; <code>types["OwnedVec"] = 1</code> + <strong>registered in `drop_types`</strong> so that region-placement triggers automatic rejection by <code>contains_drop_type</code> (<code>Trivial[R] violated: cannot place value of type \</code>'a OwnedVec\<code> into region — type contains a Drop type</code>). Eval shares <code>V_vec</code> (only type system treats them as different; internal implementation is the same mutable array). 3-backend codegen rejects both owned_vec_<em> builtins and OwnedVec type as interpreter-only (unified message `Vec / OwnedVec builtins are interpreter-only`). Added `examples/vec_vs_owned_vec.mere`: contrasts short-lived region Vec and long-lived OwnedVec in one program. Tutorial 10.6 updated: OwnedVec[T] explanation + how to choose vs Vec[R, T]. Added 6 tests (1086 passing): type of owned_vec_new; polymorphic push/get/len; region rejection via Drop; contrast that Vec[R, T] can be placed in region; 3-backend codegen rejection. Remaining Q-010: `StrBuf[R]` / `Map[R, K, V]`; unified Allocator trait API (trait-based unification of read API); Vec / OwnedVec codegen support.</em></p> <ul> <li><strong>Phase 12 #3: semantic backing for `Vec[R, T]` (Q-010 narrowed →</strong></li> </ul> <p> implementation stage 3)<strong> — Gives type system that actually tracks region to `Vec[R, T]` syntax that was parse-only in Phase 12.2. Changed Vec arity from 1 → 2; internal representation unified to `TyCon ("Vec", [TyRef BorrowedRead R TyUnit; T])` (region marker convention same as view types). Parser: `Vec[R, T]` emitted as 2-arg; legacy `T Vec` (1-arg postfix) auto-filled with default region `__heap` and expanded to 2-arg form (forward-compat). With </strong>TyVar in region position of scheme<strong>, region-polymorphic APIs are realized through scheme machinery as-is (`vec_push : forall T R_marker. Vec[R_marker, T] -> T -> unit`); R_marker unifies with concrete region marker at call site. Added special handler to `Typer.infer` App case: `App (Var "vec_new", _)` reads innermost active_regions and directly binds region of `Vec[R, T]` (same shape as view construction); empty → `__heap`. Added bracket display for 2-arg Vec to `Ast.pp_ty` (`Vec[R, int]` / `Vec[__heap, 'a]` / `Vec['a, 'b]` etc.). Verified: `vec_new ()` outside → `Vec[__heap, 'a]`; `region R { vec_new () }` → `Vec[R, 'a]` (escape is static error); `fn (v: Vec[R, int]) -> vec_len v` → `(Vec[R, int] -> int)`; `fn (v: int Vec) -> vec_len v` → `(Vec[__heap, int] -> int)`. Updated `examples/vec_basics.mere`: demonstrates auto-bind of region for `vec_new ()` inside region. Tutorial 10.6 updated: noted that region got semantic backing + explicit escape check. Added 3 tests + updated 7 existing tests to new format expectations (1080 passing). Remaining Q-010: explicit distinction from OwnedVec[T]; StrBuf[R] / Map[R, K, V]; unified Allocator trait API; Vec codegen support.</strong></p> <ul> <li><strong>Phase 12 #2: `Vec[R, T]` syntax (Q-010 narrowed → implementation</strong></li> </ul> <p> stage 2, lightweight)<strong> — Forward-compatible slice that accepts the notation `Vec[R, T]` from design doc 13_region_std_types.md into parser. Added `T_ident name :: T_lbracket :: ...` branch to `simple_ty` in `lib/parser.ml` (name is uppercase): parses bracket-delimited argument list; region marker (bare uppercase ident yielding TyCon name=[]) dropped; remaining type arguments passed to `expand_alias_or_tycon name type_args`. Result is that `Vec[R, int]` is internally identical to `int Vec` (1-arg TyCon) — generates same TyCon. Region R is a documentation marker currently with no semantic backing (region-aware allocation / lifetime tracking implementation planned in future slice). Updated `examples/vec_basics.mere`: demonstrates `(vec_new () : Vec[R, int])` annotation inside region. Tutorial 10.6 section updated: `Vec[R, T]` syntax can now be written; current R is documentation only; both forms (`int Vec` / `Vec[R, int]`) produce equivalent types. Added 3 tests (1077 passing): type annotation parse; str version; `int Vec` and `Vec[R, int]` produce same type. Implementation scale: only ~25 lines added to parser.ml. Next slice (12.3) gives R semantic backing: reflect active_regions in vec_new return type (view construction pattern).</strong></p> <ul> <li><strong>Phase 12 #1: `'a Vec` minimum harness (Q-010 narrowed →</strong></li> </ul> <p> implementation stage 1)<strong> — Adds basic variable-length vector as polymorphic builtin under name `'a Vec`, the most basic of design doc `13_region_std_types.md` region-version std types. Phase 12 total (Vec[R,T] / OwnedVec[T] / StrBuf[R] / Map[R,K,V] / Allocator trait etc.) narrowed to MVP; syntax for region parameters in type and distinction from OwnedVec come in subsequent slices. Added `V_vec of value array ref` (storage in OCaml mutable array; push appends with reallocate) + 4 builtins (`vec_new : unit -> 'a Vec`, `vec_push : 'a Vec -> 'a -> unit`, `vec_get : 'a Vec -> int -> 'a`, `vec_len : 'a Vec -> int`) to `lib/eval.ml`; `to_string` formats as `Vec[...]`. Added 4 schemes (`vec_new_scheme` etc.) to `lib/typer.ml`; `Hashtbl.replace types "Vec" 1` pre-registers as arity-1 polymorphic type. Registered in `initial_env`. Trivial[R] check works because existing `contains_drop_type` walks recursively, so placing `Conn Vec` (where Conn is a drop type) in region is auto-rejected. Added explicit stubs to Var handlers of codegen (C / LLVM / Wasm) raising `Codegen_error` when they see `vec_new` / `vec_push` / `vec_get` / `vec_len` (all 3 backends emit `interpreter-only` message). Added `examples/vec_basics.mere`: basic operations on int / str Vec + Vec inside region demo. Added 14 tests (1074 passing): type inference for 4 builtins; len of empty Vec; len/get after push; polymorphic (str Vec); region placement OK; Conn Vec rejected with Trivial[R]; eval error for out-of-range get; 3-backend codegen rejection. Future slice candidates: Vec[R, T] with region as parameter + Allocator trait + distinction from OwnedVec[T].</strong></p> <ul> <li><strong>Phase 11 #4: borrow checker minimum harness</strong> — Slice that</li> </ul> <p> consumes Q-004 "remaining implementation TODO". Added <code>check_borrows : (string * string * borrow_mode * Loc.t) list -> Ast.expr -> unit</code> to <code>lib/typer.ml</code>. Threads borrows for the same (region, var name) as active set through lexical scope; rejects coexistence of conflicting modes with <code>Type_error</code>. Coexistence allowed pairs defined in <code>borrows_compatible</code>: only shared read with shared read (<code>BorrowedRead</code> + <code>BorrowedRead</code>) and shared write with shared write (<code>SharedWrite</code> + <code>SharedWrite</code>); all else conflicts (<code>exclusive</code> family doesn't coexist with anything; shared read + shared write also rejected due to invalidation risk). AST walk: when discovering <code>Let (P_var p, Ref (mode, region, Var v_name), body)</code>, adds <code>(region, v_name, mode, value.loc)</code> to active set and recurses on body; free-standing <code>&[m] R v</code> also conflict-checks with active. <code>Pipeline.process</code> calls <code>Typer.check_borrows [] (Ast.desugar_program prog)</code> after <code>Typer.infer</code> to inspect program in one pass. Added <code>examples/borrow_conflict.mere</code> (intentional failure demo: taking <code>&mut R v</code> after <code>&R v</code>). Error message includes "previous borrow at line N, col N" note. Verified: <code>let a = &R v in let b = &mut R v</code> / <code>let a = &mut R v in let b = &mut R v</code> / <code>let a = &R v in let b = &shared write R v</code> / <code>let a = &exclusive R v in let b = &R v</code> all reject as conflict; <code>let a = &R v in let b = &R v</code> / 2 shared write / different variables OK. Added borrow checker explanation + conflict example output to <code>docs/tutorial.md</code> 10.4 section. Added 8 tests (1060 passing). Currently tracking is limited to simple Var for <code>x</code> in <code>&[m] R x</code> — complex expressions (<code>&R rec.field</code> etc.) in future. Now Q-004 design (b) borrow annotation refinement is complete in both "can be written as types + machine-verifies conflict".</p> <ul> <li><strong>Phase 11 #3: auto-deref for field access through `&R T`</strong> — At</li> </ul> <p> Phase 11.1 borrow annotation introduction, field access like <code>lg_ref.info "hi"</code> was crashing with <code>field access on non-record value</code>. Added <code>strip_refs</code> helper to <code>Field_get</code> case of <code>lib/typer.ml</code> (recursively peels TyRef wrappers); changed to perform existing view / record judgment on type after peeling. Borrow mode remains static contract; eval side already passed <code>&R v</code> through, so zero runtime changes. Result: method calls work directly through any of <code>&R Logger</code> / <code>&mut R Logger</code> / <code>&shared write R Logger</code>, like <code>lg.info "msg"</code>. Fully rewrote examples/borrow_modes.mere: rewrote signature-only demo to actually call cap methods (<code>log_action</code>, <code>db_run</code>, <code>show_config</code>) across borrow; prints <code>mk_logger</code>'s <code>[INFO]</code> output + DbHandle's <code>exec</code> call + AppConfig's <code>name</code>/<code>threads</code> read. Added 5 tests (1052 passing): field access on Pt record through <code>&R</code>; through <code>&mut R</code>; through <code>&shared write R</code>; type inference for user-defined Lg11; type confirmation extracting field from <code>&R Lg11r</code>.</p> <ul> <li><strong>Phase 11 #2: borrow annotation realistic example + tutorial 10.4</strong></li> </ul> <p> section<strong> — Milestone showing "what is it good for" of the 4 modes added in Phase 11.1 (`&R T` / `&mut R T` / `&shared write R T` / `&exclusive R T`). Added `examples/borrow_modes.mere`: realistic demo constructing 3 kinds — Logger (shared write) / DbHandle (exclusive write) / AppConfig (shared read) — inside region, then borrowing each cap with appropriate mode and passing to handler. Run prints "[logged] save_order" / "[exclusive] UPDATE ..." / "[read]". Added `examples/borrow_modes_typeerror.mere`: </strong>intentionally fails with type error<strong> demo passing `&R db` to `&mut R DbHandle` parameter (displays as documentation that `expected \`&mut R DbHandle\`, got \`&R DbHandle\`` is shown). Added 10.4 "Borrow annotation" section to `docs/tutorial.md` (4-mode table + usage examples + mode mismatch error example + current limitations (borrow checker exclusion rules and `&R T` field auto-deref are future work)). Also added 2 new examples to section 12 examples list. No test count change (1047 still). Phase 11.1 brought "writable as type" state; Phase 11.2 brought "readable with understood meaning" state. Next slice candidates: borrow checker (exclusion rules) and `&R T` field auto-deref.</strong></p> <ul> <li><strong>Phase 11 #1: borrow annotation refinement (Q-004 narrowed →</strong></li> </ul> <p> implementation stage 1)<strong> — Minimum harness for narrowing (b) borrow annotation refinement in design doc 08_effect_granularity.md down to implementation. Added `borrow_mode = BorrowedRead | SharedWrite | ExclusiveRead | ExclusiveWrite` to AST; signatures for `TyRef of borrow_mode * string * ty` (type level) and `Ref of borrow_mode * string * expr` (value level) changed to 3-arg. 4 new syntaxes in parser: `&R T` (default = BorrowedRead); `&mut R T` (ExclusiveWrite); `&shared write R T` (SharedWrite); `&exclusive R T` (ExclusiveRead). Value level `&R v` / `&mut R v` / `&shared write R v` / `&exclusive R v` similarly. `mut` / `shared` / `write` / `exclusive` are contextual keywords (regular idents in lexer; parser recognizes only after `&`). Typer's unify changed to require "region and mode equality" for `TyRef (m1, r1, t1) ↔ TyRef (m2, r2, t2)` (strict, no subtyping). pp_ty handles `&R T` / `&mut R T` / `&shared write R T` / `&exclusive R T`. Codegen (C / LLVM / Wasm) ignores mode — pointer representation is the same; only static guarantee. Verified: `fn (x: &mut R int) -> ...` type display OK; passing `&R 5` to `fn (x: &mut R int) -> 1` is type error `expected \`&mut R int\`, got \`&R int\``; calls with same mode pass; `(&R 5 : &mut R int)` annotation mismatch is type error. Logger problem (shared write representation) solved at syntax level; borrow checker (exclusion rules) in future slice. Added 14 tests (1047 passing).</strong></p> <ul> <li><strong>Phase 10 #1: aggregating where we are — tutorial / README / new</strong></li> </ul> <p> examples / SUMMARY<strong> — Milestone with 1033 tests / 3 backends / REPL / module / import in place; arranging outward-facing documentation. Added 10.5 "Modules and import" section and 11.5 "Using the REPL" section to `docs/tutorial.md`; rewrote 13 "Native compilation" from C-only to 3-backend (C / LLVM / Wasm); updated closing remark from "memory model is not implemented in codegen" to "works in all backends". Full rewrite of `README.md`: status as of 2026-06-19 (1033 tests / 3 backend parity / module / import / REPL commands); added rows for module, import, REPL command, error UX to features table; added LLVM / Wasm build paths to build examples. New examples: `examples/module_basic.mere` (`module Math { let inc / square / pow / inc_then_square ... }` + shortened internal reference demo); `examples/lib_list_ops.mere` (decls-only library exporting `module ListOps { sum / length / map }`); `examples/import_demo.mere` (imports lib with `import "examples/lib_list_ops.mere";`); `examples/repl_session.md` (Markdown showing `:type` / `:env` / `:show` / `:load` / `:reset` / multi-line in dialog session format). Created new `internal design notes`: restructured destinations of Phases 1-9 as "outward-facing" (5-min status delivery to future self / sharing partners); aggregates feature coverage, history phase table, what's missing, next directions. No test count change (1033 still).</strong></p> <ul> <li><strong>Phase 9 #2: file split — `import "./other.mere";`</strong> — Added</li> </ul> <p> <code>import</code> keyword + <code>T_import</code> token to lexer. Added <code>imported_files : (string, unit) Hashtbl.t</code> registry and <code>parse_decls</code> <code>T_import T_string path T_semi</code> branch to parser: reads target file with <code>In_channel.with_open_text</code>, recursively calls <code>Lexer.tokenize</code> + parse_program_internal, mixes resulting decls into current decl stream with List.rev_append (discards main expression). Skips same path if already registered (cycle prevention). Split <code>parse_program</code> into <code>parse_program_internal</code> (recursive worker) + <code>parse_program</code> (top-level wrapper, runs worker after <code>Hashtbl.reset imported_files</code>) — top-level cycle guard accumulator extends throughout recursive imports while being fresh per top-level call. Parser registries (constructors / records / module_names / aliases) are shared across recursive calls, so types / records / modules defined in imported files are visible from importer side. Verified: <code>import "/tmp/lib.mere"; helper base</code> references helper / base from another file; <code>import "/tmp/lib_mod.mere"; Math.sq (Math.dbl 5)</code> qualifiedly references module in import; mutual <code>cyc_a ↔ cyc_b</code> imports yield a_val + b_val = 30 (no infinite loop thanks to cycle guard); diamond pattern (importing lib via both A and B) loads once without duplication; missing file is parse error. Added 6 tests (1033 passing). Base path resolution is cwd-based; symlinks / different relative forms treated as different files (canonicalisation in future).</p> <ul> <li><strong>Phase 9 #1: minimum module harness — `module M { let f = ...; }`</strong></li> </ul> <p> + <code>M.f</code> reference<strong> — Next milestone for language surface. Added `module` keyword + `T_module` token to lexer; added `module_names : (string, unit) Hashtbl.t` registry and `parse_module_body` to `parser.ml` (slice 1: only `let` / `let rec`; terminates at `T_rbrace`); added `prefix_module_decls` (rewrites binding names and free Var references in body with `M.` prefix). Newly implemented `Ast.rename_free_vars`: shadowing-aware AST walker that excludes bind names computed by `pattern_vars` from shadow list in `Fun (param, ...)` / `Let (P_var p, ...)` body / `Let_rec [(n, _); ...]` / `With (n, ...)` body / `Match` arm patterns. Extended parser's `field_chain`: if lhs is `Var "M"` and `M ∈ module_names`, emits `Var "M.f"` instead of `Field_get`. uppercase ident atom_base also checks `module_names` before constructor / record judgment. Added decls-only mode to `parse_program` (main = `()` if only T_eof); removed `Repl.prepare_input`'s `; ()` hack (made no-op, left as identity wrapper for compatibility). Verified: `module M { let answer = 42; let add = fn x -> fn y -> x + y; }; M.add M.answer 8` → 50; internal `inc (inc x)` shortened references rewritten as `M.inc (M.inc x)`; `let rec fact = fn n -> ... fact (n-1)` M.fact self-call works; `module M; module N;` same-name bindings don't conflict; `p.x` regular field access unchanged. In REPL also can write `module M { ... }` multi-line directly; `M.f` appears in `:env`. Added 7 tests (1027 passing). Types / records / nested modules in future slices.</strong></p> <ul> <li><strong>Phase 8 #2: REPL continued — `:show NAME` + `:reset`</strong> — Added 2</li> </ul> <p> new commands to <code>lib/repl.ml</code>. (1) <code>:show NAME</code> outputs type and value at once: <code>format_show eval_env type_env name</code> helper pulls scheme from <code>type_env</code> and <code>value ref</code> from <code>eval_env</code> respectively, returns string in <code>val NAME : TY\n = VAL</code> format (uses <code>Eval.to_string</code>, so closures are <code><closure:p></code>, str is quoted, numbers / records / variants in same formatter). Unbound name yields <code>unbound name: NAME</code>. <code>print_show</code> is print entry of same content. (2) <code>:reset</code> rewinds both envs to <code>Eval.initial_env</code> / <code>Typer.initial_env</code> via <code>do_reset eval_env type_env</code>; displays <code>(envs reset)</code>. Added 2 lines to help text. Verified: <code>let x = 42; let g = "hi"; :show x</code> → "val x : int\n = 42"; <code>:show g</code> → "val g : str\n = \"hi\""; <code>:show inc</code> (closure) → "val inc : (int -> int)\n = <closure:n>"; <code>:show nope</code> → "unbound name: nope"; after <code>:reset</code> env cleared, <code>:env</code> → "(no user bindings)". Added 5 tests (1020 passing; split I/O of <code>format_show</code> / <code>do_reset</code> to directly assert pure parts).</p> <ul> <li><strong>Phase 8 #1: REPL UX improvement — multi-line input +</strong></li> </ul> <p> Diagnostic.format integration + :env / :load<strong> — 4-point enhancement to `lib/repl.ml`. (1) Switched to loop accumulating multiple lines with `read_logical_input`: if tentative parse after input yields "error at T_eof location", treats as incomplete and prompts `..>` for continuation; returns `Some input` on parse success. `is_unfinished ~source` judges by whether `Parser.Parse_error` loc matches T_eof loc in tokenize result (`eof_loc` helper + `loc_eq`); `Lexer.Lex_error "unterminated string literal"` also treated as unfinished. Empty line in continuation is `(input aborted)`; line starting with `:` interrupts multi-line buffer for standalone command execution. (2) Replaced `format_exn` with `format_diag ~source`; passes each error (`Lexer / Parser / Typer / Eval`) through `Diagnostic.format ~source ~filename:"<repl>"` — REPL also displays with Rust-style code frame, same as file mode. (3) Added `:env` command: `user_bindings` helper excludes builtin names of `Typer.initial_env` and returns only user-added bindings in insertion order, listed as `val name : type`. (4) Added `:load FILE` command: reads file, adds decls to eval/type env through `process_decl`; displays added bindings as `val name : type` then `(loaded path)`. Updated help text for new commands. Verified: can directly write multi-line `let rec` like fib/factorial in REPL; type error `let x = 5 + "hi" in x` displays caret + help:; `:load /tmp/foo.mere` loads definitions and they can be confirmed with `:env`. Added 9 tests (1015 passing) — REPL helpers (probe_unfinished detects each pattern; user_bindings insertion order / empty user env).</strong></p> <ul> <li><strong>Phase 7 #7: type error UX — hint expansion + App type error</strong></li> </ul> <p> direction fix<strong> — Expanded coverage of `Typer.type_conversion_hint`: (1) `expected int, got bool` → `use \`if b then 1 else 0\` to get an \`int\` from a \`bool\``; (2) `TyTuple ts1` vs `TyTuple ts2` arity mismatch → `tuple lengths differ — expected N element(s), got M`; (3) per-direction branching for `expected fn, got value` (extra arg / partial application); (4) `TyCon (n1, _)` vs `TyCon (n2, _)` name difference → `these are different named types (\`n1\` vs \`n2\`)`. Further restructured `Typer.infer` `Ast.App (f, arg)` case into 3 sub-cases: (a) `tf = Ast.TyArrow (param_ty, ret_ty)` → caret at arg.loc + `expected param_ty, got ta` via `unify arg.loc param_ty ta`; (b) `tf = TyVar _` → fresh var + whole unify as before; (c) others (extra arg case where `inc 3` portion of `int 3 4` is `int` etc.) → dedicated error `expected a function (\`'a -> 'b\`), got \`<actual>\`` + `help: you may be passing one too many arguments (...)`. Verified: `inc 3 4` → "expected a function, got int / help: too many arguments"; `add "hi" 3` → "expected int, got str / help: use str_len" (caret at arg.loc); `add 1 + 2` (= `add 1` arrives at int) → "expected int, got (int -> int) / help: missing an argument"; `true + 1` → "expected int, got bool / help: use if b then 1 else 0"; `f (1, 2, 3)` (where f is `(int, int) -> ...`) → "expected (int * int), got (int * int * int) / help: tuple lengths differ — expected 2, got 3"; distinct named records → "expected BarN, got FooN / help: different named types (BarN vs FooN)". Added 6 tests (1006 passing).</strong></p> <ul> <li><strong>Phase 7 #6: type error UX — type conversion hint</strong> — Added</li> </ul> <p> <code>Typer.type_conversion_hint t1 t2 -> string option</code> helper; appends <code>help: ...</code> after base message in unify error (via with_hint). Covered cases: <code>expected str, got int/bool</code> → <code>use \</code>show x\<code></code>; <code>expected int, got str</code> → <code>use \</code>str_len s\<code> ...</code>; <code>expected bool, got int/str</code> → <code>wrap in a comparison</code>; <code>expected fn, got value</code> → <code>you may be missing an argument</code>; <code>expected value, got fn</code> → <code>you may have passed a partially-applied function</code>. Other cases get no hint. Verified: <code>"answer: " ++ 42</code> → <code>help: use \</code>show x\<code></code>; <code>5 + "hi"</code> → <code>help: use \</code>str_len s\<code></code>; <code>if 1 then ... else ...</code> → <code>help: wrap in a comparison</code>. Added 4 tests (<strong>1000 passing — milestone</strong>).</p> <ul> <li><strong>Phase 7 #5: type error UX — source span (caret range display</strong></li> </ul> <p> with token width)<strong> — Extended `Loc.t` from `{ line; col }` to `{ line; col; width }`; added `Loc.mk ?(width=1) ~line ~col ()` helper (default width = 1 for backward compatibility; `Loc.dummy` has width = 0). In lexer's `tokenize`, attached token char count to pos via `with_width pos w` at output of each token: identifier / tyvar / string literal / int literal / float literal / 1-3 char operator (existing kept at 1). In `Diagnostic.format`, extended caret to multiple chars with `String.make (max 1 width) '^'`; applied bold-red ANSI color to all carets. Verified: in `let y = x + "hello"` error from `^` alone to `^^^^^^^^^^` (10 chars); in `factrial` identifier error to `^^^^^^^^` (8 chars); in `add "hello"` `add` to `^^^` (3 chars). Added 3 tests (996 passing).</strong></p> <ul> <li><strong>Phase 7 #4: type error UX — ANSI coloring</strong> — Added</li> </ul> <p> <code>Diagnostic.use_color : bool ref</code> (default false); CLI (<code>bin/main.ml</code>) sets to <code>true</code> when <code>Unix.isatty Unix.stderr && not NO_COLOR</code>. <code>ansi</code>/<code>red</code>/<code>blue</code>/<code>cyan</code>/<code>bold</code>/<code>bold_red</code>/<code>bold_cyan</code> helpers selectively insert escape codes (<code>\027[CODEm ... \027[0m</code>). In Diagnostic.format, kind is bold-red; line number, <code>|</code>, <code>--></code>, <code>=</code> in gutter are blue; caret <code>^</code> is bold-red; help: / note: keywords are bold-cyan. When <code>use_color = false</code>, everything passes through (test compatibility). Also respects NO_COLOR env var (https://no-color.org/). Verified: when run via TTY (via <code>script</code>), colored; plain when piped; plain when <code>NO_COLOR=1</code>. Added 5 tests (993 passing).</p> <ul> <li><strong>Phase 7 #3: type error UX — suggesting typo corrections via</strong></li> </ul> <p> Levenshtein<strong> — Added `Typer.levenshtein` (edit distance calculation, O(la*lb) DP), `Typer.suggest_name` (`max_dist` based on length, 3/2/1), `Typer.with_hint` / `raise_with_suggestion` helpers. Changed Type_error raises in `unbound variable` / `unknown constructor` / `unknown record type` (both in expression and in pattern) to go through `raise_with_suggestion`; appends `help: did you mean \`<name>\`?` if there's a close candidate. Extended `Diagnostic.format`: splits msg by `\n`; headline goes beside caret of code frame; rest (help:/note:) renders after code frame in `= help: ...` format. Verified: `factrial + 1` (factorial in scope) → "unbound variable: factrial / help: did you mean `factorial`?"; `Greeen` (Color = Red | Green | Blue) → "unknown constructor: Greeen / help: did you mean `Green`?"; `zzzzzz` (no close name) → no hint. Distance threshold adjusts by name length (stricter for short names); tie-break prefers shorter. Added 4 tests (988 passing).</strong></p> <ul> <li><strong>Phase 7 #2: type error UX — "expected X, got Y" form + audit of</strong></li> </ul> <p> unify call order<strong> — Changed `Typer.unify` error wording from `"type mismatch: \`X\` vs \`Y\`"` to `"expected \`X\`, got \`Y\`"` (X=expected, Y=actual). At the same time, </strong>unified <code>unify loc t1 t2</code> calls across Typer to <code>(expected, actual)</code> order<strong>: primitive type checks for Neg / Bin (+, -, *, /, %, ++) / Logic / If condition swapped to `unify ... Ast.TyXxx actual` (TyXxx=expected); Fun annotation `unify t' alpha` (annotation=expected); Match guard `unify TyBool tg`; each Match arm `unify result_var tb` (first arm is expected); Record_lit / Record_update field `unify exp_ty t` (declared=expected); Field_get / Record_update base `unify result_ty t_base`. Constr arg `unify exp ta` (param=expected). Symmetric cases (`==` lhs/rhs; if branch then/else; P_or bs1/bs2; let-rec alpha vs body) preserve meaningful order. Pattern checks (P_int/Bool/Str/Unit/constr/tuple/record) were originally `unify expected XXX` (scrutinee=expected) so no change needed. App preserves original `unify tf (TyArrow (ta, result))` (recursive structural unify compares tf.param and ta yielding "expected param_ty, got arg_ty"). Verified: `let y = x + "hello"` → "expected `int`, got `str`"; `add "hi"` (add: int->int) → "expected `int`, got `str`"; `if cond then "yes" else 42` → "expected `str`, got `int`"; record field → "expected `int`, got `str`". Added 4 tests (984 passing).</strong></p> <ul> <li><strong>Phase 7 #1: type error UX improvement — Rust-style code frame</strong></li> </ul> <p> — Rewrote <code>Diagnostic.format</code> in <code>lib/diagnostic.ml</code> to Rust-style multi-line code frame: header (<code>kind: msg</code>); location pointer <code>--> filename:line:col</code>; line-numbered margin (<code>1 | ...</code>); caret + message below error line (<code> | ^ ...</code>); context of 2 lines before + 1 line after. Changed terminal error message of <code>Typer.unify</code> from <code>"cannot unify X with Y"</code> to <code>"type mismatch: \</code>X\<code> vs \</code>Y\<code>"</code> (type names enclosed in backticks, neutral order). At zero-loc, 1-line fallback as before. Verified: <code>let y = x + "hello"</code> displays as <code>type error: type mismatch: \</code>str\<code> vs \</code>int\<code> --> file:2:13 | 1 | let x = 5 in | 2 | let y = x + "hello" in | | ^ type mismatch: ... | 3 | y</code>. Parse error / unbound variable error etc. output in common format. Added 6 tests (980 passing). Phase 7 started — improving language surface developer experience.</p> <ul> <li><strong>Phase 6 #12: Wasm codegen special-cases `'a list` show in</strong></li> </ul> <p> <code>[a, b, c]</code> form<strong> — Wasm version of LLVM Phase 5.14. In `emit_show_fn`'s variant branch, processes `TyCon ("list", [elem_ty])` as special-case before others: loop scan with cur / acc / first / tag / pl / h locals. `block $end` + `loop $lp` loads tag from head, break on Nil; on Cons, loads payload (tuple offset) → head = `i32.load offset=0 payload` → concat `, ` if needed (first flag) → concat `show_<elem_tag>(h)` → cur = tail = `i32.load offset=4 payload` → loop. After end, concat `]`. `[` / `]` / `, ` deduped via `intern_show_str`. Verified (wat2wasm + Node.js): `show [1, 2, 3]` → `[1, 2, 3]`; `show (Nil : int list)` → `[]`; `show ["hello", "world"]` → `["hello", "world"]`. Added 3 tests (974 passing). </strong>3 backends (C / LLVM / Wasm) fully parallel — the same Mere program runs on each of 3 backends as native binary / WAT<strong>.</strong></p> <ul> <li><strong>Phase 6 #11: Wasm codegen show general builtin</strong> — Wasm version</li> </ul> <p> of LLVM Phase 5.12. Wasm has no <code>asprintf</code> equivalent so <strong>all hand-rolled</strong>: <code>show_int</code> performs int→decimal string conversion on Wasm (allocates 16-byte buffer from bump pointer → writes digits right-to-left → prepends <code>-</code> if needed → returns pointer to first digit); <code>show_bool</code> registers <code>true</code> / <code>false</code> in data segment and branches with <code>select</code>; <code>show_str</code> is 2-stage concat wrapping with <code>"</code>; <code>show_unit</code> is const offset of <code>()</code>; <code>show_tuple_X_Y</code> concatenates <code>(</code>, each element show, <code>, </code>, <code>)</code> via <code>__lang_str_concat</code>; <code>show_<R></code> concats <code>R { f1 = </code>, each field show, <code>, f2 = </code>, <code> }</code>; <code>show_<V></code> is tag dispatch (nested if/else of <code>i32.load + i32.eq</code>) → each ctor: data ptr direct if nullary; concat <code>ctor_name + " "</code> + recursive payload show if payload. <code>show_types</code> Hashtbl + <code>collect_show_types</code> + <code>add_show_type</code> registers types + recursively registers dependent types (cycle guard). <code>subst_params</code> helper applies args of polymorphic record/variant (Wasm also emits separate function per mono instance; layout is shared). <code>intern_show_str</code> dedupes literals to save data segment. <code>App (Var "show", arg)</code> dispatches to <code>call $show_<ty_tag arg.ty></code>. Verified (wat2wasm + Node.js): <code>show 42</code> → "42"; <code>show true</code> → "true"; <code>show "hi"</code> → "\"hi\""; <code>show (1, "hi")</code> → <code>(1, "hi")</code>; <code>show (SS 42)</code> → "SS 42"; <code>show (Pt { x = 3, y = 4 })</code> → <code>Pt { x = 3, y = 4 }</code>; <code>show (Cons (1, Cons (2, Cons (3, Nil))))</code> → <code>Cons (1, Cons (2, Cons (3, Nil)))</code> (recursive variant works naturally). Added 8 tests (971 passing). <code>'a list</code> special-case <code>[a, b, c]</code> form in future slice.</p> <ul> <li><strong>Phase 6 #10: Wasm codegen complex patterns (P_int / P_str /</strong></li> </ul> <p> P_bool / P_unit / P_record / P_as / nested ctor / or / guard)<strong> — Wasm version of LLVM Phase 5.11. Rewrote `compile_pat` as fully recursive `(cond_local_slot, bindings)` function: P_int → `i32.eq`; P_bool → `i32.eq`; P_str → `call $__lang_streq` (new runtime helper, byte-by-byte compare yielding i32 boolean); P_unit → constant true; P_record → declared field order `i32.load offset` + sub-pattern recurse (handles both record / view); P_as → inner pattern + whole value bind; P_tuple → each element `i32.load offset=i*4` + recurse; P_constr → tag test (`i32.load offset=0 + i32.eq`) + sub-pattern recurse (nested OK). Multiple sub-tests chained with `combine_and` helper via `i32.and`. Or-patterns pre-flattened with `expand_or`. Guard evaluated in arm's bindings scope, AND with cond, short-circuit with `if/else` (no guard eval if cond is false). Added `@__lang_streq` runtime helper (block + loop with sequential byte_a / byte_b compare). Verified (wat2wasm + Node.js): `match 3 with | 0 -> 100 | 1 -> 200 | _ -> 300` → 300; `match "hello" with | "hi" -> 1 | "hello" -> 2 | _ -> 9` → 2; `match Cons (SS 5, Nil) with | Cons (SS n, _) -> n` → 5 (nested ctor); `match Pt { x = 3, y = 4 } with | Pt { x = a, y = b } -> a + b` → 7; `(a, b) as p → fst p + snd p + a + b` → 6; `LCgA | LCgB -> 1` → 1 (or); `when n < 10 -> 200` → 200 (guard). Added 8 tests (963 passing).</strong></p> <ul> <li><strong>Phase 6 #9: Wasm codegen polymorphic variant / record + recursive</strong></li> </ul> <p> variant + P_tuple sub-pattern<strong> — Wasm memory layout is uniform (every value is i32 = 4 bytes), so LLVM-style (Phase 5.9 / 5.10) monomorphization is not needed. `'a opt`, `'a Box`, `'a list = Nil | Cons of 'a * 'a list` all work via same code path as mono variant/record. Removed `params <> []` check in `Constr` and `r_params <> []` check in `Record_lit` (Wasm doesn't emit type-specific struct typedefs, so same code works for multi-instantiation). To expand `'a list` Cons (tuple payload `('a, 'a list)`) in Match, added `P_tuple` sub-pattern to `compile_pat` equivalent in `Match`: loads each element from payload tuple offset via `i32.load offset=i*4` into fresh local and binds (`Cons (h, t)` → h, t each loaded into separate locals). Verified (wat2wasm + Node.js): `type 'a opt; match LSome 42 with | LSome n -> n` → 42; `type 'a Box; let bi = Box { v = 42 } in let bs = Box { v = "hi" } in str_len bs.v + bi.v` → 44; `type 'a list; sum [1,2,3,4,5]` → 15; `length ["a","b","c","d"]` → 4. Added 4 tests (955 passing). Wasm backend's advantage: layout uniformity makes monomorphization unnecessary.</strong></p> <ul> <li><strong>Phase 6 #8: Wasm codegen Region_block + Ref + with Drop + view</strong></li> </ul> <p> construction + Unit_lit<strong> — Wasm version of LLVM Phase 5.13. Wasm's linear memory + `__lang_bump` global already acts as one region, so user's `region R { body }` is implemented in LIFO: save current value of `__lang_bump` to local at entry → evaluate body → stash result in another local → restore bump to saved value → push result back. This way allocations within region scope are "freed" at scope end (subsequent allocations can overwrite as bump pointer returns). `Ref (R, v)` (`&R v`) evaluates inner + bump 4-byte alloc + `i32.store offset=0` + push base. `With (c, v, body)` saves v to local + evaluates body + after body, if v's record has `close: unit -> unit` field, pulls env/fn_idx from closure value via `i32.load` + auto-invokes with `i32.const 0` (unit arg) + `call_indirect (type $cl)`, drops result, pushes body value. `view V[R] of T { ... }` Record_lit handled separately by view-name (same memory layout as record, bump alloc + i32.store); Field_get of view value uses field index from `Typer.views.v_fields` with `i32.load offset=idx*4`. `Unit_lit` → `i32.const 0`. Verified (wat2wasm + Node.js): `region R { let x = &R 5 in 42 }` → 42; `with c = mk 7 in c.id * 10` (close prints "closing") → 70; `view Cell[R] of int { v: int }; region R { let c = Cell { v = 7 } in c.v }` → 7. Added 6 tests (951 passing). </strong>Wasm backend covers all memory model features, on par with C / LLVM<strong>.</strong></p> <ul> <li><strong>Phase 6 #7: Wasm codegen first-class fn + closure</strong> —</li> </ul> <p> Wasm-specific constraint handling: function pointers are not memory ptr but <strong>function table indexes</strong>; indirect calls go through <code>call_indirect (type $sig)</code>. Declared <code>(type $cl (func (param i32) (param i32) (result i32)))</code> at module top; adapters registered in table starting from index 0 via <code>(table N funcref)</code> + <code>(elem (i32.const 0) ...)</code>. closure value is 8-byte memory struct <code>{ env_offset, fn_table_idx }</code>. Auto-generated env-ignoring adapter <code>(func $f_closure (param i32) (param i32) (result i32) local.get 1; call $f)</code> for each top-level fn <code>f</code> + table registration; recorded index in <code>fn_closure_table_idx</code>. At <code>Var name</code> value position, if <code>fn_closure_table_idx</code> is registered, memory-allocs closure value (<code>env=0, fn_idx=N</code>) and pushes. Indirect App: save closure to local → load env / arg / load fn_idx → <code>call_indirect (type $cl)</code>. Anonymous Fun: compute free variables via <code>free_vars</code> → capture only those registered in <code>locals</code> → register fresh adapter <code>anon_N_fn</code> in table → push to <code>pending_closures</code> queue → at construction site, memory-alloc env (store each capture in sequence), alloc closure value + push. Adapter body entry loads captures from env into local slots via <code>i32.load offset=N*4</code> before evaluating body. Drain loop in emit_program processes pending. Added <code>pattern_vars</code> + <code>free_vars</code> helpers. Verified (wat2wasm + Node.js): <code>let inc = fn x -> x + 1 in let apply = fn f -> f 5 in apply inc</code> → 6; <code>(make_adder 5) 10</code> → 15; <code>compose inc dbl 5</code> → 11; <code>twice inc 5</code> → 7. Added 7 tests (945 passing).</p> <ul> <li><strong>Phase 6 #6: Wasm codegen variant + match (monomorphic, single</strong></li> </ul> <p> payload type)<strong> — Variants also laid out in linear memory: 4 bytes (`{ i32 tag }`) if nullary-only; 8 bytes (`{ i32 tag, i32 payload }`) if payload. `variant_tags : (cname, int) Hashtbl` populated at start of emit_program from `Exhaustive.type_variants`; `variant_payload_ty` helper detects payload type (single type shared by all payload-bearing ctors; Codegen_error if differ). Compiled `Constr cname (arg)` to bump alloc + `i32.store offset=0` (tag) + (if needed) `i32.store offset=4` (payload) + push base. `Match` saves scrut to local, loads tag/payload via `i32.load offset=0/4`; each arm compiles to nested chain of `local.get tag; i32.const N; i32.eq; if (result i32) ... else ... end`; fallthrough traps with `unreachable`. Pattern subset: P_constr / P_var / P_wild; payload bind uses payload local slot. Verified (wat2wasm + Node.js): `type Color = R | G | B; match G with | R -> 0 | G -> 1 | B -> 2` → 1; `type Stat = Ok | Err of str; match Err "boom" with | Ok -> 0 | Err msg -> str_len msg` → 4; `let v = ISome 42 in match v with | INone -> 0 | ISome n -> n` → 42. Added 6 tests (938 passing). guard / polymorphic / recursive / nested pattern / or-pattern continue to be Codegen_error (future slices).</strong></p> <ul> <li><strong>Phase 6 #5: Wasm codegen record (monomorphic)</strong> — Same linear</li> </ul> <p> memory layout as tuple (Phase 6.4). Stores <code>Record_lit (name, fields)</code> in <code>Typer.records.r_fields</code> <strong>declaration order</strong> (reconstructed even if source field order differs): base = bump → immediately advance bump by 4<em>N (reserve) → write each field via `i32.store offset=i</em>4<code> → push base. </code>Field_get (inner, fname)<code> pulls index from record name of inner type → </code>i32.load offset=idx<em>4`. `Record_update (base, updates)` allocates new buffer with bump; for each field, writes new value if in updates, else copies from source via `i32.load offset=...`; returns base of new buffer. Functions that take / return record also work naturally (record is also passed as i32 offset; signature unchanged). Verified (wat2wasm + Node.js): `type Pt = { x: int, y: int }; let p = Pt { x = 3, y = 4 } in p.x + p.y` → 7; `{ p | x = 100 }.x </em> .y<code> → 400; record-returning fn </code>let mk = fn x -> Pair { a = x, b = str_len x } in print ((mk "hello").a)<code> → "hello". Polymorphic record / view continue to be Codegen_error (future slices). Added </code>wasm_with_decls<code> test helper. Added 4 tests (932 passing).</code></p> <ul> <li><strong>Phase 6 #4: Wasm codegen tuple</strong> — Tuple laid out in linear</li> </ul> <p> memory: each element 4 bytes (Mere int / bool / str all in i32 / offset representation). <code>Tuple [e1; e2; ...]</code> construction: base offset = bump; bump += 4<em>N immediately reserves memory area; write each element via `i32.store offset=N</em>4<code> at base-relative position; finally push base. Important to reserve first — nested tuple or </code>++<code> inner emit advances bump further (during implementation, fixed bug where </code>((1,2), 3)<code> summed to 22 because reserve was after writing). </code>fst<code> / </code>snd<code> builtin dispatched to </code>i32.load offset=0<code> / </code>offset=4<code>. Tuple-arg / tuple-return functions also work naturally (tuple is i32 offset, no signature change). Verified (wat2wasm + Node.js): </code>let p = (1, 2) in fst p + snd p<code> → 3; </code>let p = ("hello", 42) in print (fst p)<code> → "hello"; </code>((1, 2), 3)<code> sum → 6; tuple-arg fn </code>sum_pair (10, 20)<code> → 30. Added 5 tests (928 passing).</code></p> <ul> <li><strong>Phase 6 #3: Wasm codegen string support</strong> — Implemented</li> </ul> <p> architecture for handling strings via Wasm's linear memory. <code>(memory (export "memory") 1)</code> declares 1-page (64 KB) memory + exports; <code>(global $__lang_bump (mut i32) (i32.const N))</code> is bump pointer for dynamic alloc (mutable global). <code>Str_lit</code> lifted as <code>(data (i32.const offset) "...\00")</code> data segment; <code>wasm_string_escape</code> escapes <code>\HH</code>. <code>fresh_str_offset</code> helper assigns unique offset to each literal; accumulates in <code>str_data_decls</code> ref. <code>$__lang_strlen</code> (block + loop searches null byte) and <code>$__lang_str_concat</code> (2 strlen calls + 2 copy loops + null terminator + bump update) defined inline in WAT (emitted as runtime_helpers in one go). <code>print s</code> delegated to host (Node.js) via host import <code>(import "env" "puts" (func $puts (param i32)))</code>; value is i32 0; Node.js side accesses memory to decode + console.log. <code>str_len s</code> dispatched to <code>call $__lang_strlen</code>; <code>++</code> to <code>call $__lang_str_concat</code>. Functions taking / returning str also work naturally (Wasm also treats str as i32, so signature unchanged). Verified (wat2wasm + Node.js with puts that decodes memory): <code>str_len "Hello, world!"</code> → 13; <code>str_len ("hello, " ++ "world!")</code> → 13; <code>print "Hello, Wasm!"</code> → "Hello, Wasm!"; <code>let greet = fn name -> "Hello, " ++ name ++ "!" in print (greet "world")</code> → "Hello, world!". Added 9 tests (923 passing).</p> <ul> <li><strong>Phase 6 #2: Wasm codegen function lifting + recursion</strong> — Top-</li> </ul> <p> level <code>let f = fn x -> ...</code> and <code>let rec</code> lifted as <code>(func $f (param i32) (result i32) ...)</code>. <code>fn_skel</code> / <code>lift_fn_skels</code> / <code>find_concrete_arrow</code> / <code>resolve_fn_types</code> implemented in <code>codegen_wasm.ml</code> in parallel, same shape as LLVM Phase 5.2. <code>emit_fn_def</code> puts each fn in independent locals/instrs scope: param in slot 0 (Wasm positional locals); let bindings minted as slot 1, 2, ...; <code>local_counter</code> / <code>locals</code> / <code>instrs</code> saved / restored per-fn. Compiled <code>App (Var name, arg)</code> to <code><arg push></code> + <code>call $name</code> (only names registered in <code>toplevel_fn_names</code> get direct call). Wasm allows forward reference in same module, so C/LLVM-style forward declaration / mutual recursion special handling not needed. Verified (wat2wasm + Node.js): <code>factorial 10</code> → 3628800; <code>fibonacci 15</code> → 610; <code>is_even 7</code> (mutual recursion) →</p> <ol> <ol> <li>Added 5 tests (914 passing).</li> </ol> </ol> <ul> <li><strong>Phase 6 #1: Wasm (WAT) codegen MVP</strong> — Started on the third</li> </ul> <p> design target (Wasm). Implemented <code>emit_program : ?main_ty:ty -> Ast.program -> string</code> in new <code>lib/codegen_wasm.ml</code>; emits subset (int / bool / arith / cmp / logic / Neg / If / Let (P_var) / Var / Annot) as WAT (WebAssembly Text format, S-expression form). Wasm is a stack-based VM (different from LLVM's SSA) — each expression pushes operands in sequence, opcode consumes from stack + pushes result. Compiled <code>Bin (op, a, b)</code> to sequential <code>emit_expr a; emit_expr b; <opcode></code>. <code>If</code> to <code>if (result i32) ... else ... end</code> block. <code>Let (P_var n, value, body)</code> to combination of <code>(local i32)</code> (fresh slot assignment) + <code>local.set N</code> + <code>local.get N</code>. Comparison via <code>i32.lt_s</code> / <code>i32.gt_s</code> / <code>i32.eq</code> etc.; bool widened to i32 (<code>i32.const 0/1</code>); <code>Neg</code> expressed as <code>0 - x</code>. <code>main</code> function emitted as <code>(func $main (export "main") (result i32))</code>; local decls consolidated at function head. Added <code>-w <file></code> / <code>-we <expr></code> flags to CLI; <code>infer_program</code> helper shared across 3 backends (C/LLVM/Wasm). Verified (via wat2wasm <code>.wasm</code> binary + Node.js <code>WebAssembly.instantiate</code>): <code>let a = 10 in let b = 20 in if a + b > 25 then a * b else 0</code> → 200; <code>if 3 > 2 then 100 else 200</code> → 100; <code>let x = 5 in x * x + 1</code> → 26; <code>true && (false || true)</code> → 1. Added 14 tests (909 passing). Functions / strings / record / variant / closure / region etc. in subsequent slices of Phase 6.</p> <ul> <li><strong>Phase 5 #14: LLVM IR codegen `'a list` show special-case</strong></li> </ul> <p> (<code>[a, b, c]</code> form)<strong> — Equivalent to C codegen Phase 4.16. Special-cases `TyCon ("list", [elem_ty])` (when recursive list) before variant branch in `emit_show_fn`: scans from head with alloca/load/store + loop blocks (`loop_test` / `loop_body` / `loop_iter` / `loop_end`); stringifies each element via `show_<elem_tag>`; concats with `", "` between via `__lang_str_concat`; appends `"]"` at end. Pre-registers `@.s_lbracket` ("["), `@.s_rbracket` ("]"), `@.s_comma_space` (", "). Side: (1) `add_show_type` registers in `mono_variant_instances` / `mono_record_instances` when encountering polymorphic TyCon (struct typedef emitted for cases like `show (Nil : int list)` where mono instance can't be collected via Constr); (2) `collect_tuple_shapes` end walks substituted payload of mono variant instances (emits tuple shape of Cons payload `(int, int list)` of `int list` even without Cons in AST); (3) moved `collect_show_types` before typedef emission (so instance flow propagates correctly). Verified (clang native): `show [1, 2, 3]` → `[1, 2, 3]`; `show (Nil : int list)` → `[]`; `show ["hello", "world"]` → `["hello", "world"]`. Added 4 tests (895 passing). </strong>Phase 5 (LLVM backend) covers all C codegen (Phase 4) features<strong> — int / fn / str / tuple / record / variant / closure / region / poly / recursive variant / complex pattern / show / all memory model / list pretty-print.</strong></p> <ul> <li><strong>Phase 5 #13: LLVM IR codegen Region_block + Ref + with Drop +</strong></li> </ul> <p> view construction + Unit_lit<strong> — Implemented all Mere memory-model features in LLVM backend in one slice; equivalent to C codegen Phase 4.17 user-side region + 4.18 with Drop + 4.19 view construction. `current_regions : (name * register) list ref` tracks region scope. Compiled `Region_block (R, body)` to `alloca %__lang_region` + `__lang_region_init(ptr, 1MB)` + body + `__lang_region_free`. Compiled `Ref (R, v)` (`&R v`) to inner evaluation + sizeof (`getelementptr null` + `ptrtoint`) + `__lang_region_alloc` + `store` to write to region buffer; ptr return. `With (c, v, body)`: `let c = v` + body evaluation; after body, if v's record has `close: unit -> unit` field, auto-invokes via `c.close.fn(c.close.env, 0)` (`extractvalue` separates closure value → env/fn + call). At `Record_lit`, if `name in Typer.views`, view construction: get region name from `e.Ast.ty`'s `TyCon (V, [TyRef (R, ...)])` → build record value with `insertvalue` in declaration order → place in region with `__lang_region_alloc` + `store` → ptr return. At `Field_get`, if inner type is `is_view_type`, `getelementptr %V, ptr %x, i32 0, i32 idx` + `load` to get field. Added `TyRef _ → ptr` and `TyCon (n, _) when Typer.views n → ptr` to `llvm_ty_of`. `Unit_lit` emitted as `i32 0` (needed for `fn () -> ()`). Verified (clang native): `region R { let x = &R 5 in 42 }` → 42; `region R { let pair = &R (1, 2) in 99 }` → 99; `type Pt = { x: int }; region R { let p = &R Pt { x = 42 } in 100 }` → 100 (record also placeable in region); `drop type Conn = { id, close }; with c = mk 7 in c.id * 10` → "close 7\n70" (close called correctly at scope end); `view Cell[R] of int { v: int }; region R { let c = Cell { v = 7 } in c.v }` → 7. Added 7 tests (891 passing). </strong>LLVM backend covers all memory- model features, on par with C backend (Phase 4.21)<strong>.</strong></p> <h2 id="2026-06-17">2026-06-17</h2> <ul> <li><strong>Phase 5 #12: LLVM IR codegen show general builtin</strong> — LLVM</li> </ul> <p> version of C codegen Phase 4.12. Specializes <code>show : 'a -> str</code> per-call from arg type's <code>show_<ty_tag></code>; generates dedicated function for each type. Added <code>@asprintf(ptr, ptr, ...)</code> to runtime_decls. <code>show_types</code> Hashtbl + <code>collect_show_types</code> walks AST to find <code>App (Var "show", arg)</code>; <code>add_show_type</code> recursively registers arg type + dependent types (tuple elem / record field / variant payload), with Hashtbl guard so recursive variant <code>'a list</code> etc. doesn't infinite-loop. <code>emit_show_fn</code> emits specialized fn per type: int → <code>@asprintf("%d", x)</code>; bool → <code>select i1</code> for <code>@.s_true</code> / <code>@.s_false</code>; str → <code>@asprintf("\"%s\"", x)</code>; unit → const <code>@.s_unit</code>; tuple → call each element <code>show_T</code> → <code>@asprintf("(%s, ..., %s)", ...)</code>; record (mono / poly) → each field show + <code>@asprintf("Type { f = %s, ... }", ...)</code>; variant (mono / poly / recursive) → tag dispatch (icmp eq + br + phi) → each ctor: <code>@.s_ctor_<name></code> direct if nullary; recursive payload show + <code>@asprintf("Ctor %s", ...)</code> if payload. Format strings and ctor name strings pre-registered at start of emit_program for what is needed (<code>mint_show_global</code> / <code>mint_show_format</code> helpers). <code>App (Var "show", arg)</code> dispatched to <code>call ptr @show_<ty_tag arg.ty>(arg)</code>. Verified (clang native): <code>show 42</code> → "42"; <code>show "hi"</code> → "\"hi\""; <code>show true</code> → "true"; <code>show (1, "hi")</code> → <code>(1, "hi")</code>; <code>show (SS 42)</code> → "SS 42"; <code>show (Pt { x = 3, y = 4 })</code> → <code>Pt { x = 3, y = 4 }</code>; <code>show (Cons (1, Cons (2, Cons (3, Nil))))</code> → <code>Cons (1, Cons (2, Cons (3, Nil)))</code>. Added 9 tests (884 passing). <code>'a list</code> special-case <code>[1, 2, 3]</code> form (equivalent to Phase 4.16) in future slice.</p> <ul> <li><strong>Phase 5 #11: LLVM IR codegen complex patterns (P_int / P_str /</strong></li> </ul> <p> P_bool / P_unit / P_record / P_as / nested / or / guard)<strong> — LLVM version of C codegen Phase 4.14 + 4.15. Rewrote `compile_pat` as fully recursive `(test_cond, bindings, var_types)` function: P_int → `icmp eq i32`; P_bool → `icmp eq i1`; P_str → `@strcmp(ptr, ptr)` + `icmp eq i32 result, 0`; P_unit → constant `1`; P_record → declared field order `extractvalue` + sub-pattern recurse; P_as → inner pattern + whole value bind; P_tuple → each element `extractvalue` + recurse; P_constr → tag test + sub-pattern recurse (payload via GEP+load if recursive variant, else extractvalue). Multiple sub-tests chained via `and_cond` helper with `and i1`. Or-patterns pre-flattened with `expand_or` (typer guarantees both branches' bound names match, body duplicable). Guard evaluated in arm's bindings scope; if true → body, if false → next_label (= try next arm). Added `@strcmp` to runtime_decls. Verified (clang native): `match 3 with | 0 -> 100 | 1 -> 200 | _ -> 300` → 300; `match "hello"` str match → 2; `match Cons (SS 5, Nil)` nested ctor → 5; `match Pt { x=3, y=4 } with | Pt { x=a, y=b }` → 7; `(a, b) as p` → 6 (`P_as`); `match LCgB with | LCgA | LCgB -> 1 | LCgC -> 2` → 1 (or); `match 7 with | n when n < 5 -> 100 | n when n < 10 -> 200 | _ -> 300` → 200 (guard). Added 8 tests (875 passing).</strong></p> <ul> <li><strong>Phase 5 #10: LLVM IR codegen recursive variant + P_tuple sub-</strong></li> </ul> <p> pattern<strong> — Switched variants with self-referential payload (`type ilist = INil | ICons of int * ilist`, `'a list = Nil | Cons of 'a * 'a list`) to heap-allocated node + ptr representation. `recursive_variants` set + `variant_is_recursive` / `mono_variant_is_recursive` helpers for judgment. Populated in 2 stages within emit_program: at decl registration (source-level) + at mono instance collection (substituted). `emit_variant_typedef` / `emit_mono_variant_typedef` emit `%V_node = type { i32, T }` (on-heap node) if recursive; `llvm_ty_of` returns `ptr` if name in recursive_variants, so value type is transparent ptr. `Constr` recurse: `__lang_region_alloc` allocates node in default region; `getelementptr` + `store i32 tag` + `getelementptr` + `store T payload` write; ptr return. `Match` recurse: get tag from scrutinee ptr via `getelementptr` + `load i32`; payload of each arm similarly via `load`. In pattern compile, expand `P_tuple` sub-pattern (`Cons (h, t)`) into chain of `extractvalue` of payload tuple struct; bind each element to fresh register. `pattern_var_types` helper adds concrete types of pattern bind names to current_var_types (so polymorphic recursive calls don't leave `'a list` as-is). Match scrutinee type fallback to current_var_types if Var; same for direct-call App arg type. Reordered typedef emission to `collect_mono_instances` + recursive judgment → tuple/record/variant typedef emit (so recursive_variants state affects tuple emit). Verified (clang native): `type ilist = INil | ICons of int * ilist; sum (ICons (1, ICons (2, ICons (3, INil))))` → 6; `type 'a list = Nil | Cons of 'a * 'a list; sum [1,2,3,4,5]` → 15; `length ["a","b","c","d"]` → 4 (poly recursive list). Added 5 tests (867 passing).</strong></p> <ul> <li><strong>Phase 5 #9: LLVM IR codegen monomorphization of polymorphic</strong></li> </ul> <p> variant / record<strong> — C codegen Phase 4.11 + 4.13 implemented on LLVM side in one slice. `polymorphic_variants` / `polymorphic_records` Hashtbl defer declarations (walk `Exhaustive.type_variants` + `Typer.records` at start of emit_program; register only poly ones); recover poly variant param names via constructor's `params`. `mono_variant_instances` / `mono_record_instances` accumulate found instances; `collect_mono_instances` walks AST + fn signature to find `(name, args)`. `subst_params` / `subst_variants` substitute type vars → concrete types; `mono_variant_name n args` / `mono_record_name n args` produce specialized names (`opt_int`, `Box_str` etc.). `emit_mono_variant_typedef` determines payload type from substituted payload type via `variant_payload_ty_of`; emits `%opt_int = type { i32, T }`. `emit_mono_record_typedef` emits `%Box_int = type { ... }` with substituted field types. `llvm_ty_of (TyCon (n, args))` maps to mono name if name in `polymorphic_variants/records`. `Constr` emit: pull mono name from `e.ty`; determine payload type with `variant_payload_ty_of`. `Record_lit` / `Field_get` / `Record_update` similarly use `mono_record_name` + substituted fields for poly records. `Match` scrutinee type, if poly, uses mono name + substituted variants. Verified (clang native): `type 'a LCgOpt = LCgN | LCgS of 'a; match LCgS 42 with | LCgN -> 0 | LCgS n -> n` → 42; `type 'a Box = { v: 'a }; let b = Box { v = 42 } in b.v` → 42; specialize both types `let bi = Box { v = 42 } in let bs = Box { v = "hi" } in str_len bs.v + bi.v` → 44 (both `%Box_int` and `%Box_str` emitted). Added 7 tests (862 passing). Recursive poly variant (`'a list`) requires recursive variant support → Phase 5.10.</strong></p> <ul> <li><strong>Phase 5 #8: LLVM IR codegen default region runtime + closure/</strong></li> </ul> <p> string alloc via region<strong> — Implemented work equivalent to C codegen Phase 4.17 + 4.20 + 4.21 on LLVM side in one slice. `%__lang_region = type { ptr, ptr, i64 }` struct + `@__lang_default_region = internal global %__lang_region zeroinitializer` file-scope global + 3 helper functions `__lang_region_init/alloc/free` defined inline in LLVM IR (`region_runtime_helpers`). `__lang_region_alloc` uses 8-byte aligned bump pointer (`(n + 7) & -8` implemented with `and i64 ..., -8`; advances top via gep i8, store). Calls `__lang_region_init(@__lang_default_region, 4194304)` (4 MB) at `@main` entry; calls `__lang_region_free` before final `ret i32 0`. Replaced `malloc` in `__lang_str_concat` with `__lang_region_alloc(@__lang_default_region, ...)`; closure env (anonymous Fun) `malloc(sizeof)` similarly replaced. Added `@free` to runtime_decls; inserted region_runtime_helpers in emit order right before str_concat_helper. Verified (clang native): `(make_adder 5) 10` → 15; `compose inc dbl 5` → 11; concat like `"hello, " ++ "world"`; only `malloc` call in generated IR is one spot inside region init (one-shot free at program end, valgrind clean). Added 8 tests (855 passing). LLVM backend memory model reached the same level as C backend (Phase 4.21).</strong></p> <ul> <li><strong>Phase 5 #7 Phase B: LLVM IR codegen anonymous Fun + closure-</strong></li> </ul> <p> with-captures<strong> — Handles internal `fn x -> ...` in expression position. Computes free variables of AST with `free_vars` / `pattern_vars` helpers (excluding bound names, preserving order); filters to those registered in `current_var_types` (excluding top- level / builtin). For each capture, gets concrete type from `current_var_types`; generates `%anon_N_env = type { T1, T2, ... }` env struct typedef; pushes `anon_N_fn` adapter to `pending_closures` queue. At construction site: allocates env with `malloc(sizeof(%anon_N_env))` (LLVM `getelementptr null` trick + `ptrtoint` calculates size); writes each capture to env field via `getelementptr %env, ptr %p, i32 0, i32 idx` + `store`; assembles closure value with `insertvalue %closure undef, ptr %env, 0` + `insertvalue ..., ptr @anon_N_fn, 1`. `emit_anon_adapter` invoked by `emit_program` draining `pending_closures` (iterative loop also processes new pending added during drain); in adapter body entry block, pulls each capture from env_self with `getelementptr` + `load` into fresh register, binds to env, then emits original Fun body. Added `current_expected_ty : ty option ref`: lets parent context type serve as fallback when AST's Fun.ty is polymorphic (resolves cases where inner Fun's type stays `'a -> 'a` in let-poly curried polymorphic HOFs like `fn f -> fn x -> f (f x)`; emit_fn_def / emit_anon_adapter set return_ty at body start / restore at end). Extended Let case to add value type to current_var_types (so closure can capture variables of outer let). Verified (clang native): `let make_adder = fn n -> fn x -> x + n in (make_adder 5) 10` → 15 (capture); `let twice = fn f -> fn x -> f (f x) in twice inc 5` → 7 (curried HOF + polymorphic); `let apply = fn f -> fn x -> f x in apply (fn n -> n * 3) 7` → 21 (anon Fun passed as arg); `let compose = fn f -> fn g -> fn x -> f (g x) in ((compose inc) dbl) 5` → 11 (3-level nested closure + 2 captures). Added 7 tests (847 passing). env currently leaks via `@malloc` — default region-ization in future slice.</strong></p> <ul> <li><strong>Phase 5 #7 Phase A: LLVM IR codegen first-class top-level fn</strong> —</li> </ul> <p> Lowers <code>T1 -> T2</code> type as <code>%closure_T1_T2 = type { ptr, ptr }</code> (env, fn pointer). <code>closure_struct_name</code> helper for closure type name; <code>collect_arrow_types</code> walks AST + fn signatures to gather all used arrow types; <code>emit_closure_typedef</code> generates typedef. Auto-generates env-ignoring adapter <code>define T2 @<name>_closure_fn (ptr %env_unused, T1 %x) { ret T2 @<name>(T1 %x); }</code> for each top-level fn (<code>emit_closure_adapter</code>). At <code>emit_expr</code> <code>Var name</code>: if no shadowing in env and registered in <code>toplevel_fn_names</code>, inline-constructs closure value with <code>insertvalue %closure undef, ptr null, 0</code> + <code>insertvalue ..., ptr @<name>_closure_fn, 1</code>. <code>App f arg</code>: existing direct-call path preserved (for known top-level fn); otherwise dispatches via <code>extractvalue %closure %c, 0/1</code> to get env/fn, then <code>call T2 %fn_ptr(ptr %env, T1 %arg)</code> (no fn pointer type cast needed via opaque pointer). Added <code>current_var_types : (string * ty) list ref</code>: for polymorphic Var in fn body (parameter staying as <code>'a -> int</code> after let-poly), can pull concrete type from resolve_fn_types-derived (sets param's concrete ty at start of <code>emit_fn_def</code>, save/restore). Verified (clang native): <code>let inc = fn x -> x + 1 in let apply = fn f -> f 5 in apply inc</code> → 6; <code>let apply2 = fn f -> f (f 5) in apply2 inc</code> → 7. Added 7 tests (840 passing). Anonymous Fun (inner <code>fn x -> ...</code>) and closure-with-captures (Phase B) in separate slice.</p> <ul> <li><strong>Phase 5 #6: LLVM IR codegen variant + match (monomorphic, single</strong></li> </ul> <p> payload type)<strong> — Lowers monomorphic variant to LLVM named struct: if all ctors nullary, `%V = type { i32 }`; if payload exists, `%V = type { i32, T }` (`variant_payload_ty` detects single payload type shared by all payload-bearing ctors; Codegen_error if differ). `variant_tags` Hashtbl holds constructor → int tag; set as side effect of `emit_variant_typedef`. `collect_variant_names` walks AST + fn signature + Constr's type_name to gather used variant types (only `Typer.types` arity 0 ones). `Constr cname arg_opt` → `%t0 = insertvalue %V undef, i32 tag, 0` → optional `%t1 = insertvalue %V %t0, T arg, 1` chain constructs SSA struct value. `Match` gets scrutinee's tag with `extractvalue %V %s, 0`; tests each arm sequentially with `icmp eq i32 %tag, N` + `br i1`; fallthrough is `@abort()` + `unreachable`; merges all arm results with `phi <result_ty>` at end. Pattern is P_constr / P_var / P_wild only; payload bind creates payload register with `extractvalue %V %s, 1` and adds to bindings. Added @abort declaration to runtime_decls. Verified (clang native): `type Color = R | G | B; match G with | R -> 0 | G -> 1 | B -> 2` → 1; `type Status = Ok | Err of str; match Err "boom" with | Ok -> 0 | Err m -> str_len m` → 4; `type IntOpt = INone | ISome of int; let v = ISome 42 in match v with | INone -> 0 | ISome n -> n` → 42. Added 9 tests (833 passing). Guard / polymorphic variant / recursive variant / nested pattern / or-pattern continue to be Codegen_error.</strong></p> <ul> <li><strong>Phase 5 #5: LLVM IR codegen record (monomorphic)</strong> — Lowers</li> </ul> <p> monomorphic record (<code>type Pt = { x: int, y: int }</code>) to LLVM named struct (<code>%Pt = type { i32, i32 }</code>). Added <code>TyCon (name, []) when Hashtbl.mem Typer.records name -> "%" ^ name</code> to <code>llvm_ty_of</code>; <code>record_fields</code> / <code>field_index</code> helpers pull declaration-order fields from <code>Typer.records</code>. <code>Record_lit</code> emit constructed with <code>insertvalue</code> chain in declaration order (even if source field order differs from declared, pulls values with <code>List.assoc_opt</code> and stacks in declaration order). <code>Field_get</code> is <code>extractvalue %R %p, idx</code>; <code>Record_update</code> starts from base value and stacks each update field via <code>insertvalue</code>. <code>collect_record_names</code> walks AST + fn signature to gather all used record types (polymorphic records excluded for now, separate slice). <code>emit_record_typedef</code> generates <code>%Name = type { T1, T2, ... }</code>. Via <code>bin/main.ml</code> <code>infer_program</code> helper, so Typer.records is already populated. Added <code>llvm_with_decls</code> test helper (parallel to <code>codegen_with_decls</code>). Verified (clang native): <code>type Pt = { x: int, y: int }; let p = Pt { x = 3, y = 4 } in p.x + p.y</code> → 7; Record_update <code>{ p | x = 100 }</code> x <em> y → 400; record-returning fn `let mk = fn x -> Pair { a = x, b = str_len x } in print ((mk "hello").a)` → "hello". Added 6 tests (824 passing). Polymorphic record (`type 'a Box`) stays Codegen_error.</em></p> <ul> <li><strong>Phase 5 #4: LLVM IR codegen tuple</strong> — Lowers tuple to LLVM named</li> </ul> <p> struct (<code>%tuple_int_str = type { i32, ptr }</code>). <code>ty_tag</code> / <code>tuple_struct_name</code> helpers (same naming convention as codegen_c generates symbols like <code>tuple_int_str</code>); <code>collect_tuple_shapes</code> walks AST + fn signature to gather all used tuple types; <code>emit_tuple_typedef</code> generates <code>%name = type { T1, T2, ... }</code>. <code>Tuple</code> node emit constructs struct value in SSA register with <code>insertvalue</code> chain (starts from <code>undef</code>, stacks each element via <code>insertvalue %T %prev, Tn vn, idx</code>). <code>fst</code> / <code>snd</code> builtin compiled to <code>extractvalue %tuple_X %p, 0/1</code> (struct name resolved from arg's <code>.ty</code>). <code>llvm_ty_of (TyTuple ts)</code> returns <code>%<tuple_struct_name></code>, so tuple-arg / tuple-return function signatures automatically take correct form (<code>define %tuple_int_int @split(ptr %s)</code>, <code>define i32 @sum_pair(%tuple_int_int %p)</code>). Nested tuple (<code>((1, 2), 3)</code> → <code>%tuple_tuple_int_int_int = type { %tuple_int_int, i32 }</code>) auto-generated. Verified (clang native): <code>let p = (1, 2) in fst p + snd p</code> → 3; <code>let p = ("hello", 42) in print (fst p)</code> → "hello"; <code>let split = fn s -> (s, str_len s) in print (fst (split "hello"))</code> → "hello"; nested tuple <code>((1,2), 3)</code> sum → 6; tuple-arg fn <code>sum_pair (10, 20)</code> → 30. Added 8 tests (818 passing).</p> <ul> <li><strong>Phase 5 #3: LLVM IR codegen strings + print + ++ + str_len +</strong></li> </ul> <p> str-taking/returning functions<strong> — Maps `TyStr` to LLVM `ptr` (opaque pointer). Lifts `Str_lit s` as private constant global `@.str_N = private constant [N x i8] c"...\00"`; generated via `fresh_str_global` helper; escapes non-printable ASCII with `\HH`. Uses global symbol directly as ptr for value (no GEP needed with opaque pointer). Compiles `Bin (Concat, a, b)` to `call ptr @__lang_str_concat(ptr %a, ptr %b)`; `__lang_str_concat` defined inline in LLVM IR (combination of `malloc` + `strlen` + `memcpy` + GEP + `store i8 0`). Compiles `print` builtin to `call i32 @puts(ptr %s)` (discards return value; Mere value is 0); `str_len` to `call i64 @strlen(ptr %s)` + `trunc i64 ... to i32`. Added `TyStr → ("ptr", "%s")` to `main_format_of`; generates `@.fmt_s = c"%s\\0A\\00"` global. str-taking/returning functions auto-lowered correctly (`define ptr @f(ptr %s)`). Runtime helpers (`declare ptr @malloc(i64)` etc.) and `__lang_str_concat` body emitted in emit_program in one go; `.ll` file is self-contained. Verified (clang native): `print "Hello, LLVM!"` → "Hello, LLVM!"; `"hello, " ++ "world!"` → "hello, world!"; `str_len "Hello, world!"` → 13; `let greet = fn name -> "Hello, " ++ name ++ "!" in print (greet "world")` → "Hello, world!"; `let exclaim = fn s -> s ++ "!" in print (exclaim "wow")` → "wow!"; `let pick = fn n -> if n > 0 then "positive" else "negative" in print (pick 5)` → "positive". Added 10 tests (810 passing).</strong></p> <ul> <li><strong>Phase 5 #2: LLVM IR codegen function lifting + recursion</strong> —</li> </ul> <p> Lifts top-level <code>let f = fn x -> ...</code> and <code>let rec f = ... and g = ...</code> as LLVM <code>define iXX @f(iYY %x) { ... }</code>. Implemented <code>fn_skel</code> / <code>lift_fn_skels</code> / <code>find_concrete_arrow</code> / <code>resolve_fn_types</code> in <code>codegen_llvm.ml</code> in parallel, same shape as C codegen (combined with LLVM-specific <code>llvm_ty_of</code>). <code>emit_fn_def</code> emits each function as independent SSA scope (<code>reg_counter</code> / <code>label_counter</code> reset per-function; <code>instrs</code> save/restore). At <code>App (Var name, arg)</code>, if <code>name</code> is registered in <code>toplevel_fn_names</code>, compiled to <code>%t = call iZZ @name(iYY %arg)</code> (closure-as-value in future slice). LLVM IR allows forward reference within same module, so forward declaration needed in Phase 4 is unnecessary (mutual recursion works as-is). Verified (clang native): <code>factorial 10</code> → 3628800; <code>fib 15</code> → 610; <code>is_even 7</code> (mutual recursion) → 0. Added 6 tests (800 passing).</p> <ul> <li><strong>Phase 5 #1: LLVM IR codegen MVP</strong> — Started second backend that</li> </ul> <p> compiles Mere to native binary. Implemented <code>emit_program : ?main_ty:ty -> Ast.program -> string</code> in new <code>lib/codegen_llvm.ml</code>; converts subset (int / bool / arith / cmp / logic / Neg / If / Let (P_var) / Var / Annot) to LLVM textual IR. Hand-written text generation (no dependency on opam's <code>llvm</code> package; directly compile with <code>clang out.ll</code>). Name management via SSA register counter (<code>%t0</code>, <code>%t1</code> ...) and basic block label counter; If goes through <code>br i1</code> + label/phi; comparison via <code>icmp slt/sgt/eq/...</code>; bool computed in <code>i1</code> and zext-extended to <code>i32</code> at main end for output via <code>@printf</code> (<code>@.fmt_d = c"%d\\0A\\00"</code>). Added <code>-ll <file></code> / <code>-lle <expr></code> flags to CLI; shared infer_program helper for both C / LLVM backends. Verified (clang native execution): <code>let a = 10 in let b = 20 in if a + b > 25 then a * b else 0</code> → 200; <code>if 3 > 2 then 100 else 200</code> → 100; <code>let x = 5 in x * x + 1</code> → 26; <code>true && (false || true)</code> → 1. Added 15 tests (794 passing). Functions / strings / record / variant / closure / region etc. now Codegen_error (same scope as Phase 4 MVP).</p> <ul> <li><strong>Phase 4 #21: strings + recursive variant nodes also moved to</strong></li> </ul> <p> default region<strong> — Unifies remaining 2 malloc sites under `__lang_default_region`. Replaced `malloc(la + lb + 1)` in `__lang_str_concat` runtime helper with `__lang_region_alloc (&__lang_default_region, la + lb + 1)`; replaced `malloc(sizeof(T_node))` in recursive variant Constr emit (self-referential variant like `Cons (h, t)`) with `__lang_region_alloc(&__lang_default_region, sizeof(T_node))`. Reordered helper ordering in `emit_program` to `region_runtime_helpers → str_concat_helper` so str_concat helper can reference `__lang_default_region` symbol (ordering issue). Now the only remaining malloc on C side is base buffer allocation inside `__lang_region_init`; all user-visible alloc sites ride on bump arena. Batch free with `__lang_region_free(&__lang_default_region)` at `main` end; valgrind clean. Verified (clang native): `let greet = fn name -> "Hello, " ++ name ++ "!" in print (greet "world")` → "Hello, world!"; `sum [1, 2, 3, 4, 5]` → 15 (Cons of recursive list all in region alloc). Added 2 tests + updated 1 (779 passing; renamed "Constr mallocs node" to "Constr uses default region").</strong></p> <ul> <li><strong>Phase 4 #20: closure env moved to default region</strong> — Added</li> </ul> <p> program-lifetime arena <code>__lang_default_region</code> at file scope (<code>static __lang_region __lang_default_region;</code>); calls <code>__lang_region_init(&__lang_default_region, 1 << 22)</code> (4MB) at start of <code>main</code>, <code>__lang_region_free</code> at end. Switched anonymous closure env struct alloc from <code>malloc(sizeof(...))</code> to <code>__lang_region_alloc(&__lang_default_region, sizeof(...))</code>. Closures can outlive user's <code>region R { ... }</code> (carried out like <code>make_adder 3 |> add3 4</code>), so don't coexist with user region; needed to be in separate program-lifetime arena. Per-closure malloc cost gone; batch-freed at <code>main</code> end; valgrind also clean. Verified (clang native): <code>let make_adder = fn n -> fn x -> n + x in let add3 = make_adder 3 in add3 4</code> → 7; <code>let compose = fn f -> fn g -> fn x -> f (g x) in compose (fn n -> n + 1) (fn n -> n * 2) 5</code> → 11 (nested closure with captures all in default region). Remaining leaks: string concat (<code>++</code>) and recursive variant node (<code>Cons</code>). Added 5 tests + <code>assert_no_contains</code> helper (777 passing).</p> <ul> <li><strong>Phase 4 #19: region-izing view construction</strong> — Codegen places</li> </ul> <p> <code>view V[R] of T { ... }</code> on region's bump allocator. View value represented in C as <code>V*</code> (pointer type); at construction, allocates in region via <code>__lang_region_alloc(&__region_R, sizeof(V))</code>, copies content, returns pointer. <code>c_type_of (TyCon (V, [TyRef R TyUnit])) -> V*</code>; <code>is_view_type</code> helper distinguishes record / view; <code>Field_get</code> uses <code>-></code> for view value. View value's lifetime matches region scope (combined with Phase 2.1 escape check + Phase 4.17 region runtime) — <strong>memory model's view feature works fully at runtime level</strong>. Verified (clang native): <code>view Cell[R] of int { v: int }; region R { let c = Cell { v = 7 } in c.v }</code> → 7. Added 3 tests (772 passing; added Top_view handling to codegen_with_decls helper).</p> <ul> <li><strong>Phase 4 #18: `with` Drop execution codegen + typedef ordering</strong></li> </ul> <p> cleanup<strong> — C codegen for `with c = v in body`: at scope end, auto-calls c's `close` field via `c.close.fn(c.close.env, 0)` (only when `close: unit -> unit` field exists in Drop type; skip if absent. Multiple `with` are nested in AST, so naturally LIFO). Side: reorganized typedef structure to "all forward decls → closure typedefs → all struct bodies". Logic: for cases where record has `closure_T1_T2` type like `close: unit -> unit` field of Drop type, closure typedef needs record's full definition as function-pointer return; but C can use forward-declared struct as function pointer return type, so closure typedef can be emitted if forward decls come first. Split all variant / record / tuple typedefs into 2 stages of forward decl + body; reorder them in emit_program. Verified (clang native): `drop type Conn = { id: int, close: unit -> unit }; let mk = fn id -> Conn { id = id, close = fn () -> print ("close " ++ show id) } in with c = mk 7 in c.id * 10` → "close 7\n70" (close called correctly at scope end). Added 3 tests + updated 6 typedef snapshots to new format (769 passing).</strong></p> <ul> <li><strong>Phase 4 #17: region runtime (bump allocator)</strong> — Codegen</li> </ul> <p> <code>region R { body }</code> as a real bump allocator. Added new C runtime helper <code>__lang_region</code> (<code>{ char* base; char* top; size_t cap; }</code>) + <code>__lang_region_init/alloc/free</code> injected into generated source. <code>emit_expr Region_block</code> outputs statement expression <code>({ __lang_region __region_R; __lang_region_init (&__region_R, 1<<20); __auto_type __r_result = body; __lang_region_free(&__region_R); __r_result; })</code>. <code>emit_expr Ref (R, v)</code> emits <code>({ __auto_type __ref_v = v; typeof(__ref_v)* __p = __lang_region_alloc(&__region_R, sizeof __ref_v); *__p = __ref_v; __p; })</code> (bump alloc + copy + return pointer in region). <code>c_type_of (TyRef _ inner)</code> to <code>inner*</code>. Combined with escape check (typer), memory is batch-freed on region scope exit, but type signature guarantees <code>&R T</code> doesn't leak (Phase 2.1 escape check) for safety. <strong>Milestone where memory model went from "type level label" to "real bump allocator"</strong>. Verified (clang native): <code>region R { let x = &R 5 in 42 }</code> → 42; <code>region R { let pair = &R (1, 2) in 99 }</code> → 99; <code>type Pt = { x: int }; region R { let p = &R Pt { x = 42 } in 100 }</code> → 100 (record also placeable in region). Added 5 tests (766 passing).</p> <ul> <li><strong>Phase 4 #16: `'a list` show in `[a, b, c]` form + variant</strong></li> </ul> <p> payload tuple shape collection<strong> — Special-cases `TyCon ("list", [elem_ty])` in `emit_show_fn`; generates specialized function that strings the whole list with a while loop (`"[]"` if Nil; `[1, 2, 3]` format if Cons; matches Mere interpreter output). Side: extended tuple shape collection to include mono variant payload (`tuple_int_list_int` etc. referenced even in cases like `show ([] : int list)` that doesn't include Cons construction; fixed build failure where necessary struct typedef wasn't emitted). Verified (clang native): `show [1, 2, 3]` → `[1, 2, 3]`; `show ["hello", "world"]` → `["hello", "world"]`; `show ([] : int list)` → `[]`. Added 2 tests (761 passing).</strong></p> <ul> <li><strong>Phase 4 #15: C codegen or-pattern + match guard</strong> — Flattens</li> </ul> <p> <code>| pat1 | pat2 -> body</code> into multiple arms via pre-pass <code>expand_or</code> of Match emit (constraint that both branches bind same name set guaranteed by typer). Body is duplicated to both but safe as pure expression. <code>when ...</code> guard evaluated in arm's bindings scope; falls through if false (<code>test ? ({ bindings; guard ? body : next; }) : next</code>). Verified (clang native): <code>type Col = R | G | B; match G with | R | G -> 1 | B -> 2</code> → 1; <code>match 7 with | n when n < 5 -> 100 | n when n < 10 -> 200 | _ -> 300</code> → 200. Nested or-pattern (constructor etc. inside or) continues to be Codegen_error. Added 4 tests + updated 1 (759 passing; replaced "guard rejected" with "guard accepted").</p> <ul> <li><strong>Phase 4 #14: C codegen complex patterns</strong> — Rewrote Match</li> </ul> <p> pattern compilation as fully recursive <code>compile_pattern</code>. Decomposes each pattern into (test_expr, bindings_str); supports nesting constructor / tuple / record inside constructor; implements <code>P_int</code> / <code>P_str</code> (strcmp == 0) / <code>P_bool</code> / <code>P_unit</code> / <code>P_record</code> (named field destructure) / <code>P_as</code> (whole-value bind). <code>is_ptr_ty</code> / <code>payload_ty_for_ctor</code> / <code>field_ty</code> helpers resolve sub-value types and recursively decompose patterns. Verified (clang native): <code>match 3 with | 0 -> 100 | 1 -> 200 | _ -> 300</code> → 300; <code>match "hello" with | "hi" -> 1 | "hello" -> 2 | _ -> 3</code> → 2; <code>match Cons (Some 5, Nil) with | Nil -> 0 | Cons (None, _) -> 1 | Cons (Some n, _) -> n</code> → 5 (nested poly variant); <code>match Point { x = 3, y = 4 } with | Point { x = a, y = b } -> a + b</code> → 7. Or-pattern and guard continue to be Codegen_error. Added 6 tests + updated 4 substrings to new format (755 passing).</p> <ul> <li><strong>Phase 4 #13: C codegen polymorphic record monomorphization</strong> —</li> </ul> <p> Specializes <code>type 'a Box = { v: 'a }</code> etc. polymorphic records per type (<code>Box_int</code>, <code>Box_str</code> etc.) using same pattern as variant's Phase 4.11. <code>polymorphic_records</code> Hashtbl defers declarations (emit_record_typedef defers if r_params != []); extends <code>collect_mono_variant_instances</code> to also cover records; <code>emit_mono_record_typedef</code> concretizes field types with subst_params and generates <code>typedef struct { int v; } Box_int;</code>. <code>Record_lit</code> emit pulls mono name from Record_lit's <code>.ty</code> and emits compound literal (<code>((Box_int){.v = 42})</code>). Field_get and Record_update naturally work via <code>__auto_type</code>. Verified (clang native): <code>type 'a Box = { v: 'a }; let b = Box { v = 42 } in b.v</code> → 42; <code>let bi = Box { v = 42 } in let bs = Box { v = "hi" } in show (bi.v, bs.v)</code> → <code>(42, "hi")</code> (specializes both Box_int and Box_str). Added 3 tests + updated 1 (749 passing; replaced "polymorphic record reject" with "specialize verification").</p> <ul> <li><strong>Phase 4 #12: C codegen `show` general builtin</strong> — Auto-generates</li> </ul> <p> per-type specialized <code>show_T</code> C functions for <code>show : 'a -> str</code> by collecting per-call arg types from AST. <code>collect_show_types</code> finds <code>App (Var "show", arg)</code>; <code>add_with_deps</code> recursively registers types arg type depends on (tuple elem / record field / variant payload) (with cycle guard; doesn't infinite-loop on self-referential payload of recursive variant). <code>emit_show_fn</code> generates specialized fn per type — int/bool/str/unit trivial; tuple/record composes element show; variant (mono + polymorphic instantiation + recursive) is tag dispatch + payload show. <code>emit_expr App</code>'s <code>Var "show"</code> dispatches to <code>show_<tag>(arg)</code> call resolved by arg type's <code>ty_tag</code>. Verified (clang native): <code>show 42</code> → "42"; <code>show (1, "hello")</code> → <code>(1, "hello")</code>; <code>show (Some 42)</code> → "Some 42"; <code>show [1, 2, 3]</code> → "Cons (1, Cons (2, Cons (3, Nil)))". Based on <code>asprintf</code> (malloc leak but consistent with other codegen). Added 7 tests (747 passing).</p> <ul> <li><strong>Phase 4 #11: C codegen polymorphic variant monomorphization</strong></li> </ul> <p> — Implemented monomorphization that collects concrete instantiations from AST and fn signatures for <code>type 'a opt = None | Some of 'a</code> or <code>type 'a list = Nil | Cons of 'a * 'a list</code> etc. polymorphic variants and emits specialized struct (<code>opt_int</code>, <code>list_int</code> etc.) per instance. <code>polymorphic_variants</code> Hashtbl defers declarations; <code>mono_variant_instances</code> accumulates found instances; <code>subst_params</code> / <code>subst_variants</code> for param→arg substitution; <code>mono_variant_is_recursive</code> for recursion judgment on concrete types. Extended <code>c_type_of</code> and <code>ty_tag</code> to handle <code>TyCon (n, args)</code> with args (<code>int list</code> → <code>list_int</code> etc.). <code>Constr</code> emit pulls mono name from Constr's <code>.ty</code>; Match's <code>is_ptr</code> judgment also recursion-checks with mono name. Verified (clang native): <code>type 'a opt = None | Some of 'a; let v = Some 42 in match v with | None -> 0 | Some n -> n</code> → 42; <code>type 'a list = Nil | Cons of 'a * 'a list; let rec sum = fn xs -> match xs with | Nil -> 0 | Cons (h, t) -> h + sum t in sum [1, 2, 3]</code> → 6 (list literal + recursive sum; <code>[1, 2, 3]</code> is parser-desugared to <code>Cons (1, Cons (2, Cons (3, Nil)))</code>). Added 4 tests (740 passing).</p> <ul> <li><strong>Phase 4 #10: C codegen recursive variant + P_tuple pattern</strong> —</li> </ul> <p> Switched variants with self-referential payload (e.g. <code>type ilist = INil | ICons of int * ilist</code>) to heap-allocated node + ptr typedef (<code>typedef struct ilist_node ilist_node; typedef ilist_node* ilist; struct ilist_node { ... };</code>). <code>variant_is_recursive</code> detects self-reference in payload; registers in <code>recursive_variants</code> Hashtbl. Constr emit malloc-returns ptr with <code>({ ilist_node* __p = malloc(...); __p->tag = N; __p->payload.CTOR = ...; __p; })</code>. Match emit switches <code>.</code> vs <code>-></code> based on scrutinee's type. Expands P_tuple sub-pattern (<code>CgCons (h, t)</code>) into <code>.f0 / .f1</code> binding sequence. Circular typedef dependency resolved by emitting forward decl + ptr typedef first, then struct body after tuple/record typedefs. Verified (clang native): <code>type ilist = INil | ICons of int * ilist; let rec sum = fn xs -> match xs with | INil -> 0 | ICons (h, t) -> h + sum t in sum (ICons (1, ICons (2, ICons (3, INil))))</code> → 6 (linked list sum). Added 5 tests (736 passing).</p> <ul> <li><strong>Phase 4 #9 Phase B: C codegen anonymous Fun + closure-with-</strong></li> </ul> <p> captures<strong> — Lifts anonymous Fun in expression position as heap-allocated env struct + adapter + closure construction. Capture vars rewritten to `__env_self->name` via `current_env_subst` map; capture types resolved by traversing scope via `current_var_types` (workaround for polymorphic residual problem after let-poly). Closure typedefs emitted in inner→outer order (post-order walk) to avoid circular references. `current_expected_ty` passes context type to Fun emit; estimates inner Fun's type from outer fn's return_ty. Verified (clang native): `let apply = fn f -> fn x -> f x in let inc = fn n -> n + 1 in apply inc 5` → 6 (curried HOF); `let twice = fn f -> fn x -> f (f x) in twice inc 5` → 7; `let make_adder = fn n -> fn x -> x + n in (make_adder 5) 10` → 15 (closure with capture). Added 4 tests + updated 1 (731 passing).</strong></p> <ul> <li><strong>Phase 4 #9 (Phase A): C codegen first-class functions</strong> —</li> </ul> <p> Represents <code>T1 -> T2</code> type function value as C struct <code>closure_T1_T2 = { void* env; T2 (*fn)(void*, T1); }</code>. Auto- generates env-ignoring adapter (<code>f_closure_fn</code>) + value const (<code>f_as_value</code>) for each top-level fn. <code>c_type_of (TyArrow ...)</code> maps to closure struct name; <code>ty_tag</code> also handles nesting. <code>emit_expr Var</code>: at value position if name is top-level fn, emit <code>f_as_value</code> (Codegen_error if using inner-lifted in value position). <code>emit_expr App</code>: known top-level Var call continues on direct call fast path; otherwise dispatches via closure <code>({ __auto_type __c = e; __c.fn(__c.env, arg); })</code>. <code>collect_arrow_types</code> walks AST + fn signatures to gather arrow types and auto-generates closure typedefs. Verified (clang native): <code>let inc = fn x -> x + 1 in let apply = fn f -> f 5 in apply inc</code> → 6 (top-level fn passed as value to HOF works). Phase B (inner / anonymous fn value-ization) in separate slice. Added 6 tests (727 passing).</p> <ul> <li><strong>Phase 4 #8: C codegen closure conversion (defunctionalization)</strong></li> </ul> <p> — Added pre-pass that lifts <code>let h = fn x -> body in ...</code> inside function body to top-level. free_vars helper computes free variables (excluding builtin / top-level fn names of typer's initial_env); prepends captured variables to C function's param list (defunctionalization). <code>emit_expr</code> Let sees <code>Hashtbl.mem inner_lifts name</code> and skips lifted bindings; App passes capture args at call site. Captures are int/bool/str/unit only (tuple/record/function value capture is Codegen_error). Supports multi-level nesting (h captures x and n from 2 levels). Side: changed <code>resolve_fn_types</code> to pull monomorphic types at call site via <code>find_concrete_arrow</code> for Fun.ty issue after let-poly. Verified: <code>let outer = fn x -> let h = fn y -> x + y in h 10 in outer 5</code> → 15; nested 2 levels → 6. Added 4 tests + updated 1 (721 passing; replaced old "closure reject" test with "lift result verification").</p> <ul> <li><strong>Phase 4 #7: C codegen variant + match</strong> — Compiles monomorphic</li> </ul> <p> variant types (<code>type Status = Ok | Err of str</code>) to tagged union (<code>typedef struct { int tag; union { const char* Err; } payload; } Status;</code>). <code>Constr</code> to compound literal (<code>((Status){.tag = 1, .payload.Err = "boom"})</code>). <code>Match</code> to ternary chain in statement expression (<code>__scrut.tag == N ? ({ binding; body; }) : ...</code> + fallthrough <code>abort()</code>). Pattern subset: <code>P_constr</code> (nullary or <code>P_var</code> / <code>P_wild</code> sub); <code>P_var</code>; <code>P_wild</code>. Guard / polymorphic variant / nested pattern are Codegen_error. Verified (clang native): <code>type Color = R | G | B; match G with | R -> 0 | G -> 1 | B -> 2</code> → 1; <code>type Status = Ok | Err of str; match Err "boom" with | Ok -> 0 | Err msg -> str_len msg</code> → 4. Added 9 tests (715 passing).</p> <ul> <li><strong>Phase 4 #6: C codegen record support</strong> — Compiles <code>type Point</code></li> </ul> <p> = { x: int, y: int }<code> to </code>typedef struct { int x; int y; } Point;<code>. Implements Record_lit / Field_get / Record_update (Record_update uses </code>({ __auto_type __rupd = base; __rupd.f = v; __rupd; })<code> statement expression pattern). </code>collect_record_names<code> walks AST + fn signature to gather used record types and auto-generate typedefs. Extended </code>compile_to_c<code> to include top-level decl processing (same as Pipeline.type_of, skips eval; only record/variant/view/drop registration). Verified (clang native): </code>let p = Point { x = 3, y = 4 } in p.x + p.y<code> → 7; record update → 102; record-returning fn → 15. Polymorphic record (</code>type 'a Box = { v: 'a }<code>) continues to be Codegen_error. Added 7 tests (706 passing).</code></p> <ul> <li><strong>Phase 4 #5: C codegen tuple support + AST type annotation</strong></li> </ul> <p> foundation<strong> — As foundation, added `mutable ty : ty option` to `Ast.expr`; `Typer.infer` now records inference results on each node. This lets codegen directly reference per-node types. Compiles `Tuple` to C struct (`typedef struct { ... } tuple_int_int;`) + C99 compound literal `((tuple_int_int){.f0 = 1, .f1 = 2})`. Compiles `fst` / `snd` builtin to `.f0` / `.f1` field access. Supports arbitrary element types (int/bool/str + nested tuple); auto-generates struct per shape (`collect_tuple_shapes` walks entire AST + fn signature). Verified (clang native): `let p = (1, 2) in fst p + snd p` → 3; `let p = ("hello", 42) in print (fst p)` → "hello"; `let split = fn s -> (s, str_len s) in print (fst (split "hello"))` → "hello". Added 6 tests (699 passing).</strong></p> <ul> <li><strong>Phase 4 #4: C codegen: str-taking / returning functions</strong> —</li> </ul> <p> Allows lifted function param / return to also use str (const char<em>). Added `param_ty` / `return_ty` to `fn_decl`; `lift_fn_skels` extracts skeletons → `resolve_fn_types` flows all lifted fns to typer as one let-rec group for type inference (handles self / mutual recursion) → `c_type_of` maps Ast.ty to C type (int/bool → `int`, str → `const char</em><code>, unit → </code>int<code>). Compiles </code>str_len<code> builtin to C's </code>strlen<code> (App special case). Verified (clang native): </code>let greet = fn n -> if n > 0 then "pos" else "neg" in print (greet 5)<code> → "positive"; </code>let exclaim = fn s -> s ++ "!" in print (exclaim "hello")<code> → "hello!"; </code>str_len "hello, world!"<code> → 13. Added 5 tests (693 passing).</code></p> <ul> <li><strong>Phase 4 #3: C codegen string support</strong> — Compiles <code>Str_lit</code></li> </ul> <p> to C string literal; <code>++</code> via runtime helper <code>__lang_str_concat</code> (malloc-based); <code>print</code> builtin to <code>puts</code> (statement expression returning int 0). Switched <code>let</code> to GNU/Clang extension <code>__auto_type</code> so same emit works for both int/str values. Made <code>emit_program</code> type-aware (<code>~main_ty</code>); selects printf's format from main's type (int/bool → <code>%d</code>, str → <code>%s</code>, unit → printf skip). Verified: <code>print "hello, world!"</code> → hello, world!; <code>"hello" ++ " " ++ "world"</code> → hello world (all clang native). Malloc leaks (region/GC integration in future slice). Added 6 tests / restructured existing codegen tests as fragment inspection (688 passing).</p> <ul> <li><strong>Phase 4 #2: C codegen function lifting</strong> — Lifts top-level</li> </ul> <p> <code>let f = fn x -> ...</code> and <code>let rec f = fn x -> ... and g = fn y -> ...</code> as C function (with forward declaration). Compiles <code>App (Var name, arg)</code> form direct calls to C <code>name(arg)</code>; both self-recursion and mutual recursion work (factorial 10 = 3628800, fibonacci 15 = 610, is_even 7 = 0 confirmed via clang native). Closure (<code>fn ...</code> inside function body) continues to be Codegen_error. Added 5 tests (681 passing).</p> <ul> <li><strong>Phase 4 #1: C codegen MVP</strong> — First step from interpreter to</li> </ul> <p> native. Implemented <code>emit_program : Ast.program -> string</code> in new <code>lib/codegen_c.ml</code>; converts subset of int / bool / arith / cmp / logic / Neg / If / Let (P_var only) / Var / Annot to C expression (let compiled to single C expression via GCC/Clang statement expression <code>({ ... })</code>). Added <code>-c FILE</code> / <code>-ce <expr></code> flags to CLI; outputs C source to stdout. <code>clang OUT.c -o BIN && ./BIN</code> for native execution. Functions / strings / record / variant / region / view etc. now Codegen_error. Added 7 tests (677 passing); manual E2E verified via <code>clang</code> (<code>let a = 10 in let b = 20 in if a + b > 25 then a * b else 0</code> → 200).</p> <ul> <li><strong>example: examples/pipeline.mere</strong> — Realistic example</li> </ul> <p> (~75 lines) combining region / view / effect (builtin Logger / Metrics + cap passing + using sugar) / with Drop. Simple build pipeline: open/close user session with <code>with session = open_session logger uid</code>; process each task with <code>region R { ... }</code>; inside region build <code>view Task[R]</code> to calculate size. Output is session open/close log + per-task [task] log + [METRIC] inc / record + user log + final total. Demonstrates Mere's full feature set working consistently in a practical example.</p> <ul> <li><strong>Phase 3.1: `with` Drop semantics</strong> — <code>with c = v in body</code></li> </ul> <p> requires v's type to be a Drop type (declared <code>drop type ...</code>); Trivial value is type error (use <code>let</code>). On eval side, calls v's <code>close: unit -> unit</code> field at scope end (no-op if absent). Multiple <code>with x, y in body</code> close in LIFO order y → x. Rewrote examples/with_caps.mere based on Drop type. Implemented case (i) of design doc 12_drop_and_with.md. Added 6 tests / restructured 6 (670 passing).</p> <ul> <li><strong>effect: builtin `Logger` / `Metrics` cap types + `mk_logger`</strong></li> </ul> <p> / <code>mk_metrics</code> constructor builtins<strong> — Provides cap types as stdlib. Registered `Logger { info, warn, error: str -> unit }` and `Metrics { inc: str -> unit, record: str -> int -> unit }` in typer; added corresponding V_record constructor functions to eval. Users don't need to redefine cap types each time (overrides allowed). Rewrote examples/effects.mere with builtin usage. Added 7 tests (668 passing).</strong></p> <ul> <li><strong>effect: `using [cap]` syntax sugar</strong> — Desugars <code>fn x using</code></li> </ul> <p> [logger] -> body<code> to </code>fn logger -> fn x -> body<code> (caps are outer-most curried args). Eases partial application iteration frequent in cap-passing style (main pattern of Q-003/Q-006 solution). Type annotations allowed; multiple caps allowed; combination with regular params allowed. Implements auxiliary design of design doc </code>10_effect_trial_findings.md<code>. Added 7 tests (661 passing). Rewrote examples/effects.mere in sugar form too.</code></p> <ul> <li><strong>example: examples/effects.mere</strong> — Demonstration of</li> </ul> <p> Capability passing pattern (about 75 lines). Declares <code>Logger</code> / <code>Metrics</code> cap types as records; demos 3 patterns: direct use in low-order function / bucket-brigade / partial application passing to high-order function. Demonstrates that design doc <code>05_effect_system.md</code>'s "side effects = passing capability as values" works with current Mere (HM + function args + record + curry) alone — no need for new syntax for effect system.</p> <ul> <li><strong>region Phase 2.6</strong>: <code>Trivial[R]</code> constraint — Allows declaring</li> </ul> <p> Drop type with <code>drop type Name = ...</code>. At <code>&R v</code> / <code>R.alloc(v)</code> / view field construction, walks inner type; if it includes a type registered in <code>drop_types</code> registry, type error "Trivial[R] violated". Function type is Trivial (closure value itself is not Drop). Syntactified case (i) of design doc 12_drop_and_with.md. <code>with</code> expression + Drop execution in Phase 3. Added 7 tests (654 passing).</p> <ul> <li><strong>region Phase 2.5</strong>: <code>R.alloc(v)</code> syntactic sugar — Method-call</li> </ul> <p> style notation for <code>&R v</code>. Parser holds region_stack; inside <code>region NAME { ... }</code> body, desugars <code>NAME.alloc(EXPR)</code> to <code>Ref (NAME, EXPR)</code>. If R is not an in-scope region, treats as regular field access; existing <code>obj.alloc(...)</code> patterns unaffected. Added 7 tests (647 passing).</p> <ul> <li><strong>region Phase 2.4</strong>: type-level region tag for view values +</li> </ul> <p> region propagation for field access / record update — View construction returns <code>TyCon (name, [TyRef (target_region, TyUnit)])</code> to embed region in value type; <code>Field_get</code> / <code>Record_update</code> reads view name + embedded region and uses <code>subst_region</code> to substitute field type with actual region. View value itself becomes target of escape check (<code>Cell[S]</code> can't be carried out of region S). Added <code>Name[R]</code> notation heuristic to pp_ty. Added 5 tests (640 passing). Resolves known limitation "field access returns raw R" from Phase 2.3.</p> <h2 id="2026-06-16">2026-06-16</h2> <ul> <li><strong>region Phase 2.3</strong>: enforces region of view construction +</li> </ul> <p> region parameter substitution — View can be constructed only inside <code>region { ... }</code> block. At construction, view declaration's region parameter <code>R</code> is substituted with active region name; if field has <code>&R T</code>, tag aligns automatically even with different region name. Added views Hashtbl and active_regions stack to typer; push/pop at <code>Region_block</code>; view dispatch + <code>subst_region</code> at <code>Record_lit</code>. Ties in with §5 "view type" section of memory-model.md.</p> <ul> <li><strong>region Phase 2.2</strong>: <code>view V[R] of T { fields };</code> declaration —</li> </ul> <p> Introduced view type fixed in Q-009 as syntax. Like <code>view Node[R] of int { value: int, next: int };</code>, takes region parameter <code>[R]</code> and (optional) internal type <code>of T</code>, declares fields with <code>{ field: ty, ... }</code>. In Phase 2.2 treated as "region-tagged record" (region is only recorded, not enforced); <code>Node { value = 1, next = 0 }</code> construction and <code>n.value</code> access work. Strict semantics (construction only inside region; mandatory <code>&R T</code> fields) in future Phase. Design doc: <code>14_view_types.md</code>'s 3 axioms (immutable / region-scoped / structural identity) at stage of syntactifying first 2.</p> <ul> <li><strong>region Phase 2.1</strong>: <code>&R v</code> value expression + escape check —</li> </ul> <p> <code>&R 5</code> turns value into region-tagged reference type. At exit of <code>region R { body }</code>, checks if body's type leaks R; compile- time error if leaked. Region promoted from "type-system label" to "actual safety guarantee".</p> <ul> <li><strong>region / `&R T` Phase 1</strong> — First step into memory model.</li> </ul> <p> <code>region R { body }</code> expression introduces R as region name into scope; added <code>&R T</code> as reference type to AST/typer/eval. Phase 1 is <strong>syntax only</strong> — escape check, Trivial constraint, view type, <code>r.alloc(v)</code> semantics from Phase 2 onward. Design doc: corresponds to 11_region_vs_arena.md / 14_view_types.md.</p> <ul> <li><strong>Exhaustiveness Phase 1</strong> (Exhaustive module) — Detects bool</li> </ul> <p> and variant type exhaustiveness as warnings. <code>match Some x with | Some n -> ...</code> outputs "missing None" to stderr but evaluation continues. Guarded arm conservatively "not covered"; as-pattern and or-pattern transparent. lib/exhaustive.ml doesn't depend on Typer (Typer calls register_variants to populate).</p> <ul> <li><strong>Math builtins 8</strong> (<code>pi</code>/<code>e</code> constants + <code>sqrt</code>/<code>f_abs</code>/<code>f_neg</code>/</li> </ul> <p> <code>floor</code>/<code>ceil</code>/<code>round</code>) — Float arithmetic basics complete.</p> <ul> <li><strong>`int_max`/`int_min` constant builtins</strong> — Mere's first</li> </ul> <p> non-function builtins.</p> <ul> <li><strong>`time : unit -> float` + `exit : int -> 'a`</strong> — Unix epoch</li> </ul> <p> and process termination.</p> <ul> <li><strong>Float comparison 4</strong> (<code>f_lt</code>/<code>f_le</code>/<code>f_gt</code>/<code>f_ge</code>).</li> </ul> <ul> <li><strong>CSV parser example</strong> (~130 lines, reduced RFC 4180).</li> </ul> <ul> <li><strong>mini_calc.mere extension</strong>: let binding + variables + env-</li> </ul> <p> based eval; shadowing works.</p> <ul> <li><strong>list_lib.mere</strong> added: 12 list utility functions written in</li> </ul> <p> Mere itself (map/filter/fold_left/fold_right/length/rev/take/ drop/range/replicate/for_all/any).</p> <ul> <li><strong>Float type introduced</strong> — <code>TyFloat</code> primitive + <code>Float_lit</code></li> </ul> <p> (<code>1.5</code> literal) + V_float; 4 conversions (<code>float_of_int</code> / <code>int_of_float</code> / <code>str_of_float</code> / <code>float_of_str</code>) + 4 arithmetic (<code>f_add</code> / <code>f_sub</code> / <code>f_mul</code> / <code>f_div</code>). No implicit int/float conversion. Resolves known limitation "no float".</p> <ul> <li><strong>File I/O</strong> — <code>read_file : str -> str</code> / <code>write_file : str -></code></li> </ul> <p> str -> unit<code>. Can write CLI tools. Added </code>examples/word_count .mere<code>.</code></p> <ul> <li><strong>`str_unescape` builtin</strong> — Decodes <code>\n</code> <code>\t</code> <code>\r</code> <code>\\</code> <code>\"</code></li> </ul> <p> <code>\/</code>. Escape-string support for JSON parser.</p> <ul> <li><strong>Character literal `'X'`</strong> — Lexer only; length 1 str.</li> </ul> <p> Disambiguates with tyvar <code>'a</code> (closing quote presence); <code>match c with | 'n' -> ...</code> for dispatch.</p> <ul> <li><strong>List display improvement</strong> — <code>to_string</code> displays Cons/Nil</li> </ul> <p> chain as <code>[a, b, c]</code>. JSON parser output dramatically more readable.</p> <ul> <li><strong>Documentation overhaul</strong> — README rewrite + newly added</li> </ul> <p> <code>docs/{tutorial, language-reference, stdlib-reference, patterns}.md</code> (1100+ lines).</p> <ul> <li><strong>`divmod`</strong> — Mere's first tuple-return builtin (<code>int → int →</code></li> </ul> <p> (int <em> int)`).</em></p> <ul> <li><strong>`square` / `cube`</strong> — int → int 2nd / 3rd power.</li> </ul> <ul> <li><strong>`sum_range`</strong> — O(1) sum via Gauss formula.</li> </ul> <ul> <li><strong>`incr` / `decr`</strong> — int → int +1 / -1.</li> </ul> <ul> <li><strong>`iter_n`</strong> — Higher-order side-effect loop.</li> </ul> <ul> <li><strong>Polymorphic `const` / `flip`</strong> — Mere's first 3-quantified,</li> </ul> <p> higher-order polymorphic builtins. Implemented via forward-ref of <code>apply_value_ref</code>.</p> <ul> <li><strong>Polymorphic `id` / `swap` / `pair`</strong> — Standard set of tuple</li> </ul> <p> ops complete.</p> <ul> <li><strong>Polymorphic `fst` / `snd`</strong> — Mere's first 2-quantified</li> </ul> <p> scheme builtins.</p> <ul> <li><strong>`try_or`</strong> — Mere's first error-handling builtin.</li> </ul> <ul> <li><strong>`fail` / `show`</strong> — Mere's first polymorphic builtins</li> </ul> <p> (scheme.quantified).</p> <ul> <li><strong>as-pattern / or-pattern</strong> — <code>(a, b) as p</code>, <code>| 1 | 2 | 3 -></code></li> </ul> <p> ...<code> (typer enforces binding name/type match).</code></p> <ul> <li><strong>Structural equality</strong> — <code>==</code> / <code>!=</code> recursively compare</li> </ul> <p> tuples / records / constructors.</p> <ul> <li><strong>Type alias `type Name = T;`</strong> — Parse-time substitution;</li> </ul> <p> disambiguates variant/record/alias via <code>|</code>/<code>of</code>.</p> <ul> <li><strong>Function composition `<<` / `>>`</strong> — Right-associative;</li> </ul> <p> higher precedence than <code>|></code>.</p> <ul> <li><strong>Multiple type parameters `('a, 'b) result`</strong> — Resolves known</li> </ul> <p> limitation "up to 1 type parameter".</p> <ul> <li><strong>Top-level let pattern</strong> — <code>let _ = ...;</code>, <code>let (a, b) = ...;</code></li> </ul> <p> etc. at top-level; resolves known limitation.</p> <ul> <li><strong>If without else</strong> — <code>if cond then body</code> (body unit type).</li> </ul> <ul> <li><strong>Match guard `| pat when expr -> body`</strong> — Resolves known</li> </ul> <p> limitation "no guard".</p> <ul> <li><strong>Block expression `{ e1; e2; eN }`</strong> — Parser sugar for</li> </ul> <p> Let(P_wild) chain.</p> <ul> <li><strong>List pattern `[a, b, ...t]`</strong> — Symmetric to literal; parser</li> </ul> <p> sugar.</p> <ul> <li><strong>Record update `{ p | x = 10 }`</strong> — Immutable update.</li> </ul> <ul> <li><strong>Record type `type Point = { x: int, y: int }`</strong> — Nominal</li> </ul> <p> records; polymorphic; partial pattern.</p> <ul> <li><strong>Mutual recursion `let rec ... and ...`</strong> — Resolves known</li> </ul> <p> limitation "no mutual recursion".</p> <ul> <li><strong>List literal `[1, 2, 3]`</strong> — Parser sugar for Cons/Nil chain.</li> </ul> <ul> <li><strong>Pipe `|>` / signature alias</strong> — Ergonomic improvements.</li> </ul> <ul> <li><strong>Multi-arg typed fn</strong> — <code>fn (x: int, y: str) -> body</code> desugars</li> </ul> <p> to curry.</p> <ul> <li><strong>Massive stdlib additions</strong> — print_int / str_of_int /</li> </ul> <p> int_of_str / str_len / not / min / max / abs / pow / gcd / lcm / clamp / sign / even / odd / chr / ord / to_upper / to_lower / str_trim / str_rev / str_contains / str_count / str_replace / str_starts_with / str_ends_with / str_repeat / substring / char_at / is_digit / is_alpha / is_space / read_line / print_no_nl / print_err / assert / bool_of_str / str_compare and many more.</p> <hr> <h2 id="2026-06-15-06-16-early-week">2026-06-15 — 06-16 (early week)</h2> <ul> <li>Main extensions: operator expansion (<code>/ %</code> <code><= >= > !=</code> <code>&& ||</code>),</li> </ul> <p> let pattern, <code>with</code> expression, polymorphic types (<code>'a opt</code>), tuples, sum types + pattern matching.</p> <ul> <li>Design docs: Q-008 (region/arena integration), Q-009 (view type</li> </ul> <p> 3 axioms), Q-010 (region-version std), Q-011 (Drop order). Mere's memory model design map complete.</p> <hr> <h2 id="2026-06-06-start-date">2026-06-06 (start date)</h2> <ul> <li>After OCaml 4-phase trial, fixed host language as OCaml (Q-001</li> </ul> <p> resolved).</p> <ul> <li>In 1 day, completed minimum core "integer + let + bool + if +</li> </ul> <p> function + recursion + bidirectional type check + REPL" (24 tests).</p> <ul> <li>Strings + print + <code>++</code> concat + unit (slice 1); REPL (slice 2);</li> </ul> <p> multiple top-level decls (slice 8).</p> <ul> <li><strong>Hindley-Milner type inference + let-polymorphism</strong>: implemented</li> </ul> <p> Algorithm W + occurs check + generalize/instantiate. Inference of annotation-less functions, polymorphic id, polymorphic compose, let-poly all work (slice 9, 29 tests).</p> <hr> <h2 id="cumulative-as-of-2026-06-16">Cumulative (as of 2026-06-16)</h2> <ul> <li>Design docs: 4 (Q-008/009/010/011)</li> <li>Implementation slices: <strong>62</strong></li> <li>Tests: <strong>567</strong> (initial 35 → 567, 16×)</li> <li>Builtins: <strong>68</strong></li> <li>Known limitations resolved: <strong>8</strong> (mutual recursion / guard /</li> </ul> <p> multi-type-param / top-level let pattern / list display / char literal / file I/O / float)</p> <hr> <h2 id="not-yet-started-future">Not yet started (future)</h2> <ul> <li><strong>`&T` reference</strong> — borrow annotation (<code>&shared write</code> etc.)</li> </ul> <p> → core of memory model</p> <ul> <li><strong>`region R { ... }` / `view V[R] of T`</strong> — implementation of</li> </ul> <p> Q-008/009</p> <ul> <li><strong>Effect system</strong> — capability types and effect tracking</li> <li><strong>Native codegen</strong> — LLVM or Wasm</li> <li><strong>Exhaustiveness check Phase 2</strong> — precise exhaustiveness for</li> </ul> <p> int/str/float/tuple/record; redundancy check</p> <ul> <li><strong>Inline unicode / Unicode source</strong> — currently ASCII only</li> <li><strong>Module system</strong> — file split + namespace</li> <li><strong>Dependent types / refinement types</strong> — staged introduction</li> </ul> <p> per 04_fundamental_tradeoffs.md</p> <ul> <li><strong>Row polymorphism</strong> — no annotation needed for record update</li> <li><strong>Multi-line REPL</strong> — REPL is single-line only</li> </ul> </main> <footer>Generated by Mere SSG (contrib/site/build.mere)</footer> <script> (function() { var input = document.getElementById('search-input'); var results = document.getElementById('search-results'); if (!input || !results) return; var index = null; var debounce_t = null; function render(query) { if (!query) { results.innerHTML = ''; results.style.display = 'none'; return; } var q = query.toLowerCase(); var matches = index.filter(function(p) { return p.title.toLowerCase().indexOf(q) >= 0 || p.content.toLowerCase().indexOf(q) >= 0; }); if (matches.length === 0) { results.innerHTML = '<p style="color:#888">No matches for "' + query + '"</p>'; } else { results.innerHTML = '<ul>' + matches.map(function(p) { var excerpt = ''; var idx = p.content.toLowerCase().indexOf(q); if (idx >= 0) { var start = Math.max(0, idx - 40); var end = Math.min(p.content.length, idx + 80); excerpt = (start > 0 ? '…' : '') + p.content.substring(start, end) + (end < p.content.length ? '…' : ''); } return '<li><a href="' + p.url + '">' + p.title + '</a>' + (excerpt ? '<br><small style="color:#888">' + excerpt + '</small>' : '') + '</li>'; }).join('') + '</ul>'; } results.style.display = 'block'; } input.addEventListener('input', function() { var v = input.value; clearTimeout(debounce_t); debounce_t = setTimeout(function() { if (!index) { fetch('search.json').then(function(r) { return r.json(); }) .then(function(data) { index = data; render(v); }); } else { render(v); } }, 100); }); })(); </script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css"> <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-clike.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-bash.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-ocaml.min.js"></script> <script src="playground/prism-mere.js"></script> <script> // The Mere SSG emits `language-sh`, but Prism expects `language-bash` document.querySelectorAll('code.language-sh').forEach(function(el) { el.classList.remove('language-sh'); el.classList.add('language-bash'); }); Prism.highlightAll(); </script> </body> </html>