0.1.52 — 2026-10-12
THE FIFTY-SECOND, the lupin half of wolf 0.2.29. Pin unchanged: 294d626d (wolf-lang v0.2.24). Eleven commits since 0.1.51, all the lupin half of one wolf-lang lane, s232 (the shell's signals; a planted break, 04f2554, and its revert, 5dd150f, among them). Every lane was merged before the release lane (r34) began; r34 adds the version.
- The other lane in wolf 0.2.29 has no lupin half. wolf-lang's s231 (no cap on parked tasks, parked waits that do not poll, a dial outside the socket lock, wolf-lang#713) changed only the compiled runtime. lupin answers #713's witness at N = 64 as the compiled tiers do (it never had the pool's cap). It stalls a program that dials more than 128 connections to its own listener before accepting one: wolf-interp#239, open (the checked machine's twin is wolf-lang#718).
- The walk at this release: 1002 files, 955 entries, 47 members; 760 reach
run, 721 match, 94 dynamic, 64 conservatism, 72 out of scope, 4 mismatch, every one filed. Unchanged from 0.1.51 to the byte (the pin did not move; s232's rows live intests/until a re-pin brings them into the walk). - Ruling now made that the lane implemented ahead of it, as implemented: #70 (s232's signals, R1–R9; R9: lupin serves what the Rust standard library reaches and refuses the send and the disposition read by name).
- lupin's archive still ships no std (ruling #59 B(1)): a
lupininstalled alone answersunsupportedforuse std.…with nothing set; one placed beside a 0.2.29wolfreads thestd/wolf ships.
The shell's signals: the mirror (wolf-lang s232, #679, #681, #141; ruling #70)
wolf-lang s232 gives every signal POSIX names a bit (twenty a program may catch, eight it may only send), one shape rule for every set, a read of the ignored set, a send to any pid, a meaning's number and this process's id and its parent's. This machine mirrors what std reaches without unsafe (ruling #70, R9, wolf-lang [os.signal.shell.note]):
- Served:
os_pid(std'sprocess::id),os_ppid(unixparent_id; windows itsunsupportedrow) andos_signal_number(the linux and macOS numberings, by bit; windowsunsupported; any other host refuses by name rather than guess). - Shape rows everywhere:
os_signal_poll,ignoreanddefaultrefuse an empty set or any bit outside the twenty listenable meanings withio(a poll refused only an all-unmapped set before;512isUSERnow);os_signal_sendisinvalidfor a pid not above 0 or wider than a process id and a meaning that is neither 0 nor one of the 28;os_signal_numberisinvalidfor anything but one meaning.os_spawn_job'sdefaultstake the twenty. - Refused by name:
os_signal_send(std sends no signal but SIGKILL, and only to a child it holds) andos_signal_ignored(nosigaction) on unix; windows answers theirunsupportedrow. lupin still never listens. tests/signal_shell_s232.rsruns wolf-lang s232's corpus row, the row it moved and its nine fixtures, byte for byte.
0.1.51 — 2026-10-11
THE FIFTY-FIRST, the lupin half of wolf 0.2.28. Pin unchanged: 294d626d (wolf-lang v0.2.24). Nine commits since 0.1.50, all the lupin half of one wolf-lang lane, s230 (the calls that change a file; a planted break, 71014ec, and its revert among them). Every lane was merged before the release lane (r33) began; r33 adds the version.
- The other lanes in wolf 0.2.28 have no lupin half. wolf-lang's s226 (the checked machine runs tasks), s227 and s229 (E1010 at a call that keeps or sends what it is handed: wolf-lang#660, #665) and s228 (the runtime residue) changed only the compiler. Where lupin answers their programs differently, the difference is filed and still open, and wolf-lang's gates pin it by version: wolf-interp#227 (no rendezvous back-edge in the race detector) and #230 (
whenthrough a closure holding the mutex istrap(exclusivity)), #228 and #229 (a UB finding inside a proc; tasks' module state), #232 (a freed region's bytes reached through aListelement, aMapkey or a channel payload run toexit(0)), #233 (a soundcopyof a scalar struct underin r { }traps) and #234 (anet_readresult returned out of its region runs). The 0.2.28 pairing measures each against this release's published archive. - The walk at this release: 1002 files, 955 entries, 47 members; 760 reach
run, 721 match, 94 dynamic, 64 conservatism, 72 out of scope, 4 mismatch, every one filed. Unchanged from 0.1.50 to the byte (the pin did not move; s230's rows live intests/until a re-pin brings them into the walk). - Ruling now made that the lane implemented ahead of it, as implemented: #67 (s230's spellings and shapes; R10 = A: lupin serves what the Rust standard library reaches and refuses the rest by name).
- lupin's archive still ships no std (ruling #59 B(1)): a
lupininstalled alone answersunsupportedforuse std.…with nothing set; one placed beside a 0.2.28wolfreads thestd/wolf ships.
The calls that change a file, and os_error() after exists (s230, wolf-lang#346, #698, #699, #700, #702, #704; #235)
lupin's half of wolf-lang s230, written from [os.fs.mode], [os.fs.create.mode], [os.fs.symlink], [os.fs.link], [os.fs.times], [os.fs.truncate] and [os.fs.umask]. Eight names resolve. Seven are served through the standard library's own calls, no unsafe: fs_set_mode (chmod, a final link followed), fs_open_perm and fs_create_dir_mode (a creation's permission bits, less the umask), fs_symlink (the target verbatim, an empty one invalid), fs_link (a second name; never replaces, never follows), fs_truncate (a length on a handle, the offset unmoved) and fs_set_times. Every shape row (invalid) is answered before containment and before the host, in the compiler's order; windows answers the clause's unsupported row for the mode, the moded creations and the symbolic link.
What the standard library does not reach is refused BY NAME, never answered with a row the host would not give:
os_umask, whole and on every host: there is noumaskin the standard library. A mask outside0o777is stillinvalid.fs_set_timeson a link itself (followfalse and the path a link): that isutimensatwithAT_SYMLINK_NOFOLLOW, and the library sets times only on an open handle. On a path that is not a link the flag changes nothing and the call is served. "Now" is this machine's clock, read once per call; a path this process can open neither for reading nor for writing, and a "now" the host refuses for want of ownership, are refused by name too (the host's own call needs neither).
Whether lupin may take a safe system-call crate for these two was wolf-lang's [os.fs.change.note] R10, the question ruling #57's R8 answered "no" for the terminal calls. Ruling #67 answers it the same way (R10 = A): both stay refused by name.
os_error() is the host's number after exists and cross_device (#235). fs_create_dir and fs_rename matched those two kinds ahead of the one place the number was noted, so it read 0 after exactly those rows where every compiler machine answers EEXIST and EXDEV. Both old rows are fixed, and every new row notes it.
tests/fs_change_s230.rs runs wolf-lang's six corpus rows and five unix fixtures byte for byte and asserts each refusal on its words; every row of wolf-lang's fs_change_lanes gate agrees with this lupin as the sibling.
0.1.50 — 2026-10-10
THE FIFTIETH, the lupin half of wolf 0.2.27. Pin unchanged: 294d626d (wolf-lang v0.2.24). Thirty-six commits since 0.1.49, the lupin halves of six wolf-lang lanes: s220 (5, a wrapping product past i128), s219 (7, jobs and the signal poll), s225 (9, env_unset and os_exec), s224 (4, json last-wins), s218 (7, the stat record, lstat, readlink, the typed listing) and s204 (4, the std beside the binary). Every lane was merged before the release lane (r32) began; r32 adds the version and one CI line. wolf-lang's gates pin 0.1.49 as pre-mirror in int_truth_lanes, json_dup_lanes, fs_stat_lanes, ctrl_c_lanes and exec_lanes; the 0.2.27 pairing measures each against this release's published archive and drops what it answers.
- The walk at this release: 1002 files, 955 entries, 47 members; 760 reach
run, 721 match, 94 dynamic, 64 conservatism, 72 out of scope, 4 mismatch, every one filed. Unchanged from 0.1.49 to the byte (the pin did not move; the six lanes' rows live intests/until a re-pin brings them into the walk). - Rulings now made that lanes implemented ahead of them, each as implemented: #57 (s219's surface, R8 = A: lupin refuses the terminal half by name), #59 (the std beside the binary; lupin's archive ships no std), #60 (s218's spellings), #61 (json: one member per name, first position, last value), #62 (
env_unset, the execve shape). - lupin's archive still ships no std (ruling #59 B(1)). A
lupininstalled alone answersunsupportedforuse std.…with nothing set, where awolffrom the 0.2.27 archive runs it; alupinplaced beside thatwolfreads thestd/wolf ships. - CI: the PR smoke's cap goes from 20 to 30 minutes. The windows smoke had grown to about 19 m 40 s across these lanes and was cancelled at the cap twice with nothing failed (r31's #214, s224's #225; wolf-interp#215, #226). The cap is a budget, not a fix: trimming the smoke's targets stays open in #215.
The std beside the binary is the last std root (wolf-lang s204, ruling #29)
wolf-lang ruling #29 makes the compiler's default std root a std directory beside the wolf binary, where its release archive now ships wolf-std at a pin. lupin reads the same default beside its own binary, last: --std-root, then LUPIN_STD, then a std/ beside the entry (s166's fixture rule), then std/ beside lupin. A lupin unpacked beside such a wolf (the pairing's sibling layout) therefore reads the very tree the compiler reads, and use std.env runs there with nothing set, where it answered unsupported. A lupin with nothing beside it answers as before. lupin's own archive ships no std (the ruling owed is named in wolf-lang's s204 PR). The no-std refusal for a home-module method now names the std/ beside lupin as a third way to configure a root.
A repeated json object key is last-wins (wolf-lang s224, #124)
Ruling B22 (2026-09-11) made a repeated object name last-wins, and wolf-lang s224 writes it as [os.json.dup]: one member per name, at the position where the name first appears, holding its last value. lupin read the FIRST occurrence and counted every one, as the three compiler machines did, so json_get("{\"a\": 1, \"a\": 2}", "a") was 1 and json_len of that object 2; it is now 2 and 1. A path walks through the last container a repeated name holds, and a nested object that json_get re-renders carries the name once ({"a":1,"b":2,"a":3} renders {"a":3,"b":2}); the root's own text is unchanged. Written from the clause, not from wolf-lang's kernel (the track's independence rule). Red at 1c1f096: tests/json_dup_s224.rs, which holds wolf-lang's seven corpus/json/dup_*.lu rows (copied verbatim) to their headers; lupin 0.1.49 answers all seven first-wins. wolf-lang's json_dup_lanes gate pins 0.1.49 (f516a5f) by version and commit until a lupin release carries this.
Exec and env unset: the mirror (wolf-lang s225, #534)
wolf-lang's [os.proc.exec] and [os.env.unset] add two builtins.
env_unset(name)removes a name from this machine's environment overlay as a tombstone:env_getanswersmissingafter it, absent is not an error, andenv_set's names areinvalid. **A spawned child is now handed the host environment with the overlay applied** — everyenv_setset and everyenv_unsetremoved — where before a lupin child inherited the host's environment untouched, so anenv_setnever reached it (a parting nothing witnessed: lupin children are null-wired; the new witness asks through an exit status).os_exec(exe, argv, env, map)replaces this interpreter through std'sCommandExt::exec, which is safe code: argv[0] as given, the handed environment as the whole one, the machine's working directory, and descriptors 0..2 from the map (an unmapped one is this process's own). The program is resolved first — along the handed environment'sPATH, or/usr/bin:/bin— because std's exec moves descriptors before it tries the program; a refusal this machine can see coming never reaches it. Underconform-runthe buffered stdout is written before the exec and no record follows. A map that names a descriptor above 2, or closes one, is refused BY NAME, asos_spawn_fds's is. Witnesses:tests/exec_s225.rsover wolf-lang s225's rows and fixtures, byte for byte (the new image's pid is the pid the test started). Red at51cb491: neither name resolves (unsupported).
A wrapping product past i128 keeps its low bits (wolf-lang s220)
wolf-lang s220's four-machine integer sweep found that lupin multiplied every integer in i128 with checked_mul, so a wrapping[u64] product whose exact value passes 2^127 — (2^64 - 2)^2, or (2^64 - 2) * 16648325558172019491 — trapped overflow where its answer is the product's low 64 bits (4 and 3596837031075064250) on every compiler machine. A wrapping product now falls back to i128's wrapping product, whose low 128 bits reduce reads; a saturating one saturates by sign; a checked one still traps. Red at f516a5f (0.1.49): tests/int_truth_s220.rs, which holds all eighteen of wolf-lang s220's int_truth_* corpus rows (copied verbatim) to their headers; lupin answered the other seventeen already. wolf-lang's int_truth_lanes gate pins 0.1.48 and 0.1.49 by version and commit on this row until a lupin release carries the fix.
Ctrl-C and jobs: what lupin can mirror (wolf-lang s219, #622)
wolf-lang's [os.signal.disp], [os.signal.poll], [os.proc.job], [os.proc.status] and [os.term] add eleven builtins. lupin forbids unsafe and links no libc, so it serves what std reaches and refuses the rest by name ([os.term.note] R8):
- Served:
os_spawn_jobwith a process group and no terminal or defaults (std'sCommandExt::process_group; a map asos_spawn_fds's, 0..2),os_proc_pid,os_wait_status(the exit code, or-Nfor a death by signal N),os_signal_poll(0 for every well-formed set — this machine never listens, so nothing is queued), and every SHAPE row in the compiler's order: a bad map, group, tty or defaults isinvalid, a bad meaning setio, a bad terminal modeinvalid. - Refused by name:
os_signal_ignore/os_signal_defaultof a well-formed set (nosigaction),os_pgidand the fouros_term_*calls (nogetpgrp,tcgetpgrp,tcsetpgrp,tcgetattr,tcsetattr), and a job spawn that hands the child the terminal or sets dispositions back to default. Whether lupin may take a safe syscall crate for the terminal half is the maintainer's ruling (R8). tests/job_s219.rsruns wolf-lang'sjob_rows.luandstatus.lubyte for byte and asserts each refusal names its call.
The full stat record, lstat, readlink and the typed listing (s218, wolf-lang#625, #626)
lupin's half of wolf-lang s218, written from [os.fs.stat], [os.fs.readlink] and [os.fs.readdir]. fs_stat and fs_lstat answer the twenty-word record (kind with links, fifos, sockets and devices told apart; size; modified_ms; the have mask; mode, nlink, uid, gid, blocks, dev, ino, rdev; access, modification, change and birth times to the nanosecond) from one host call; fs_read_link the target's bytes, invalid on a non-link; fs_read_dir_entries the directory in the host's order, each entry its own kind and its name's bytes, a non-UTF-8 name included. Containment: fs_stat keeps the family's resolved check, so a link whose target leaves the served tree is refused by name; fs_lstat and fs_read_link check the parent only, since they follow nothing. No unsafe. Every row of wolf-lang's fs_stat_lanes gate agrees.
0.1.49 — 2026-10-08
THE FORTY-NINTH, the lupin half of wolf 0.2.26. Pin unchanged: 294d626d (wolf-lang v0.2.24). Fifty-three commits since 0.1.48, in six lanes: is74 (11, volatile and atomics), is75 (9, the CI slot budget), and the lupin halves of wolf-lang's s200 (5, the byte surface), s213 (13, the kernel papercuts, with r31's four), s215 (9, a child's descriptors) and s216 (6, copy region). Integrated by r31 one lane at a time. The 0.2.26 pairing drops the wolf-lang gate pins that waited on #185, #194, #200 and each lane's mirror.
- The walk at this release: 1002 files, 955 entries, 47 members; 760 reach
run, 721 match, 94 dynamic, 64 conservatism, 72 out of scope, 4 mismatch, every one filed. Two rows newly run and match (memory/packed_field_raw_read.lu,memory/raw_ub_misaligned_repr_c_field.lu): s213's struct pointee over is74's pointee class, which also answers wolf-interp#205. - Rulings now made that lanes implemented ahead of them: #50 (
-> never), #51 (!on integers), #56 (copy region), each A.
Volatile access and atomics on *T (is74, #185, #194)
kw07 (K3 = B) and kw11 (K5 = A) gave wolf read_volatile/write_volatile and nine raw-pointer atomics with Order as syntax; lupin answered unsupported on every row. On every row lupin now answers what the compiler's lanes answer (64 witnesses in tests/rulings_is74/, measured against wolf 0.2.24), the counts of tasks running atomics included.
- Volatile (
[mem.unsafe.volatile]).p.read_volatile()andp.write_volatile(v)on a*TwhoseTisu8…u64,i8…i64orbyte: one access of the width, on an allocation an ordinary one (P1, P3 asp[0]). Any other pointee is E1307 at the method; outsideunsafe, E1301. A misaligned address is the new §7 row L3 (clausemem.unsafe.volatile, O11). - Atomics (
[conc.mm.atomic.raw],[conc.mm.atomic.order],[conc.mm.fence]).atomic_load,_store,_swap,_add,_sub,_and,_or,_xor(the old value, wrapping at the width) andatomic_cas((T, bool));fence(o), a prelude name (W0304 when shadowed). Every order runs asseq_cstand is read as a mark, never evaluated. A pointee that is not a fixed-width integer is E1308; an order the operation does not admit, an operand that is not a mark, an unknown mark, andOrderas a value are E1309, each at the compiler's span; an atomic outsideunsafe, or a fence weaker thanseq_cst, is E1301. A misaligned atomic is row L4. The tier walk now reports the first E1301 in source order (it walked fns by name). - Tasks: every atomic acquires and releases its location's clock, so
atomic_counter.luprints400000 100000as the compiled tiers do; two atomics never race, and an atomic beside an unordered plain access does. The plain-increment twin still trapsrace. Atomics are not schedule points, so a spin-wait whose writer has not run does not terminate here (docs/approximation-contract.md§10.7).
CI: the slot budget (is75)
The org's free plan runs 20 jobs at once, five of them macOS, across every repo, and a full wolf-interp run is 17 jobs and about 21 slot-hours. The plan job in .github/workflows/ci.yml now decides the matrix by event: a push to trunk, the nightly, workflow_dispatch and a pull request labelled full-matrix run the full matrix as before; any other pull request runs ubuntu's three shards and ladder in full plus one windows and one macOS smoke (build, fmt, clippy, every test target but the heavy ones SMOKE_OMITS names, and the fast ladder rungs; a 20-minute cap, which r31 found at its edge: one windows smoke timed out at 20.2 minutes with nothing failed, wolf-interp#215). ci/test-shards.sh gains smoke-args; ci/assert-shard-coverage.sh checks the OSes SHARD_OSES names (all three when unset) and says which. CONTRIBUTING.md documents the budget and how to ask for the full matrix before a merge. The plant (a windows-only failing test in cli) went red in the full matrix (run 37549965195, windows shard 3) and was absent from the smoke, as designed; reverted.
The byte surface, mirrored (s200; wolf-lang#405, #411, #417, #407)
wolf-lang's s200 gives a program its standard streams as bytes, a bulk byte scan, a copy the host makes, and the host's error number beside the row. lupin answers each one as the compiler's three lanes do (tests/byte_surface.rs: wolf-lang's four corpus rows and two stdin fixtures, standard input a file, a pipe and an offset the test shares).
[os.fs.std], the byte calls.fs_read,fs_read_chunk,fs_writeandfs_write_chunkserve descriptors 0, 1 and 2, where they answeredio. A read of 0 reads the process's descriptor through a safe duplicate, so the offset is the one the process shares. A write to 1 or 2 goes into the machine's capture, the oneprintandeprintwrite, in program order. Unlike the offset calls, these four serve the standard streams on windows too: a read or a write needs noGetFileType.fs_closeon 0..2 staysio.[os.fs.copy],fs_copy_chunk(src, dst, max) -> int ! {eof, io}. One read and one write through this machine's buffer: the bytes and offsets the compiled rungs move. Both handles are checked first, thenmax;eofat the end.- **
[mem.list.bytes],bytes_find(xs, b, from) -> int ! {none}andbytes_count(xs, b) -> int.** The scalar definition, element for element.fromoutside0..lenisnone. [os.fs.error],os_error()andos_error_text(code). Each task's machine keeps the host's number for its last fallible fs call, 0 for a success or a failure decided before the host; the total predicates (fs_exists,fs_is_file,fs_is_dir) leave it alone. The text is std's rendering without its(os error N)suffix, the compiler's runtime word for word. The row mappers record the host error even where the row staysio(io_rowkeeps its uniform row).
The kernel papercuts, lupin's half (s213, #200; wolf-lang #572, #575, #577, #579)
wolf-lang s213 closes four gaps the PAX kernel worked around; lupin now answers each of its rows as the compiler's checked machine does (22 witnesses in tests/rulings_s213/, 19 of them the s213 corpus rows verbatim). Two of the four spellings — -> never and ! on an integer — were ruled by the maintainer on 2026-10-07 as implemented (rulings #50 and #51, A each).
- #579, a module item through its module's name (
[mem.static.4]).counter.COUNTreads, and insideunsafewrites, another module'svar; a qualifiedvaraccess outsideunsafeis E1301 at the qualified name. Module state is now keyed by its module, so a non-root module'svarwritten by its own fn before any read of it runs (it was "not a local place"), and two modules' items of one name are two items. - #577, a field of a raw element (
[mem.unsafe.raw.5]). A*Sto a#[repr(c)]struct carries the struct's C layout:p[i]steps bysize_of(S)(it stepped one byte), andp[i].f,(*p).fand nested paths read and store the field's bytes at its offset, a compound operator reading first. Row L4 is asked of the element atalign_of(S)(1 when packed). A whole#[repr(c)]element throughp[i]is refused by name (it read one byte). - #575,
!on an integer ([type.int.not], ruling #51 = A). The bitwise complement at the operand's width —-x - 1signed,max - xunsigned — never a trap; a byte widens toint; a complemented literal adopts an unsigned binding's type first (const M: u32 = !0xfff).!on a float or astris E0409 at the operand. - #572,
-> never([type.fn.never], ruling #50 = A).neverresolves as a type; a call to aneverfn needs nothing at run time. A-> neverbody holding areturn, or whose tail is()by shape, is E0401 at thereturnor the tail.
A child's descriptors, a pipe, the working directory, isatty (s215, pelt's H2)
wolf-lang s215 adds four host builtins ([os.proc.fds], [os.proc.pipe], [os.fs.chdir], [os.fs.isatty]); lupin mirrors them, witnessed by the compiler's own rows and fixtures in tests/proc_fd_s215.rs.
os_pipe() -> (int, int) ! {io}: a pipe's read and write ends as two handles of the fs table (3 and up), close-on-exec; every handle call serves them.os_chdir(str) -> () ! {denied, io, not_found}moves this machine's working directory — never the process's, which runs many programs at once. Relative fs and unix-socket paths,os_cwdand every spawn follow it; containment stays the served tree, so a directory outside it is declined by name, as an open there is.os_isatty(int) -> bool ! {io}: 0..2 ask the process's streams, 3 and up the table; closed or forged isio.- **`os_spawn_fds(str, List[str], List[int]) -> int ! {denied, invalid, io, not_found, unsupported}`** for child descriptors 0, 1 and 2 from the table or the standard streams, in the clause's order of refusal (shape
invalid, windowsunsupported, sourcesio, then the program). A target above 2 or a close (-1) is refused by name: placing or closing a child's descriptor between fork and exec needsunsafe, which this crate forbids, and stable Rust 1.97 has no safe call for it. An unmapped 0..2 keeps this machine's spawn posture, the null device.
copy region { … } (wolf-lang s216, #612)
wolf-lang's [mem.region.copyout]: a region block whose value is copied into the region the block was entered from before the block's region is freed, so a long-running loop keeps each turn's result and frees the rest. lupin read the spelling as a plain copy of a plain block and trapped region-fault at the } on every such program (0.1.47, 0.1.48).
- A third sugar exit beside free and freeze. On the closing brace's edge only, after the enclosing region is current again, the value is copied and re-homed there: every
strrebuilt (charged to that region), lists and structs re-allocated, tuples, maps and error/enum payloads rebuilt. Then the block's region is freed. Areturn,?orbreakleaving the block copies nothing, as before. - A fn value, closure, region, pool, cell, raw pointer or conc handle has no copy independent of the region:
region-fault, the dynamic half of the compiler's E1010 at the block. tests/region_copyout.rscarries wolf-lang's rows until the re-pin brings them into the corpus walk.
0.1.48 — 2026-10-06
THE FORTY-EIGHTH (is72 and is73), the lupin half of wolf 0.2.25. Pin: 294d626d (wolf-lang v0.2.24, the TAG), two releases forward of 0.1.47's 8e36bc1a (v0.2.22). Thirty-four commits since 0.1.47, in two lanes: is72 (14, the place trace) and is73 (20, the re-pin blockers). lupin now carries the spec and corpus of the compiler it pairs with, and the 0.2.25 pairing drops the sixteen wolf-lang gate pins that waited on #190 and #188.
The re-pin: 8e36bc1a → 294d626d (r30)
- Census. Corpus files 947 → 1002 (55 new, every one an entry; none leaves), entries 900 → 955, members 47. Anchors 569 → 595: 26 added (
abi.interrupt,abi.layout.*,abi.link*,conc.mm.atomic.*,conc.mm.fence,mem.prov.device,mem.static*,mem.unsafe.raw.4,mem.unsafe.volatile*), none dropped. s209's L4 clause (mem.unsafe.raw.4), which 0.1.47 answered ahead of its anchor, is in the vendored registry now. - The walk. 749 entries reach
run: 704 match, 94 are dynamic counterparts, 64 conservatism, 89 out of scope, 4 mismatch, every one filed. The bundle: 1042 programs, 995 records, 313 anchors covered (the ratchet 290 → 313). - DIV-2026-028, new (wolf-lang#603). kw11's
conc/atomic_race_plain.luischeck: passat phasewir: four tasks race on one non-atomic word, UB that the row "promises only compiles". This machine detects the race and trapsrace, which[conc.mm.race.3]permits, andpasscannot spell it. Filed as a known divergence; neither machine changes. - Not mirrored yet, by name. kw11's other nine
conc/atomic_*rows (raw-pointer atomics andfence, wolf-interp#194) decline at resolve and count out of scope;conc/atomic_outside_unsafe.luand kw07'smemory/volatile_outside_unsafe.lu(#185) pin E1301 on a surface this machine does not resolve, and the conformance gate names both. - is73's two rows ruled ahead of the old pin (
grammar/attr_repr_unimplemented.lu,grammar/attr_section.lu) are re-spelled at this pin; both waivers retired by name.
The re-pin blockers: module state, the link, exact layout (is73, #190, #188, #197)
r29 found lupin could not re-pin on wolf v0.2.23: kw09's memory/static_init_cycle.lu overflowed the interpreter's stack (SIGABRT, no record) and aborted the corpus walk, and kw08's and kw09's rows parted eight times. On every row lupin now answers what the compiler's three lanes answer (90 witnesses in tests/rulings_is73/, measured against wolf 0.2.23), or refuses by name where the clause says the machines do.
- #190, module state (
[mem.static]). A module initializer is comptime: one that reads a modulevar, or needs its own value, is E0705 at the reference, as many times as the compiler's engine meets it (statics::init_check). A cycle the static walk cannot see — through a fn's body — is declined by name at run time; no initializer graph recurses into the stack's end (Machine::initialize, a 512-item rail). Every read and write of a modulevaroutsideunsafeis E1301. Module state of a type that is not static data (aList, a struct, astrvar) is refused by name, before the E1301, somemory/read_param_escape_static.luis out of scope rather than a second code beside its pinned E1002. - #190, the link (
[abi.link.extern],[abi.link.section]).extern "c" let NAME: *Tparses; a non-pointer type, an initializer,var/const, or the form inside a body is E0821 at the compiler's span, another ABI string E0818; a program that declares one is refused by name (a link-time symbol).#[section("…")]on a fn or a modulelet/varleaves the closed set's refusals; a program that places one is refused by name (section placement). - #188, exact layout (
[abi.layout.packed],[abi.layout.align],[abi.layout.query]).#[repr(c, packed)]and#[repr(c, align(N))]are in the closed set; a representation that cannot be laid out is E0820 (one per struct, the compiler's spans).size_of,align_ofandoffset_of(T, field)answer the clause's C layout at comptime for scalars and#[repr(c)]structs (src/layout.rs), E0708 for a native layout and E0403 for a field the struct lacks. Amutlend of a packed field, or areadlend of an aggregate one, is E0819. The whole-aggregate raw store stays refused by name. - #197, the fuzz record.
fuzz_smokeheld a rejection to one diagnostic, a contract older than is70's every-E0817 records and[proto.record.first]; seed0x1001f43a57eed01at the v0.2.23 pin found it. The harness checks[proto.record.first]'s record now, and the seed's mutant is kept (tests/fuzz_seeds/). - At the old pin (v0.2.22) two rows were ruled ahead of it until the re-pin re-spelled them:
grammar/attr_repr_unimplemented.luandgrammar/attr_section.lu. The re-pin to v0.2.24 above carries both. is73 proved the walk on an overlay of v0.2.23 (8edac3ee): 982 files, 935 entries, 3 mismatches, each a standing DIV entry; r30 re-measured that overlay byte for byte before choosing v0.2.24.
The place trace: lupin --trace-places (is72)
The book's chapter 7 asked what a plain = on a str field does (it copies: str is Copy). lupin now shows it. --trace-places writes one JSON line after every statement of every function activation, and after a body's tail, naming the state of every place in the executing frame: each binding and every struct field, tuple element, and the list elements and map values the program addressed.
- Spelling.
lupin --trace-places FILEand `lupin run --trace-places FILEwrite the trace to stderr;--trace-places=PATH` writes it to PATH. Stdout is never used.--jsonrefuses the flag, and so does any position before a subcommand. - Schema version 1 (
"trace":1, manual chapter 6,docs/manual/06-place-trace.md). A line carriestask,fn,depth,at("line:col"),tail:trueafter a body's tail,trap:<kind>when the statement trapped, thenplacesandevents. A place haspath(p.lead,t[0],xs[2],m["k"]),state(live,moved,uninit),type;valuefor a live scalar orstr;reiniton a place written after a move;by(take,move,plain,match,freeze,mut) andaton a moved one. An event is amove,reinitorcopy(byplainorcopy, andtowhen the value went into alet). A container lists at most 16 addressed elements, lowest first, withlenandelided(memory/byte_list_ledger.luwrote 65,536 per line before the cap). - The trace and the trap cannot disagree. A place's state is read from
SlotState, the field theuse-after-movetrap checks; which spelling moved it, and what was copied, live in side records that exist only while the trace is on. - The trace changes nothing.
ci/place-trace-identity.shruns every vendored corpus file through the release binary with the flag off and on and byte-compares stdout, stderr (trace lines aside) and exit, on every CI ladder (ubuntu, macOS, windows). Nine goldens (tests/place_trace/:examples/pack.lu, the book's ch07 §7.2 tree and its moved leaf, take, copy, plain move, elements, granules, the 16-element cap). A planted break (aCopy-shaped initializer moving under the flag) was seen red in CI and reverted.
0.1.47 — 2026-10-05
THE FORTY-SEVENTH (s209's lupin half), the lupin half of wolf 0.2.24. Pin: 8e36bc1a (wolf-lang v0.2.22, the TAG). This is the same pin as 0.1.46: there is no re-pin in this release. Eleven commits since 0.1.46, all in one lane. lupin now answers ruling #36's row L4 the way the checked machine does, so the 0.2.24 pairing drops the six L4 pins s209 set in raw_align_lanes.rs.
The re-pin on v0.2.23 (8edac3ee) waits (r29, measured at the unshipped c3f1dab, the vendor trees byte-equal to the tag's; the orchestrator ruled to ship without it). Its census would move corpus files 947 → 982 and anchors 569 → 586 (17 added, none dropped). Three things block it:
- #190.
memory/static_init_cycle.lu(let A: int = B + 1overlet B: int = A * 2) overflows lupin's stack. The crash aborts the corpus walk itself.extern "c" lethas no grammar here, somembrane/extern_let_{image,not_ptr}.lufail E0201 at parse. - #188. Four layout rows answer E0817 where the corpus runs them:
comptime/layout_query_repr_c.lu,memory/packed_fields_at_offset_of.luandmemory/raw_repr_{align,packed}_layout.lu. Two answer E0817 where the corpus pins E0820 (grammar/attr_repr_unlayable.lu) or E0819 (memory/packed_field_lend.lu). - #196. With those rows in the corpus,
fuzz_smoke's mutated-corpus test finds afailrecord carrying two diagnostics (seed0x1001f43a57eed01).
Every other new row matched or was out of scope; kw07's volatile rows (#185) do not mismatch. L4's own clause, [mem.unsafe.raw.4], is not at v0.2.23 either: it lands at wolf-lang a3465f87, after the tag, so a re-pin on v0.2.23 still would not carry it.
Row L4: a misaligned ordinary raw access (s209, ruling #36 = A, #192)
- lupin defined what the compiled tiers assume away. A
*u16,*u32or*u64read or write at an address that is not a multiple of its width (p.with_addr(p.addr() + 1), thenq[0]) ran toexit(0)here, while wolf's release tier emitsload i32 … align 4. It is now §7 row L4 (x-ub-rowL4,x-ub-clausemem.unsafe.raw.4; licenses O12), asked inraw_load/raw_storebefore the provenance check and only for a pointer with an allocation; a pointer no allocation owns keeps L2. Wolf-lang s209 lands the clause and the checked machine's half; itsraw_align_lanes.rspins 0.1.46 as pre-mirror. tests/ub/l4_misaligned.luand its twinok/l4_aligned_ok.lu; the enumeration has twelve rows. The pin does not move (the clause's anchor is not at v0.2.23 either; see above).
0.1.46 — 2026-10-04
THE FORTY-SIXTH (s208's lupin half, is70, and r28's re-pin), the lupin half of wolf 0.2.23. Pin: 8e36bc1a (wolf-lang v0.2.22 — the TAG), one release forward of 0.1.45's dfcc2f13 (v0.2.21). Fifty-two commits since 0.1.45 in two lanes, then the re-pin. It closes the gaps the PAX and compiler lanes filed against lupin (#174, #175, #178, #179, #180, #181, #182, #184): on every row they pinned, lupin now answers what the compiler answers, so the 0.2.23 pairing drops 25 of the 26 pins 0.2.22 kept and kw06's 7. **One place lupin was looser than the compiler is closed: *p outside unsafe is E1301.** And it mirrors ruling #34: an else-less if is () whatever its taken block answered.
The re-pin (r28; 60b93e3). vendor/upstream/{spec,corpus} and the upstream gitlink move to 8e36bc1a, an ancestor of the released line (merge-base --is-ancestor dfcc2f13 8e36bc1a holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 892 → 947 (55 new, none leaves), entries 846 → 900, members 46 → 47 (kw02's membrane/geo/geo.lu; 09779e8, 87d8792); anchors 546 → 569 (23 added, none dropped, the key sets diffed both ways); distinct conforms: anchors 379 → 392; the coverage ratchet 276 → 290; the bundle 930/884 → 985/938 programs and records (ff55678). Thirty-four of the fifty-four new entries reach run (3037ef1): twenty-three match their check: and eleven are the dynamic counterparts of s207's E1010 rows (region-fault at run time). kw03's three narrowing-cast traps gain fault snapshots (1bdb791). The three rows is70 ruled ahead of the pin retire (0f2e540, 3037ef1, b0fbceb): memory/unsafe_sig.lu's peek is pub now (be13b445), so it fails E1302 at resolve as pinned and leaves the run ledger; comptime.lu drops its #[noalloc] (e951afbb) and resolves; ffi.lu carries an aarch64 twin of its asm, so every host keeps one and declines it by name, and the CI ladder's check has one answer again. The walk's mismatch count is 3 on every host: DIV-2026-019 and DIV-2026-027's two rows. s203's first_* rows pin E0107 and E0207, so both numbers leave UNPINNED_CODES (0cc24c1; the not-UTF-8 and nesting-rail sites ride them); E0817 and E0818 are resolve-rung codes in tests/conformance.rs; and a phase: lex row may carry a lexical fault after the parse refusal it pins (ruling #28; 0f2e540, fa60f85).
An else-less if is () (s208, ruling #34 = A; #179's if half; 3ec58bc). Whatever its taken block answered, a raise included, the value of an if with no else is (): rows/unit_discard_if_value.lu prints () (). s208 left no changelog text; this paragraph is written from its commits (54d6c3f..8d82031, PR #186).
is70 — the gaps the PAX lanes filed. lupin now answers what the compiler at wolf-lang 50830027 answers on the rows the KWC and compiler lanes pinned against it. Every row is a witness under tests/rulings_is70/, measured on the compiler's three lanes and seen red at trunk first. The vendored pin does not move (still dfcc2f13, v0.2.21); three of its rows are ruled ahead of the pin until the 0.2.22 re-pin carries their re-spelling (below).
- **Prefix
*poutsideunsafeis E1301** (#184 item 3, the one place lupin was looser than the compiler).*p = v,*p op= vand a read of*ptake the ringp[0]takes, at the*p. - The provenance methods and the integer side of the casts (#184,
[mem.prov.expose], kw06).expose,with_addrandwith_exposedjoinaddrandis_null. A pointer made from an integer no allocation owns keeps its address:N as *TwidensNby its own signedness and never traps;*T as Nis the address's bits for a 64-bitNand the address asuintotherwise,overflowoutsideN.is_nullcompares the address with zero. (addrstill exposes the tag. The clause says it does not, but every machine answers the witness that would tell them apart the same way, so the reading waits for one.) conform-run --target(#182,[abi.target]). The host's own triple runs.x86_64-unknown-noneisunsupported, naming "the freestanding target x86_64-unknown-none", and nothing runs. Any other triple is a tool error naming both.- A
returnout of a region block carrying a value the block frees trapsregion-fault, the same fault the block's own value gets (#178,[mem.region.escape]). errdeferin a function that cannot fail is E0607 at the keyword (#179's second half,[err.errdefer]).rows/negative/errdefer_infallible.luleaves the run ledger, refused as pinned. Theifhalf shipped with s208 (3ec58bc), which left no changelog text.- E0602 at an annotated binding (#180,
[err.rows]). `let a: T ! {row} = eis refused atewhene`'s known row holds a tag the declaration cannot; a!Tannotation admits no row. - The C membrane (#181,
[mem.unsafe.sig], kw02). E1302 applies only topub/pub(pkg)fn items and methods. A module-private fn, anexport fnand anextern "c" fnmay carry*T. A raw pointer argument is a copy of the pointer: no retag, no protector. A call into a hand-declaredextern "c" fnoutsideunsafeis E1301, except in acomptimefn, where E0701 is the answer. Insideunsafeit is still refused by name. - The closed attribute set,
cfg(target), the one ABI string (#174,[gram.item.attr.set],[gram.item.attr.cfg],[abi.c.seams]). An attribute nothing reads in its position is E0817, at the attr or at each badrepritem, and every one is reported.#[cfg(target = "S")]is evaluated against the host triple or its architecture: a node another target gates is dropped after parsing, and acfgthis machine cannot read keeps its node and is E0817.extern "S"withSother than"c"is E0818. - The first diagnostic (#175,
[proto.record.first], ruling #28). The parser runs over the lexer's recovered stream, and the earliest diagnostic of the two tiers is the record's (the lexer's when they tie). A group that a later closer of another kind leaves open is E0202 at its opener. A run of stray characters is one E0107. An unterminated string's E0102 spans from its quote. "Expected a pattern" is E0207. A top-level token that starts no declaration is E0203. A declaration keyword inside an error row is E0202 at the row's{. - #187 is not changed. macOS
read_aton a pipe's write end still answersio. The compiler's resolution, wolf-lang#566, is unruled. - Ruled ahead of the pin, each re-spelled in wolf 0.2.22 and not in the vendored 0.2.21.
memory/unsafe_sig.luruns: itspeekis private, and be13b445 makes itpub.comptime.luis E0817 on its#[noalloc], which e951afbb drops. On an aarch64 host,ffi.lu's x86_64-gated asm is dropped and the row runs toexit(1), until the same commit's aarch64 twin arrives.tests/conformance.rsandtests/run_corpus.rswaive them by name, and each waiver goes red when its row stops parting.
0.1.45 — 2026-10-03
THE FORTY-FIFTH (is68, is69, s199's lupin half, and r27's re-pin), the lupin half of wolf 0.2.22. Pin: dfcc2f13 (wolf-lang v0.2.21 — the TAG), one release forward of 0.1.44's cdde128a (v0.2.20). Forty-five commits since 0.1.44 in three lanes, then the re-pin. It heals the one parting wolf 0.2.21 shipped pinned by version: a -> !T function's row is what its body can raise, so rows/eu_bind_empty_row_handled.lu prints 43 42 42 here as on every compiler machine (#176). It fixes lupin's two P0s from the org triage: a view that outlives its region is a region-fault and is never read from freed bytes (#126), and a unit context's value is () (#103 row 1). And it mirrors wolf 0.2.22's fs_seek, fs_tell and fs_read_at and the standard streams (wolf-lang#426, #424): the first handle is 3.
The re-pin (r27; 31d8dfa). vendor/upstream/{spec,corpus} and the upstream gitlink move to dfcc2f13, an ancestor of the released line (merge-base --is-ancestor cdde128a dfcc2f13 holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 861 → 892 (31 new, none leaves), entries 816 → 846, members 45 → 46 (s202's memory/versioned_loop_cross_module/ring/ring.lu; b8fa2a9, fd90571); anchors 544 → 546 (type.row.defer, type.row.match; none dropped, the key sets diffed both ways); distinct conforms: anchors 376 → 379; the coverage ratchet 273 → 276, the two new anchors and conf.trap.assert; the bundle 899/854 → 930/884 programs and records (3dc6dfa). No check: line moves; faults/index_origin_min_overflow.lu annotates its binding, so it now traps at the 1-origin shift it is about rather than at the literal (e0d1b96). Twenty-four of the thirty new entries reach run (79c0d45): nineteen match their check:, three are conservatism rows (s202's typecheck/numlit_binding_literal*.lu pin E0415 at the binding; this machine traps overflow at the literal) and two are filed. The six that do not reach run are is67's refusals, matched at resolve: tests/conformance.rs now reads E0801 and E0816 under [type.row.match] and E0611 under [type.row.defer] as resolve-rung codes, keyed on the clause (2c2cba3, fd414b2).
DIV-2026-027, filed (c2bba7c; wolffe-lang/wolf-lang#556). faults/assert_msg_effect_fails.lu and faults/assert_msg_name_fails.lu trap assert here as pinned. This machine also renders the message as one stdout line before the trap, which [conf.trap.assert] permits ("until then implementations may drop the message"). The rows' stdout= admits only the dropped rendering, so both are a MISMATCH and both are now waived by name. lupin 0.1.44 did the same thing. The walk's mismatch count is 3: DIV-2026-019 and these two.
A -> !T function's row is what its body can raise (is69, wolffe-lang/wolf-interp#176). 01-grammar.md gives -> !T an *inferred private row*, and the compiler seals it per module (s15): the least fixpoint, over the module's inferred functions, of the tags a body raises at its tail or a return (return Neg, Io(3)), the row of a fallible value that flows out (a call with a row, a Map index, a local bound to one) and the row of every ? (a closure's ? is its own). 0.1.44's [type.row.match] reader read every such row as the open row {..}, so a match over the result with no _ was E0801 whatever the body raised — wolf-lang's rows/eu_bind_empty_row_handled.lu (fn f() -> !int { 42 }, match a { v => v + 1 }) answered fail(E0801) where every other machine prints 43 42 42, and r26 pinned 0.1.44's answer by version in fallible_bind_empty_row_lanes.rs. src/rowinfer.rs is the inference, cached on the program; rowmatch::callee_row reads it for an own or a used fn, so sema's E0801 judge, the evaluator's two-half dispatch and the lint all see the body's row, and an uncovered tag is named ("does not cover Neg") as the compiler names it. A binding annotated !T takes its initializer's row. A body this machine cannot read (a method call, a closure call or a spawn whose row would flow out) keeps {..}, 0.1.44's reading. Twenty witnesses under tests/rulings_is69/: nineteen red at 8f08e49 (on 0.1.44's code), green at 675ccea, beside the compiler on every one; one control unchanged.
A view carries its receiver's region (is68, wolffe-lang/wolf-interp#126, the mirror of wolf-lang#392). A [mem.str.view] product — a slice, a trim, a split/words/lines piece, a view of a view — carries the home of the str it views (1a1c14f). A view that outlives the region its receiver was built in is the clause's trap(region-fault) (mem.region.intra.2), never a read of freed bytes. 0.1.44 ran nineteen such shapes to exit(0) from freed bytes; each is E1010 on the compiler. memory/region_str_view_return.lu is now a dynamic counterpart, not a conservatism row (d11cafe).
A unit context's value is () (is68, #103 row 1). An else-less if and a unit fn body yield () (5314dfb). Where this machine can type the tail, a typed tail in a unit context is refused E0401 at wolf's code and span, in a late tier pass (db9ac2a). Rows 2 and 4's static halves stay owed on #103. Found beside it and filed for the compiler: wolf-lang#541 (checked propagates a discarded W0601 row that native and release drop).
trim and its family take no argument (is68, #125). A [mem.str.ws] call that passes an argument is E0402 at resolve where the receiver is visibly a str (a4862f7). Where the receiver cannot be typed, the evaluator declines it by name and never trims a cutset (a77c4a4). trim_cutset_refused.lu is a match at resolve.
str and bool are declared scalars (is68, #138). They join the declared-scalar lattice in a late wide pass (a75adcc), so let s: str = side from an int is E0401 in the root and in a sibling module alike. The sibling's diagnostic names its file. lint_is61's t01 parting heals.
A nested fn's parameter modes are a module fn's (is68, #169). A call to a nested fn is held to its declared modes, E1007 at resolve (3ffdfd7). A moded nested fn runs with the module fn's convention (7dc33ba), so nested_fn_mut_param.lu matches and nested_fn_mut_moveout.lu traps use-after-move. r26's NESTED_MODED_FN_DECLINED waiver went red by name and was retired (b6ea927).
Seek, tell and positional read; the standard streams (s199's lupin half, wolffe-lang/wolf-interp#171, the mirror of wolf-lang#426 and #424). fs_seek, fs_tell and fs_read_at resolve, dispatch and declare the rows [os.host.sigs] gives them, including the new tag unseekable (ESPIPE) (99c35ca, ff53642). Descriptors 0, 1 and 2 are the standard streams for fs_fstat and the three offset calls ([os.fs.std]); this machine duplicates them with no unsafe, and close and read stay io there. The first handle is 3 (it was 1). On windows the four calls on 0..2 are declined by name (a7304a5): GetFileType is the only honest classifier there and the crate forbids unsafe. tests/fs_offset.rs passes 0 of 4 before the mirror and 4 of 4 after.
0.1.44 — 2026-10-02
THE FORTY-FOURTH (is65, is67, and r26's re-pin), the lupin half of wolf 0.2.21. Pin: cdde128a (wolf-lang v0.2.20 — the TAG), one release forward of 0.1.43's c2401f05 (v0.2.19). Twenty-three commits since 0.1.43 in two lanes, then the re-pin. It carries every lupin mirror wolf 0.2.21's gates pin 0.1.43's answers on by version: a match over a fallible value has two halves (the maintainer's ruling #21, [type.row.match]; wolffe-lang/wolf-lang#497) and a ? under a defer or errdefer is refused, E0611 (ruling #19, [type.row.defer]; wolffe-lang/wolf-lang#498); with a flow out of an argument list withdrawing the call's claims and protectors (#163).
The re-pin (r26; eb00a30). vendor/upstream/{spec,corpus} and the upstream gitlink move to cdde128a, an ancestor of the released line (merge-base --is-ancestor c2401f05 cdde128a holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 778 → 861 (83 new, every one an entry, none leaves), entries 733 → 816, members 45 → 45 (76469fa, bfb517d); anchors 542 → 544 (mem.tier0.excl.4, type.row.else; none dropped, the key sets diffed both ways); distinct conforms: anchors 374 → 376; the coverage ratchet 271 → 273, the two new anchors; the bundle 816/771 → 899/854 programs and records (189f141). Two check: lines move, memory/elem_dyn_read_after_mut.lu (run "3 2", eg03) and memory/mut_read_overlap.lu (run, s186), both fail(E1002) before; this machine already ran each, so both turn from conservatism rows into matches. Eighty of the eighty-three new entries reach run (69b3e18): forty-five match their check:, twenty-eight are dynamic counterparts of the compiler's E1002 (trap(exclusivity), twenty-three) or E1001 (trap(use-after-move), five), and seven are conservatism rows (three run-time offsets the compiler cannot prove distinct, two shorthand initializers, two nested read parameters). The three that do not are the nested fns that declare a parameter mode (memory/nested_fn_mut_{param,omitted,moveout}.lu), which this machine declines at resolve under #38's scoped v1; nested_fn_mut_omitted.lu pins E1007, which the resolve rung owns, so tests/conformance.rs waives it by name until the mirror lands (6b7ac41, #169). No new divergence; DIV-2026-019 is still the one filed.
A flow out of an argument list withdraws the call's claims and protectors (is65: wolffe-lang/wolf-interp#163). A ?, a return, a break, a continue or a trap that leaves a call's argument list never enters the call; [mem.tier0.excl.4] has a mut claim take effect at entry and [mem.prov.tag] protects the parameter-entry tag for the call's extent, and with no entry there is no extent. The list is abandoned — every access it held released, every protector it minted withdrawn, a (mut …) receiver's included — and the signal leaves as it left the argument. Through 0.1.43 the protected Reserved child stayed in the tree, so the next write through the place was a foreign write to a protected tag: put(mut xs, v(ok)?) and (mut xs).push(v(ok)?) answered ub(mem.ub) (mem.prov.state, P1) at the caller's next push, and the continue forms answered it inside the loop, where wolf 0.2.19 prints [1, 5, 3] and [1, 1, 3] on checked, native and release; the claim itself stayed held until its scope popped, which the REPL's top level never does ([repl.trap.alive]: trap(exclusivity) on every line after a trap out of a list). Eighteen witnesses under tests/rulings_is65/ — each exit kind, a nested call inside the argument in three shapes, a read lend, a scalar, two muts, every receiver form, and the REPL after a trap — each red at 3e7f230 and green at dfebf6b; six controls (a trap in a program, a take, a field, an element, an impl and a view-set receiver) unchanged. No new divergence; the ratchet holds at 271.
A match over a fallible value has two halves (is67, lupin's half of the maintainer's ruling #21 — wolffe-lang/wolf-lang#497, the clause [type.row.match] s197 writes). The scrutinee has type T ! {row}; a row arm names a tag of the row (binding its payload: Io(e) => …), a value arm is any pattern over T; an identifier that names a tag of the scrutinee's row is a row arm, anything else a value pattern; _ covers what is left on both halves; the row is consumed. A row value is tried against the row arms only and any other value against the value arms only, so match look(m, "a") { none => -1, v => v } prints 5 where 0.1.43 printed -1 (the row arm's name BOUND the value, the first-arm disease on the value half: eleven silent wrong answers under tests/rulings_is67/). What makes a match a row match is what a signature or a binding spells (src/rowmatch.rs, one reader for the evaluator, the lint and sema): a call to a module fn — own or used — by its declared return row, a builtin with a declared row, a Map index (V ! {none}, [mem.map.absent]), a local bound by such an initializer or by a T ! {row} annotation; a scrutinee the reader cannot name keeps the old dispatch unchanged. A non-exhaustive row match is E0801 at the resolve rung naming the missing tag, the missing variant or literal, or the uncovered value half ([proto.cmp.rung] makes the resolve-rung emission of the checker's code agreement, as E0805); a guarded arm counts for nothing; a tag that is also a constructor name reachable from T is E0816 (s197's code, wolf-lang PR #510), whether or not an arm spells it; an or-pattern mixing the halves or an @ at the top of an arm is refused by name before anything runs. The lint reads a row arm as no binder, and E0802 says nothing about a row match. else |e| match e { … } is unchanged. Twenty-one witnesses red at c08285f, green at 79af9df, five controls green on both sides.
A ? inside a defer or errdefer is refused (is67, lupin's half of ruling #19 — wolffe-lang/wolf-lang#498, s196's [type.row.defer] and E0611). The deferred expression runs while the function is already leaving, so the error has nowhere honest to go: the shape is fail(E0611) at the resolve rung, at the ? expression, reading the whole deferred expression (a call argument, an interpolation hole, a binding, a block); a ? inside a closure defined under the defer is that closure's own and runs. 0.1.43 ran the ? and made the row the function's result (… 9); wolf 0.2.20's native and release overflow the compiler's stack on the same programs.
0.1.43 — 2026-10-01
THE FORTY-THIRD (is61, is63, is62, and r25's re-pin), the lupin half of wolf 0.2.20. Pin: c2401f05 (wolf-lang v0.2.19 — the TAG), one release forward of 0.1.42's ec56a08f (v0.2.18). Forty-nine commits since 0.1.42 in three lanes, then the re-pin. It carries every lupin mirror wolf 0.2.20's gates pin 0.1.42's answers on by version: two-phase arguments (the maintainer's ruling #17, [mem.tier0.excl.4]; wolffe-lang/wolf-lang#476), the closure lent into its own call (#160), each operand once under a slice or a receiver (#157, #162), the field shorthand as its longhand (#159, wolffe-lang/wolf-lang#486), a mut receiver claimed while its own arguments run (#164, wolffe-lang/wolf-lang#487) and a view-set receiver's write-back (wolffe-lang/wolf-lang#494); with the lint's W1002 beside a moved-out mut parameter (#155), which retires r24's waiver.
The re-pin (r25; 21413a5). vendor/upstream/{spec,corpus} and the upstream gitlink move to c2401f05, an ancestor of the released line (merge-base --is-ancestor ec56a08f c2401f05 holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 747 → 778 (31 new, every one an entry, none leaves), entries 702 → 733, members 45 → 45 (75c05e4, 28fdb36); anchors 542 → 542, the same key set diffed both ways; the coverage ratchet 268 → 271 (mem.dyn.unsize, mem.iter.excl, mem.tier0.borrow, each cited first by a new memory/elem_* row); the bundle 785/740 → 816/771 programs and records (41be529). One check: line moves: memory/mut_elem_excl.lu, fail(E1002) → run "2" (eg02), which this machine already answered, so it turns from a conservatism row into a match. Thirty of the thirty-one new entries reach run (7da5709): seventeen match their check:, eight are dynamic counterparts of the compiler's E1001/E1002 (the six elem_whole_read_*_after_move rows trap use-after-move, elem_claim_whole_read and elem_prefix_mut_pair trap exclusivity), and five are conservatism rows (elem_dyn_*: a run-time index the compiler cannot prove distinct, whose value this machine sees); elem_header_methods_after_move_pool.lu is out of scope (Pool). No new divergence; DIV-2026-019 is still the one filed.
Every operand once, the shorthand is the longhand, a receiver's own argument (is62: wolffe-lang/wolf-interp#157, #162, #159, #164 and the lupin halves of wolffe-lang/wolf-lang#487 and #494).
- A slice of an indexed element runs each index once (
9fb80c8, #157).g[gi()][lo()..hi()]rangitwice bound or in a hole and three times as a member base: the place lookup ran the base's index before it learned the bracket was a slice, and the caller then evaluated the whole expression again. A bracket that cannot be a place is now refused before its base runs. The same held forcount(),is_empty(), areadargument, every index of a deeper base, a field's or astr's element, and open, inclusive or^endpoints ([mem.model.order]). - A flow out of a receiver's index leaves once (
d6a1cb0, #162).rows[idx()?].count(),(mut rows[idx()?]).push(9),ss[idx()?].bytes().len,ss[idx()?].lenranidxtwice when its?propagated: the receiver's and the member base's place lookup swallowed the flow as "not a place" and evaluated the base again. Only "not a place" falls back now;?,return,break,continueand a trap leave from the operand. - The field shorthand is the longhand (
301fdef, #159, s190's reading of wolffe-lang/wolf-lang#486).W { xs }is parsed asW { xs: xs }, so it moves a non-Copyvalue (through 0.1.42 it copied and printed1 2), a later use trapsuse-after-move, a closure capturing through it borrows (exclusivitywith W1102), a module-levelletnames it, and every analysis reads it —lint_is61'st16_struct_shorthandnow answers its longhand'strap(use-after-move)with no W1002, as wolf trunk refuses it E1001. - A
(mut …)receiver is claimed while its own arguments run (ee0cbfc, wolffe-lang/wolf-lang#487).[mem.tier0.excl.4]: a receiver'smutclaim is two-phase, the receiver the first argument.(mut xs).push({ xs = [9]; 5 })answeredub(mem.ub); it trapsexclusivityat the write, as does an element or field write, a move, a re-claim one call down or as another receiver, a lend into the same call ((mut c).absorb(c)) and a closure lend — some of which ran through 0.1.42 (3 9,3,2). A read of the receiver in its own argument ((mut xs).push(xs.len),total(xs),(mut c).add(c.n)) runs. - All fourteen of s192's receiver rows trap (wolffe-lang/wolf-interp#164, by the same claim): through 0.1.42 eight answered
ub(mem.ub)and six ran —(mut a).absorb(mut a),(mut a).absorb(a), a closure lend, the container of an element receiver or the struct of a field receiver replaced, a view-set field written. - A view-set receiver holds its view set only (
da9aa91, wolffe-lang/wolf-lang#494).fn set_x(mut self.{x}, …)claimsp.x, notp, while its arguments run, and writes back only the view-set fields it changed, so(mut p).set_x({ p.z = 9; p.z })prints10 9([mem.tier0.excl.3],[mem.tier0.excl.4]); through 0.1.42 the whole write-back lost the argument's write (10 3). A write to a view-set field in the argument still traps. - 53 witnesses in
tests/rulings_is62/; the shorthand rows run beside their longhand twins.
The move is the write (is61, wolffe-lang/wolf-interp#155).
- W1002 stands down where the body moves a
mutparameter out (62cfe67). The compiler refuses a move out of amutparameter that reaches a return (E1001,[mem.tier0.mode.mut], wolffe-lang/wolf-lang#464) and drops its "never written" beside it; this machine traps at run and kept saying W1002. The lint now reads the same evidence: a move out of the parameter, a field, an element or a map value —move e, a non-Copyplace in value position, atakestore, aMapvalue read out underelse/?— that reaches a return with no later access of an overlapping place is the parameter's write. A move used again in the body (a read, a second move, the loop's back edge, adefer) keeps W1002, as the compiler does. A parameter whose name the body rebinds draws neither W1002 nor W1003, as on the compiler. Measured against wolf 0.2.19 on 118 shapes (tests/lint_is61.rs): 117 agree; the one parting is a program the compiler refuses at typecheck, before the at-return check runs. - r24's waiver retires (
8aa78d2): the warns ledger now holds s184'smut_param_moveout_{whole,elem,field,map}.luwith no filing.
Two-phase arguments (is63, the maintainer's ruling #17 of 2026-09-30; [mem.tier0.excl.4], wolffe-lang/wolf-lang#485's clause, not yet at the pin).
- A
mutargument's claim takes effect at call entry (8fd78de). Within one call the arguments run left to right; eachmutclaim is held pending until the list is done, and a later argument may READ the claimed place and runs, seeing the value from before the call: an operand (bump(mut a, a + 1)), a header or member read (grow(mut xs, xs.len),ensure(mut f, f.len + n)), a whole read (fail(mut fl, "{fl.store}"), afor, a receiver, a slice), a nested call's own argument (ring_drop(mut r, ring_len(r)),bump(mut xs[0], total(xs))). Through 0.1.42 each of these trappedexclusivity. A later argument that writes the place, moves it, claims it again, or lends it into the same call still traps, with the record it had;takearguments are unchanged. - A bare
Copyargument is a read, not a lend (9046a66): `bump(mut a, a),f(mut p, p.x)andbump(mut xs[0], xs[0])run; a non-Copy` place passedreadbeside its claim (both(mut xs[0], xs)) is the lend it was and traps. D39's caller-side overlap half retires with it (docs/approximation-contract.md§6.12). - A closure lent into the call that claims its capture traps (
a6d7791, #160).grow2(mut xs, fn() { xs.len })ran through 0.1.42, because this machine copies captures and checked nothing when a closure was written. A closure literal written as a bare argument now records each capture whose place that call holds pending, and holds itmutwhile the body runs, so the body's read trapsexclusivitythere. The compiler refuses the same program as E1002. A closure bound before the claim, one created inside a nested call, and one that never reads the claimed capture all run. - 58 witnesses in
tests/rulings_is63/(27 reads that run with wolf 0.2.19's bytes where it runs them, 18 writes, moves, re-claims and lends that trap, and s186's 13 corpus rows verbatim at theircheck:lines); 15 earlier witnesses and tests move to the ruling by name (15cfade,dee7167,31b8354). The corpus census moves one row,memory/mut_read_overlap.lu, from dynamic counterpart to static-conservatism (46 → 45, 55 → 56) until the re-pin carries s186's re-spelled header.
0.1.42 — 2026-09-30
THE FORTY-SECOND (is57, is58, is59, is60, and r24's re-pin), the lupin half of wolf 0.2.19. Pin: ec56a08f (wolf-lang v0.2.18 — the TAG), two releases forward of 0.1.41's 93a5fe50 (v0.2.16). Forty-four commits since 0.1.41 in four lanes, then the re-pin. It carries every lupin mirror wolf 0.2.18's gates pin by version — a whole read of a place holding a moved part traps (#143), a Map read moves a non-Copy value out (#144), a nested index store runs each operand once (#145), the caller sees a mut parameter its callee moved out (#146) — and the rulings of 2026-09-29 and -30 on [mem.model.place.elem] 1(c): a member read under an element claim reads the member (wolffe-lang/wolf-lang#472), len, count and is_empty are header reads (wolffe-lang/wolf-lang#474), and every other method or a slice beside a moved part reads the whole container and traps (#149); with #151 (one evaluation) and #152 (an element read inside an expression). wolf-lang's gates pin 0.1.41's answers on fourteen cases (seven lists) by version; each is one of these, measured against its ruled cell by the lane that mirrored it.
The re-pin (r24; c4143cf). vendor/upstream/{spec,corpus} and the upstream gitlink move to ec56a08f, an ancestor of the released line (merge-base --is-ancestor 93a5fe50 ec56a08f holds); the two trees are byte-identical to wolf-lang's at the tag. The census moves (every count asserted by a test): corpus files 690 → 747 (57 new, none leaves), entries 646 → 702, members 44 → 45 (d9d71fe, 611b579); anchors 539 → 542 — mem.model.place.elem, mem.model.place.rhs, os.fs.path.domain, key sets diffed both ways, none dropped — and the coverage ratchet 264 → 268; the bundle 728/684 → 785/740 programs and records (7a7ac64). The fixture set gains s181's with-file-index.json, accepted and round-tripped (4617c65). Fifty-two of the fifty-six new entries reach run (88416d3): thirty-two match their check:, eighteen are dynamic counterparts of the compiler's E1001/E1002/E1014 (fourteen trap use-after-move, four exclusivity), and two are conservatism rows (elem_dyn_move_const_read, elem_dyn_mut_pair: a run-time index the compiler cannot prove distinct, whose value this machine sees); the three Pool rows are out of scope and s181's resolve/sibling_diag/main.lu matches fail(E0302) at resolve (its member, geometry/shapes.lu, holds the duplicate and is refused read alone, bc19fa8). s180's two index stores that take (index_store_take_{list,map}.lu) are dynamic counterparts, not resolve refusals as push(take …) is here (f3801c8).
- DIV-2026-026 retires (
9c3c98a,a5bf4ba): the pin carries wolf-lang eg01'slet s2 = copy tbl[2]inmemory/list_session_struct.lu, and the published wolf 0.2.18 and this machine both answerexit(0)printing102 1 1408 4 184. One filed divergence is left, DIV-2026-019. - Filed, not fixed: W1002 on s184's moveout rows (wolffe-lang/wolf-interp#155). On
mut_param_moveout_{whole,elem,field,map}.luthis machine's lint says W1002 ("never written") where the corpuswarns:ledger is empty; the body moves the parameter out, the compiler reports E1001 and stands W1002 down, and this machine traps at run. The warns-ledger test waives exactly those four rows for exactly that code, and fails if the waiver outlives the disagreement (bc0ea33).
Header reads, whole reads, element reads and one evaluation (is60; the maintainer's ruling of 2026-09-30 on wolffe-lang/wolf-lang#474 and #149: "len, count, is_empty are header reads … push, get, pop, slices and an impl method's self read the whole container").
count(),is_empty()andlen()under an element claim run (wolffe-lang/wolf-lang#474's lupin half,4708106): `bump(mut xs[0], xs.count())trappedexclusivity; a bare header method on aList` orMapplace now asks is59's header route, asxs.lendoes. An impl method namedcount, a whole-container claim, and the element's own header still trap with trunk's record.- An element read inside an expression checks the element (#152,
e675c2f;[mem.model.place.elem]1(a)):bump(mut xs[1], xs[0] + 1)trapped because the index-read lend checkedxswhole before the index was known. While every claim that meets the container lies below it, a bracket chain (xs[i],g[i][j],b.xs[i]) evaluates its operands in trunk's order and checks the element's full path; the bare argument already did. When the element itself is claimed the trap is trunk's (container path, clause, span), now after the index operands ran, as the bare form's is. - A method or slice on a container holding a moved part traps (#149's lupin half,
e78c40a):push,get,pop,last,clear,Map.remove, an impl or home-module method'sselfin any mode, and a slice beside the moved element now trapuse-after-moveat the receiver (before any argument or endpoint runs);len,count()andis_empty()keep running on every container above the moved part. is56's and is58's tests that pushed,get-ed and sliced beside a moved element follow the ruling (80434ba,ea7e403). - A read of an evaluated place runs its operands once (#151,
4a8c506;[mem.model.order]):let n = g[f()].lenranfthree times (alive_placethat found no slot for the member, then the member route evaluating its base again); so did an argument,copy, amatchscrutinee, a nested or grouped base, and — the same family — an index out of range (let n = g[f()], twice before the bounds trap), an absentMapkey, and a slice (xs[a()..2], whose endpointsplace_ofran before refusing a slice as a place). Each now runs once, with the same record.
Witnesses: tests/rulings_is60/ (82; s185's nine rows from wolf-lang ecaf655a among them), and path tests in src/eval/tests.rs and src/eval/place.rs.
A member read under an element claim reads the member (is59, wolffe-lang/wolf-lang#472, ruled A; [mem.model.place.elem] 1(c) at wolf-lang d3bd49cc: "an element is never the container's header"). bump(mut xs[0], xs.len) trapped exclusivity: a member that is not a stored field (xs.len, m.len) was read by reading its container, and the container met the claimed xs[0] as a prefix. Now, while a claim is held under the container's binding, the check is made on the member's own path (xs.len), which no element or key claim reaches; the rest of the read (the Moved trap, a freed region, provenance) is the container's as before. Both routes a member is read by take it — a dotted path (xs.len, b.xs.len) and a projected base (g[0].len, (xs).len) — on a List and a Map, under a call's claim or a borrow's (1709aec). A claim on the whole container, a prefix of it, or the element whose member is read still traps with the same record, clause and span as before, and a program with no claim on the binding reads exactly as it did. Witnesses: tests/rulings_is59/ (18, 0d45a6a; eg02's elem_member_read_after_mut.lu among them). Not carried: xs.count() and xs.is_empty() are method calls and still read the receiver whole (wolffe-lang/wolf-lang#474 asks for a ruling); an element read inside an expression still checks the whole container (#152); a member read's base operand runs three times (#151). wolf-lang's a_member_read_under_an_element_claim_stays_refused (at d3bd49cc) expects this machine's old trap for any lupin version, so it goes red against any lupin carrying this change until eg02b pins 0.1.41 as pre-mirror.
The four mirrors (is58; wolf 0.2.18's gates pin lupin 0.1.41's answer on three of them by version, so the release that carries this turns those rows to the ruled answer).
A nested index store evaluates each operand once (#145, f44b975; [mem.model.place.rhs], [mem.model.order]). g[i()][j()] = v ran i() twice, and a three-level store its first two operands twice: the store asked whether its base was a raw pointer by evaluating the base, then evaluated the base's operands again to find the place. The place is now found once and kept. Compound, take, a field between the indices, a map inside a list and a list inside a map all run each operand once, outermost first, then the value — wolf 0.2.18's bytes on ctl_store_order_nested_index.
A whole read of a place holding a moved part traps (#143, be3d4b5; [mem.tier0.move.2] under [mem.model.place.elem]). After move xs[0], copy xs, "{xs}", xs == …, let ys = xs, an argument in any mode, a return, a literal's field or element, and a match on it all handed the moved part on; after move p.x, so did let q = p. Each now traps use-after-move, naming the part and its move site; so does a for that binds an element holding a moved part, and a whole move. The base of a projection is not a whole read: xs.len, xs[1], p.y, a method receiver and a for head read only what they pick. The walk costs nothing until a program first moves a part.
A mut parameter left moved-out is moved-out in the caller (#146, c3af852; [mem.tier0.mode.mut], wolf-lang s184). The writeback carried the parameter's value and always marked the caller's place live, so var t = move xs in the callee left the caller reading its original [1]. The parameter slot's state now travels back with its value — through a free call, a trait-qualified call and an impl method's mut self — and the caller's read traps at the callee's move site. A parameter stored back, on the path the call took, runs as before.
else and ? read a Map value out (#144, e965adb; [mem.map.absent]: "A Map index READ copies exactly when V copies"). var v = m[k] else … copied a non-Copy value and left the entry live, so a later read of m[k] saw it. Under else and ? the read now consumes the entry as let r = m[k] always did — a Copy value is copied, anything else moves until a store through the key revives it — through a local, a field, an element of a list, or a mut parameter. An absent key moves nothing. An interpolation hole ("{m[k]}") still reads without moving, as the compiler reads it.
The nineteen corpus rows wolf 0.2.18's three gates name for these clauses run against their ruled lupin cell in tests/rulings_is58.rs; five flip.
CI is sharded on every OS (is57, #123). The linux and macOS test jobs ran unsharded against the 355-minute cap — the trunk run at 0.1.41's 0cfc0cf timed out on linux at 355.3 min. Both now run three test shards and a ladder-and-bundle job, as windows did (229d7b4): the shard key is a function of the target name alone (ci/test-shards.sh, 22617d6), and ci/assert-shard-coverage.sh holds every OS's shards to the declared targets, per test, with a planted-red self-test (eefff0e, f54c791). The per-test lists before and after are identical on ubuntu (1,261) and macOS (1,260) (19849e3, 38a0a7a); the PR run fell from 5 h 52 m to 2 h 55 m (run 36504583782, ci/is57.md).
0.1.41 — 2026-09-27
THE FORTY-FIRST (is56), the lupin half of wolf 0.2.18's point release. Pin unchanged: 93a5fe50 (wolf-lang v0.2.16 — the TAG). Eighteen commits since 0.1.40, one lane. It mirrors the read half of wolf-lang's [mem.model.place.elem] (eg00): a moved element is moved on every read path, which is the answer wolffe-lang/wolf-lang#460's witness needed from this machine. Not carried, and still open: a whole read of a container holding a moved part (#143), a Map read that copies a non-Copy value out (#144), a nested index store that evaluates its outer operands twice (#145) and a caller's read of a whole mut parameter its callee moved out (#146). wolf 0.2.18's gates pin 0.1.41's answers on #144, #145 and #146 by version, so the release that mirrors each one turns its row red by name.
A read of a moved element traps (is56, #141; [mem.tier0.move.2] under wolf-lang's [mem.model.place.elem], eg00). move xs[0] always marked that one slot moved, and a place read (let b = xs[0], xs[0].field) always trapped on it; but every read that picks an element out of a container VALUE handed the moved bytes on. let a = move xs[0]; print(xs[0]) printed 1 1. Now each of those reads traps use-after-move, naming the move site: the index read xs[i] and m[k] (9bc2f89); get, first, last, pop, par, Map.remove and Map.pairs (39782fe); a slice covering the element (08105f4); and a for over a list or map, at the iteration that reaches it (3ce773b). The slice and par trap lines name no element number (bda3ed7, D61): a 0-based position is not the number an origin-1 writer reads, and the move site already says which element it was. Reads of no element are untouched — xs.len, a push, a store, another element or key. eg00's fourteen witnesses run against their ruled lupin cell in tests/rulings_eg00.rs; five flip to the trap, among them wolf-lang#460's, where 0.1.40 printed 2 1 1. Not widened: a read of the WHOLE container or struct holding a moved part (copy xs, "{xs}", let q = p after move p.x) still does not look inside.
One corpus row filed, DIV-2026-026. memory/list_session_struct.lu moves tbl[2] with a plain let and iterates tbl: it traps here now, and its header's exit(0) holds on the compiler only through wolffe-lang/wolf-lang#460. Filed until the pin carries wolf-lang eg01's copy tbl[2] (057c99e, b444fd0).
diff-run retires a waiver only on a full-depth clean compare (6c22d42, ebc65ae). A counterparty tier that stopped short of the rung this machine reached — the default tier reaches run on no entry — used to count as "compared clean", and asked for DIV-2026-026's retirement.
0.1.40 — 2026-09-25
THE FORTIETH (is55). Pin unchanged: 93a5fe50 (wolf-lang v0.2.16 — the TAG). Sixteen commits since 0.1.39, one lane, and it mirrors two rulings the maintainer took on 2026-09-24 and wolf-lang's s182 wrote into the spec (wolf-lang 0468dead). No corpus row moves and no answer in the differential against the published wolf 0.2.16 moves; what moves is lupin's answer on seven of s182's ten ruled witnesses, each now the ruled cell. Measured on kasumi against trunk and head release builds, before and after the rebase onto 0.1.39 (docs/divergence-log.md, "The two mirrors III").
The index store follows push (wolf-lang#438, [mem.region.edge.elem], [gram.expr.assign]). xs[i] = v and m[k] = v copy a non-Copy place in and leave v live, as a plain push(v) does; xs[i] = take v moves it, the one store position an assignment admits a mode in. At 0.1.39 the plain store moved the place (a later (mut xs).push(4) trapped use-after-move) and the take store was E0201. take anywhere else in an assignment is still E0201, and a store through a field still moves (b8e54e0, 60b37fe, 53ec794). The compiler half is wolf-lang s180. A use after a take store is a static E1001 on the compiler and a dynamic trap(use-after-move) here, each machine's ruled cell — the resolve rung's move check keys on call-site markers and is not widened.
Confinement is resolved, not lexical (wolf-lang#386, [os.fs.path.domain]). A path is judged after .. is applied and symlinks are followed: sub/../x inside the tree is served (0.1.39 declined it by name for spelling a ..), and a symlink that carries a path out of the tree is declined by name, unsupported, never a row. A unix socket path takes the same resolved answer, because [os.fs.path.domain] makes a socket path and a file path one object (95cc274, cfea132, c00f6c7). Absolute paths and paths that climb out stay declined by name, as before.
lupin conform-run resolves where it is invoked ([os.fs.path]: "a relative path resolves against the process's working directory, on every tier"). The door observes ONE program per process, as wolf conform-run does, and until now took the private observation root that exists for walks observing many programs in one process — so a program reading a file its harness had put in the cwd, or writing through a link its setup made there, answered not_found about a directory it was never run in. That is the row s182 read as a symlink defect in 0.1.38: lupin run served the same program, and a plain file in the cwd with no link anywhere failed the same way. The corpus walk, diff-run, the conformance export, the explorer, the fuzzer and every in-process test keep the private root (4cc7fa4, 72a19ae, 16b7866); three tests/fs_family.rs tests that pinned the root at the CLI moved to the embedded door, where both of its properties are still asserted, and a fourth pins the cwd read.
The ten ruled witnesses land verbatim under tests/rulings/, each in its own directory with its expected.toml, and tests/rulings_s182.rs runs every one through the built binary's conform-run --json from its own directory and asserts lupin's ruled cell (cafaba5, feb7631). Red at the witnesses' commit on exactly the seven that flip; green at the head. Seven flip — index_store_copies_{list,map} trap → exit(0); index_store_take_{list,map} fail(E0201) → trap(use-after-move); fs_path_inside_after_dotdot unsupported → exit(0); fs_path_symlink_in row → exit(0); fs_path_symlink_out row → unsupported — and three hold: index_store_read_param exit(0), fs_path_absolute and fs_path_climbs_out unsupported.
What did not move. lupin corpus at 0.1.39 and at the head: byte-identical reports over the pinned corpus (646 entries, 44 members). diff-run against the published wolf 0.2.16 on the default, checked, native and release counterparties: 8 / 8 / 10 / 10 divergences on both sides, every report and conservatism ledger identical except the native tier's memory/unsafe_ub_uaf.lu, where the COUNTERPARTY's exit status after its own use-after-free read differs run to run; lupin's side is ub(mem.ub) in both. New divergences: zero. The ruled ones sit outside the corpus; the wolf-lang corpus carries s180's index-store witnesses from 0.2.17, which this release is the first to answer as ruled.
0.1.39 — 2026-09-24
THE THIRTY-NINTH (is54). Pin 2e4ca769 (wolf-lang v0.2.15) -> 93a5fe50 (wolf-lang v0.2.16 — the TAG); git merge-base --is-ancestor 93a5fe50 v0.2.16 answers 0. 666 -> 690 corpus files, 625 -> 646 entries, 41 -> 44 members, 524 -> 539 anchors (fifteen added, none dropped, key sets diffed both ways), coverage 256 -> 264.
The census, predicted before the pin moved and measured after. No pre-existing row moved (the two walks diffed row by row); nineteen of the twenty-one new rows were called right — memory/pool_place_write.lu is out of scope, not a match (a subscript on a pool reaches the map-index path), and rows/negative/error_alias_private/main.lu runs, a conservatism row, where E0304 was predicted. With this release's fixes exactly three rows move, the three predicted: 503 match, 28 dynamic counterpart, 53 conservatism, 61 out of scope, 1 filed mismatch, 504 reaching run.
A row is a use (wolf-interp#134, [type.err.alias.qualified], wolf-lang#434). use disk.IoErrors whose only mention is -> int ! IoErrors was E0305 "never used": the reference walk read a signature's success type and skipped its row, so the machine-applicable fix-it would have deleted a live import. Return rows, fallible parameter types, fn-typed parameters' rows and alias unions all count now, and an alias import nothing names is still E0305. rows/error_alias_qualified/main.lu moves from a mismatch to a match (7a50d0e).
The handles have type names, and a proc talks back (wolf-interp#130, [conc.proc.handle], [conc.proc.join], wolf-lang s170). Scope and Proc[T] are prelude type names: fn fan_out(s: Scope) and fn collect(p: Proc[int]) resolve, Scope[T] and a Proc without exactly one argument are E0401 at the written type (wolf 0.2.16's code and span), and scope/proc in type position stay E0206 with a help naming the capitalised spelling (78395fb). p.join() blocks until the proc exits and yields its value, each abnormal exit — error, killed, cancelled, fault — a payload-free row tag; it is the monitor's exit reason read synchronously, so a join after the exit answers at once (7dc4982). The site's census parting, conc/proc_join_param.lu, is a match, and so is conc/proc_join_value.lu.
The protocol mirror's tail (wolf-interp#129 items 2–5, wolf-lang s169). The record emits trap_message — a failing assert(cond, msg)'s own words — on that trap only, never compared (de020d2, 40c1e2b). --phase stops answer pass, never unsupported, short of the rung where a construct would be refused: measured, and pinned by a test that was green before any change (item 3 needed no code). differ::claim() no longer reads unsupported as a clean pass of its own rung, and pass against a run outcome is never a divergence ([proto.cmp.pass]; the published reading in compare moves with it) — which moves rows/negative/tag_undeclared_arg.lu, a pre-existing gating finding, into the conservatism ledger (f2e6912, c7a5076). And EXIT_REJECTED = 65 retires: lupin lex and lupin parse exit 2 on a rejection, as every front door does under [conf.exit.static] (12fd79a).
A file index on record diagnostics (wolf-lang#437, the lupin half, in the shape s181 fixed on the issue). The validator admits a top-level files table and a file index on each diagnostic and refuses them out of shape (31bdffd); the record emits them when a diagnostic lies outside the entry — package-relative, entry at index 0, every warning attributed, the file-walking module laws attributed — and is byte-identical otherwise (280d9d8–fd6004b); comparison resolves the index (fe0c445). Against wolf 0.2.16, which does not emit it yet, three sibling-file refusals now compare span-or-code: DIV-2026-025, open until the compiler half ships. The resolve checks that walk items rather than files still report entry-relative spans for a sibling's fault.
The linux archives start on glibc 2.35 (wolf-lang#447, the lupin half). 0.1.38's x86-64 and aarch64 archives were built on ubuntu-latest and imported GLIBC_2.39 through two weak symbols Rust std links when the build host has them (pidfd_spawnp, pidfd_getpid), so they did not start on Ubuntu 22.04. Both linux dist jobs build on ubuntu-22.04 / ubuntu-22.04-arm, and ci/glibc-floor.sh measures the shipped binary's highest GLIBC version against the 2.35 floor — in release.yml before upload and in a new dist floor CI job on every PR (6943e79, bbad776, e593963). The README states the floor.
Differential against the published wolf 0.2.16 (diff-run, default, checked and native): 8/8/10 divergences at 0.1.38 and at 0.1.39, three gone at every tier (proc_join_param, error_alias_qualified, tag_undeclared_arg) and three arriving (DIV-2026-025).
0.1.38 — 2026-09-21
THE RE-PIN (is53). Pin 41695e7 (wolf-lang s166, dev-stamped) -> 2e4ca769 (wolf-lang v0.2.15 — the TAG). 654 -> 666 corpus files, 613 -> 625 entries, 514 -> 524 anchors (ten added, none dropped, key sets diffed both ways).
0.1.37 could not be pinned by anybody, and this release is why it can be now. 0.1.37 declared its specification checkout as 41695e7, and git merge-base --is-ancestor 41695e7 v0.2.15 answers no — it is a branch head the rebase-merge landing s166 rewrote under other shas, surviving only because wolf-lang preserved it as refs/tags/lupin-0.1.37-conformance-pin. So no downstream had a truthful way to say "this runs wolf 0.2.15 and lupin 0.1.37" (wolf-interp#127; wolf-web hit it trying to bump three pins and shipped two). The new pin is the released tag itself, so the same command answers yes, reflexively, and the sentence has a referent.
wolf-lang's own 0.2.15 changelog records that a trunk re-pin was tried at the cut and reverted, for two reasons that were both this machine's: it materialized for i in a..b (3.91 GB peak RSS on a 50-million range), and its corpus runner could not parse s163's run(exit=nonzero). is52 fixed both against synthesized witnesses, because the corpus at the old pin had no file that exercised either. Both hold against the real files now: grammar/range_value_wide_iter.lu and grammar/range_header_inclusive_max.lu are matches at first sight, and conc/proc_link_root_death.lu — the first run(exit=nonzero) file this corpus has ever carried — is a match in the ledger. It also found the one site is52 missed: tests/run_corpus.rs keeps its own matcher beside ledger::judge's, and that copy had no Nonzero arm, so the file panicked expected exit=nonzero, observed exit(1) while the census called it a match. A duplicate matcher is only as good as its least-updated copy.
The census, predicted before the pin moved and measured after. 480 -> 491 match, 47 -> 48 static conservatism, 59 -> 59 out of scope, 26 -> 26 dynamic counterparts, 1 -> 1 filed mismatch, 476 -> 485 reaching run, 0 failures. Twelve entries arrive, none leaves, and no pre-existing entry changes class — which was the lane's falsifiable claim, checked by diffing the two corpus walks row by row rather than by comparing totals. The prediction called eleven of the twelve new rows right and missed memory/push_take_moves.lu: it was called conservatism on the reasoning that typecheck is a rung this machine does not perform, and it is a match, because the move analysis lives on resolve here and E1001 is answered there.
trap_message is accepted (wolf-interp#129, [proto.record.trap], wolf-lang s169). OPTIONAL_FIELDS gains a fourth entry, so wolfgang's records for a failing two-argument assert stop reading to this validator as malformed. The field is never compared — [conf.trap.assert] lets an implementation drop the message, and [proto.cmp.defined-divergence] names it — and the seal it widens still closes: a bare key that is not on the list is still refused with "implementation extensions must begin with x-". Both halves were seen red before the fix was trusted, each break planted in turn.
E0206 stopped being this implementation's choice. s163 landed corpus/grammar/type_position_keyword.lu, the file wolf-lang#320 asked for, so "a keyword where a type must begin" is corpus-pinned and comes out of diag::UNPINNED_CODES. Following the counterparty's number cost nothing: is45 had already moved assume noalias off E0206 to E0212 to keep it free.
DIV-2026-021 is RETIRED — the let-group locus row, open since is38. let a, b = 1, 2 is fail(E0201)@parse on both machines, and for eight releases they pointed ten bytes apart: this parser at the comma, the compiler at the end of the initializer list. wolf-lang#228 ruled and s163 moved the compiler onto the comma. Measured before the entry came out, not inferred from the corpus header that announces it: the counterparty answers span [511, 512] under conform-run --json --checked, byte-for-byte this machine's locus, and the differential runner's span-or-code line is present at the old pin and absent from all four tiers at the new one. differ::FILED_DIVERGENCES is one entry again. This machine never moved.
Two new differential findings are named and left un-triaged, because routing a finding is a ruling: memory/push_take_moves.lu (the same E1001 at a different rung, resolve here against mem) and conc/proc_link_root_death.lu (exit(1) here against exit(121), where [conc.proc.root] says the status is implementation-specified and [proto.cmp] compares it anyway). Four further gating findings pre-date this release and are recorded in docs/divergence-log.md for the same reason: CI's differential lane SKIPs without a counterparty, so nothing automatic can see any of them.
The bundle. programs/records 692/651 -> 704/663, moving together for the first time since is47 because every new corpus file is an entry; anchors_covered 248 -> 256; forward_tags holds at 109.
0.1.37 — 2026-09-16
THE METHOD SURFACE (is49, is50, is51). Pin a7f517e (wolf-lang v0.2.12) -> 30731a6 (v0.2.14) -> 41695e7 — two pin moves in one release, the first time this machine has taken two, and the reason is that three lanes landed behind one tag.
The pin this release declares is preserved by a tag. 41695e7 is an s166 branch head, and the rebase-merge that landed s166 on wolf-lang's trunk left it reachable from no branch and no tag — so the sha lupin --version prints could not be checked out of a fresh clone. It is now refs/tags/lupin-0.1.37-conformance-pin in wolf-lang, a tag rather than a branch because it is a fixed point. Re-pinning to a trunk sha was tried at the cut and reverted: at 12ca8acc this machine OOMs on grammar/range_value_wide_iter.lu and range_header_inclusive_max.lu (it materializes the range s161's #381 taught the checked machine to walk) and its corpus runner does not know s163's run(exit=nonzero). Those are this repository's mirrors to take, not the pin's fault.
What a reader can do at 0.1.37 that they could not at 0.1.36. Write xs.map(f), xs.filter(keep), xs.fold(init, step), xs.sorted() and the rest of the ten combinators, and have them run here — as the std wolf code they are, resolved to their home module and called as the free call, never as builtins this machine special-cases. Write xs.par(f) beside them and get map's answer. Write a list literal, name a range[int], alias an error set, match a bare dotted path like io.Eof, and open, create and remove real files and directories through std.fs. Three lanes, one surface.
is49 wrote the three clauses (list literals, range[int]/range[char], named error-set aliases) and took the pin to v0.2.14. is50 mirrored s157's and s160's clauses that is49 had ledgered and left, filled in the std.fs names that did not resolve, and closed all five of its issues. is51 took the pin to s166's tree and mirrored the method surface and the combinator set from the clause text, never from wolfc.
The ten combinators, and why the number is ten. [type.comb.set] names map, filter, fold, sum, sort_by, sorted_by, sorted, enumerate, zip and collect as std wolf code, with par an eleventh row that is the language's own builtin. sorted is the one worth saying out loud: it was written early, withheld while use std.cmp in std.list darkened every importer on the native rung, and carried in on a rebase once that cleared — so wolf-std's own changelog headline still says "nine functions" and calls sorted missing while its body records that sorted shipped. Ten is the number this machine mirrors and ten is the number the pairing below is derived for; a nine-row table drops sorted silently.
THE COMBINATORS (is51). Pin 30731a6 (wolf-lang v0.2.14) -> 41695e7 (wolf-lang s166, dev-stamped, rebased on trunk 4b56441): 615 -> 654 corpus files, 498 -> 514 anchors (sixteen added, none dropped, key sets diffed both ways). The subject is wolf-lang#390, ruled option 1 by the maintainer — par stays — and the clauses s166 wrote for it: [type.method] (methods on std data), [type.comb] (the combinator set), and [conc.task.par] completed with order, failure, captures, chunking, determinism and cost. Mirrored from the clause text, never from wolfc.
The method surface. recv.name(args) IS home.name(recv, args): List -> std.list, Map -> std.map, str -> std.str, range -> std.range, a closed table. Resolution is (1) the builtin methods of the type, (2) the home module's pub fn whose first parameter carries the receiver's type constructor, called as the FREE call — same instance, same code, so xs.any(p) and list.any(xs, p) are one call by construction — and (3) traits, unchanged. A home module needs no use and binds no name: it loads when some method call in the program names one of its pub fns, under a key no source can spell. The refusals are the counterparty's codes by name: E0301 with no std root configured (naming the home module and the ways to configure one), E0403 for no candidate or a first parameter the receiver does not fit (sum(xs: List[int]) on a List[str]), E0402 for arity counted against the written arguments, and E0804 where the spelled receiver mode disagrees with the candidate's first parameter — a mut candidate is (mut xs).sort_by(less) and the bare spelling traps exclusivity, as it does on a builtin. take is never a method name ([type.method.take]), and its refusal names xs[..n], xs[..min(n, xs.len)] and xs[n..]. Step (1) gains clear on List and Map. The combinators themselves are std wolf code this machine RUNS — none of them is a lupin builtin.
par. The one builtin combinator: xs.par(f) opens a scope, splits 0..n into k = min(n, W) contiguous chunks whose lengths differ by at most one, spawns a task per chunk, joins, and reads the slots back in index order — out[i] is f(xs[i]) whatever the schedule. W is fixed at 4 on every host (1 on wasm), because the scheduler runs one task at a time whatever W is, and a host-sized W would make the spawn events differ between two hosts replaying one seed. k == 1 runs on the calling task with no spawn and n == 0 spawns nothing, both permitted. A chunk stops at its first error value, the scheduler cancels its siblings, and the first failure in schedule order becomes the par's row — a failed par has no value. f is checked as a spawned closure's body (E1101), and W1101 does NOT ride along there: its text is a claim about one task's own copy and par has k of them, which is what the witnesses pin.
The census, measured on the rebased tree (trunk 92d98af, is50 underneath): 478 match, 60 out of scope, 26 dynamic counterparts, 48 static-conservatism, 1 mismatch — filed, zero unfiled — with 476 entries reaching run. DIV-2026-024 retires at is50, which is why the filed mismatch falls from two to one. Sixteen of s166's rows are new here: ten match and six are the refusals this machine makes by name at run time rather than at resolve. One prediction missed in each direction, both recorded in docs/divergence-log.md.
A lupin gap the new clauses exposed. [type.comb.set]'s collect is one generic function over range[T], and is49's [type.range.name] check refused that signature outright — T is neither int nor char — so every home module carrying collect was E0401 at its own declaration. The check now admits a RIGID generic parameter, exactly as [type.map.key]'s sibling check already did; range[bool] is still E0401 and bare range still E0405.
THE TWO MIRRORS (is50). No pin move: 30731a6 (wolf-lang v0.2.14), is49's. The subject is s157's and s160's clauses that is49 ledgered and left, wolf-interp#107 and #111, and three smaller issues: #112 (std.fs names that did not resolve), #110 (the fs tier's residue) and #102 (an imported trait alias).
A bare dotted path is a pattern ([gram.pat.nullary], #107). Color.Green => and io.Eof => parse as PatKind::Path and match a payload-less constructor by the payload form's own resolution rule (Machine::constructor_path_matches, shared by both). This covers or-patterns and the else |pat| handler position too. The E0201 "a dotted path in a pattern must carry a payload" is retired, and grammar/match_nullary_variant.lu runs exit(0), green\nfirst\n, so DIV-2026-024 and its waiver go. Arity stays the checker's question: a bare path that names the value's constructor while the value carries a payload declines by name citing E0808, with the payload spelled (Shape.Rgb(_, _, _)), rather than falling to a later arm.
take of a read parameter traps ([mem.tier0.mode.read], #107). eat(take b) where b is a read-mode parameter traps exclusivity at the argument, the row E1014's write half has trapped since 0.1.8. The barrier is the write barrier's (read_param_write), so a projection (take b.items) and a Copy element trap too, while a body-scope shadow (var b = copy b), a take-mode parameter and the plain move-out fn f(b: T) -> T { b } (wolf-lang#359's question, not this one) do not. Every shape was measured against wolf 0.2.14 --checked. memory/read_param_take.lu moves from conservatism to dynamic counterpart.
A declaration wins its own name, pinned ([conf.resolve.ambient], #107). Already right at 0.1.34. tests/resolve_ambient.rs pins the three shapes the corpus file does not: a sibling file's read_line called from the entry file, fn assert (declared and warned, but the call still reaches the primitive) and a declared time_now_ms. Each has the compiler's verdict, stdout and W0304 span.
The materializing str producers are region sites ([mem.region.escape], #111). upper, lower, repeat and replace now charge their answer to the ambient region at exact size and carry it as the value's home (builtin::produced_str). str_from_utf8 had charged since 0.1.21 but was never homed. A read after the region's wholesale free is the [mem.region.intra.2] fault, so memory/region_str_repeat_return.lu and region_str_from_utf8_return.lu trap region-fault (conservatism -> dynamic counterpart), as do repeat(0), an empty-needle replace, a produced str held by a binding outside its block, and one sent out of a proc's own region ([mem.region.proc]). The view side (trim, get, strip_*, the split/words/lines pieces) stays home-less. Projected reads already carried their place's region: a nested field, a tuple element and a list element are pinned beside the corpus's one field. Measured against wolf 0.2.14, the compiler's two tiers disagree on the charge: --native charges all five and trap(alloc-contract)s them under cap: 0, while --checked charges none of them (it does charge +). This machine follows the clause and the native tier; filed as wolf-lang#391. Whether a view of a region-BUILT str carries its receiver's sites is unstated, and all three lanes let it escape clean; filed as wolf-lang#392 instead of decided here.
net_writev_head ([os.net.writev.head], #111). The gather with a str head: the head enters net_writev's vector as one more part, and an empty one contributes none, so the wire, the park and the rows are net_writev's by construction. net/writev_head_gather.lu moves from out of scope to a match, and its degenerate shapes plus a multibyte head are byte-identical to both compiler tiers.
std.fs's missing names, and the rows they exposed (#112). fs_create_dir (one level: exists over anything already there, not_found under a missing parent), fs_remove_dir (one empty directory: io for a non-empty one or a file) and fs_write_chunk (the handle's byte write) are in the tier. The issue's fourth name, fs_rename_atomic, is no builtin on either machine (E0301 on wolf 0.2.14; std.fs names it only to say it does not exist). Reading the three rows off the compiler's own E0602 showed that five of this machine's declared fs rows had drifted: fs_create has no not_found, fs_create_dir_all is {denied, io}, fs_read_dir carries utf8, and fs_rename carries cross_device and exists. It was observable: a handler's cross_device => arm was a binding and caught a plain io. The rows are now the compiler's. fs_answer coarsens any tag outside a call's declared row to io, so a host error can never reach a handler as an undeclared tag again, and fs_rename keeps its two refusals by kind. Recorded on wolf-lang#181. wolf-std's four dark rows (fs/chunk_stream, directories_and_metadata, exists_row, fstat_rows) run with the expected verdicts under this binary, and the other thirteen rows are unchanged.
The fs residue (#110). fs_write_bytes resolves its path before it reads its payload, so a refused path wins over invalid. A handle read retries Interrupted (fs::read_retrying). A listing holding a non-UTF-8 name is the utf8 row fs_read_dir declares, for the whole call (linux-only test). The run-varying os_cwd under observation and the consumed-then-charged handle read are behaviour kept on purpose, stated in the approximation contract §6.19.
An imported trait alias expands its bound (#102). sema::expand_bound reads a qualified bound (tiny.Num) through the alias table of the module its head names, and the alias's own list in that module, so an alias chain behind a renamed import works too. wolf-std's tests/ops/num_alias_tier.lu (Num = Add + Sub + Mul + Div + Rem + cmp.Eq + cmp.Ord, ledgered mirror-lag(E0501)) runs exit(0) with the compiler's stdout sha256, and wolf-std's ledger goes red on it, as designed. The cost was one function.
Census, predicted before the first edit and measured after the last (lupin corpus). Baseline 790c127: 460 match / 54 out of scope / 2 mismatch (both filed) / 23 dynamic counterpart / 41 conservatism / 449 reach run. Predicted and measured: 462 / 53 / 1 / 26 / 38 / 451, every cell, the five rows by name. The compiler's table at wolf-lang v0.2.14, re-derived against the 790c127 binary as control, is 351/148/0/107/8 checked and 381/148/0/77/8 native (agreements / completeness / soundness / unsupported / hard), coverage B 451 and BOTH 331/361. That is five ledger counts moved per tier, all -> agreement, and hard is now the eight #167 warning-parity rows alone. Every cell was predicted before the first edit.
THE THREE CLAUSES (is49). Pin a7f517e (wolf-lang v0.2.12) -> 30731a6 (wolf-lang v0.2.14), two releases in one bump: 580 -> 615 corpus files, 475 -> 498 anchors (twenty-three added, none dropped, key sets diffed both ways). The subject is wolf-interp#106, s158's mirror — three surface additions the compiler carried at 0.2.14 and this machine refused by E0201 at the first new token: list literals ([gram.expr.list], [type.list.lit]), a nameable range[int] / range[char] with start / end ([type.range]), and transparent error-set aliases ([gram.item.error], [type.err.alias]). Eighteen witnesses; the eighteenth, rows/error_alias_ident.lu, needed no mirror at all.
List literals. A [ that BEGINS a primary opens a literal; a [ that follows an expression is index_args, as it always was — position decides and nothing else does, so [10, 20, 30][1] is a literal indexed with no lookahead (grammar/list_lit_index.lu). The value is a fresh List[T] allocated into the ambient region, elements evaluated left to right exactly once each before the list exists, and the ledger agrees with the spelling the clause names: the container is charged as List() charges it and each element charges the growth push would have, at the same stride, to the same birth region — pinned by a test that reads region_bytes under both spellings. The static half, sema::list_lit_check, is a ScalarWalk-shaped pass over literal SHAPES: the first element that does not fit is E0401 at that element with the first element as the "because" (grammar/list_lit_mixed.lu, byte-identical span with the compiler, [368,373]); an annotated let/var pushes its element type into every element and a non-List annotation is E0401 at the literal; a bare let xs = [] is E0419 at the [] with the annotation spelled for that binding (grammar/list_lit_untyped_empty.lu, [409,411] on both machines). A call argument, a field, a declared return supply a context this pass does not check — the permissive direction, the sema boundary's rule.
range[int] as a type — and the claim that was false. range joins sema::PRELUDE_TYPE_NAMES (type position only, so var range = true is not a W0304 hazard; pinned), closed at the two element types: bare range is E0405, range[f64] is E0401 at the argument. .start/.end are two arms of builtin::property. wolf-interp#106 says "lupin already does this — Value::Range has been built half-open since its first range arm". It had not: at 0.1.36 Value::Range carried inclusive: bool to eval_for, which computed if inclusive { end } else { end - 1 } on raw i128, and 0..=int.MAX as a value neither trapped nor normalized. Now a..=b normalizes at construction to end = b + 1 under Machine::checked, so (2..=7).end is 8 and let big = 0..=9223372036854775807 traps overflow at the let (grammar/range_type_overflow.lu, 21:15) — and the for HEADER form is unchanged, a non-materializing counted walk to the inclusive bound, so for i in 9223372036854775805..=9223372036854775807 still iterates three times ([type.range.value]). Value::Range carries elem: RangeElem (Int(IntTy) | Char) and no flag; 'a'..='d' steps the scalar value and range_type_char.lu prints a d e. Recorded on #106.
Error-set aliases are spellings, and the spelling is gone before anything reads a row. error IoErrors = {none, parse} is an item; error is CONTEXTUAL on three tokens (Ident("error"), IDENT, =), so a binding, a function, a field and an assignment target named error parse as they always did (lex::CONTEXTUAL 12 -> 13, §6.2 at the pin names it). A postfix row position admits a bare path — -> int ! IoErrors is -> int ! {IoErrors} — and ! IDENT continues a type the way ! { does. The semantics are one function: sema::expand_error_aliases runs on every unit of a module after all are parsed and before any is collected, and REWRITES every error row through the module's aliases — signatures, fields, annotations, casts, closure parameters, index type arguments — so the raise-tag vocabulary, ?'s widening, the else handler coverage, the lints and every rendered type work on tags as they always have ([type.err.alias.transparent], [type.err.alias.diag]). Aliases compose by naming and flatten by [gram.type.row.flatten]'s rule, a repeated tag one tag (rows/error_alias_union.lu); an alias entry with a payload is E0601; a cycle is E0610, once, at the ENTRY that closes it with the loop named (rows/negative/error_alias_cycle.lu, [434,435] — the compiler's own locus); an open row in an alias is E0201 at the .., in the parser, where the corpus's phase: lex puts it ([368,370]). The clause's prose says "E0515" for the cycle; the registry, docs/diagnostics.md, the compiler and the witness say E0610, which is the code here — the nit is filed upstream.
Census, predicted before the first edit and measured after the last. Baseline re-measured with the 0.1.36 binary at the NEW pin: 443 match, 54 out of scope, 19 mismatch (18 unfiled: the fourteen positives, the three negatives answering E0201 for the wrong reason, and match_nullary_variant), 23 dynamic counterparts, 41 conservatism, 435 reaching run. At the head: 460 match, 54 out of scope, 0 unfiled mismatches (2 filed), 23, 41, 449 reaching run — six of seven cells as predicted, the seventh (449, not 448) an arithmetic slip in the prediction, not a row the census surprised. The rows this lane leaves, by name: grammar/match_nullary_variant.lu (s157's [gram.pat.nullary], filed as DIV-2026-024 against wolf-interp#107 and waived — is50's), net/writev_head_gather.lu (net_writev_head declined by name, #111), memory/region_str_{repeat,from_utf8}_return.lu and memory/read_param_take.lu (static refusals this machine runs clean — conservatism, #111). The compiler's table, re-derived at the head (cargo xtask differ with --control the 0.1.36 release binary, wolf 0.2.14 at 30731a6, both tiers): agreements 332 -> 346 checked and 362 -> 376 native, hard 23 -> 9 on both (8 Diag warning-parity rows + 1 Verdict, match_nullary_variant.lu), completeness 151 and unsupported 108/78 unmoved, coverage B 435 -> 449 and BOTH 315 -> 329 / 345 -> 359 — THE INTERPRETER BUMP MOVED 14 LEDGER COUNT(S), the fourteen files named, three fail-pinned rows moved below the ledger to their right codes. Every cell as predicted, on both tiers; r20 predicts against these.
0.1.36 — 2026-09-12
THE FS TIER (is48). Pin unchanged at a7f517e (wolf-lang v0.2.12); this release adds no spec and no corpus, only an implementation this machine had declined since s38.
The ruling. lupin has answered the whole io/fs family with "this machine has no filesystem by design" for every release up to 0.1.35 — a reading of [proto.cmp.defined-divergence] that put the HOST's filesystem outside the comparison surface. Asked whether that meant file input and output would never be possible in the reference implementation, the maintainer ruled otherwise (BACKLOG B16): build the tier. Real files, no mocks, written from spec/11-os.md §6 and the witnesses under corpus/fs/, never from wolf_rt::fs.
eval::fs implements [os.fs]. Twenty-two names, three of which are one call: fs_open is mode 0 and fs_create is mode 1, and fs_open_mode takes the caller's own out of a set of SIX — 0 read, 1 write (create, truncate), 2 append (create), 3 read-write (create, no truncate), 4 create-new (exclusive), 5 read non-blocking. A mode outside the set is invalid decided before the filesystem is touched, which is the clause's own ordering and is tested as such: fs_open_mode("no-such-dir/x", 99) is invalid and not not_found. Handles are 1-based and never reused, so 0, a negative and a closed handle are all the io row — corpus/fs/fstat.lu's closed_is_io and forged_is_io. fs_fstat answers [kind, size, modified_ms] off ONE metadata read on the handle, in fs_size's and fs_modified_ms's units so the handle's answer and the path's compare without a conversion. fs_read_dir SORTS, bytewise, which is what makes a listing program's stdout the same on ext4, APFS and NTFS.
wolf-interp#86's mode-5 half closes for real. Mode 5 is a read open that cannot park: on unix it carries O_NONBLOCK, and on a regular file it is mode 0 in every respect — same handle, same bytes, same fs_fstat — which is the parity corpus/fs/open_nonblock.lu exists to pin. There is no libc dependency here and unsafe_code is forbid, so the flag is a written-down constant per host; it is pinned BY BEHAVIOUR rather than by inspection, by a test that opens a REAL fifo with no writer and fails on a timeout if the open parks. windows has no equivalent on this path and serves mode 5 as mode 0, by name, exactly as the clause says.
Containment: the one thing the old posture got right, kept. A path that is absolute or climbs out of the working directory is refused BY NAME (unsupported), never by a row — a row would be a claim about the host. The compiled lane does not contain paths this way (probed), so this is a stated narrowing and not an accident: the corpus walk, the differ, the explorer and the fuzzer all run corpus programs in-process, and a path that climbs out is the one bug in this tree that could damage the machine it runs on.
Six bugs found by reviewing the branch against itself, all fixed here. The private observation root paid for most of them: the three predicates asserted away a row that resolution really can answer, so a file planted at $TMPDIR/wolf-obs turned every observed run into a PANIC; a root that could not be built collapsed into the live sentinel and bound a unix socket in the user's real directory; the root's name was predictable in a world-writable directory, so a planted symlink redirected every write (the leaf is claimed with create_dir now, which fails on a symlink, and carries a clock reading); a failure between making the root and making its target/ leaked a directory nothing would remove; path_row produced an exists row that no path call declares, so fs_create_dir_all over a FILE handed a program a tag its own handler could not match (the compiled lane answers io, and so does this one now); and the REPL was treated as an observation, so a session's files were written to a temp root and deleted when it ended — stdout pass-through and "this is somebody's real directory" are different properties and now have different flags.
Windows device names are refused by name. NUL, CON, AUX, PRN, COM1-COM9 and LPT1-LPT9 are ordinary components to a path parser and DEVICES to windows wherever they appear, so the lexical containment check had to name them. Refused on every host, because the corpus is shared across the matrix; console.txt, communication.log, COM0 and COM10 stay admitted.
Two spec gaps filed, not guessed at (wolf-lang#365). [proto.cmp.triage] makes the clause the defendant first, and §6 is silent twice over: it never says whether an fs_* path may be absolute or climb out of the working directory (this machine refuses both by name; wolf 0.2.12 admits both, probed), and it gives fs_remove a row set without saying which row a DIRECTORY takes (the lanes agree today only because both delegate to the same standard library — denied on macOS, io on linux). Both readings are on the issue.
read_line does NOT land. Stdin is not a file, no pinned clause names an injectable one, and nothing in the whole corpus calls it. The name still resolves, so the refusal reads "unsupported feature" and never "unknown name" — the same reason the fs names resolved before they worked.
A handle-level failure is io on every host. [os.fs.fstat] says "on a handle the hosts answer io for nearly everything, the entry being already resolved", and this release takes that literally instead of forwarding the host's error kind. CI is what found it: reading an APPEND handle is EBADF on macOS and ERROR_ACCESS_DENIED on windows, so forwarding the kind made one program answer io on one host and denied on the other — a portability hole in a row set programs branch on. The compiled lane answers io here too (probed at this pin: a write to a read handle, a read from a write handle and a closed handle are all io), so the uniform row is the compatible reading as well as the clause's. The PATH calls keep their kinds, where not_found and denied are the answer rather than noise.
A unix socket lands in the observed program's own directory. A socket path is a filesystem object, so net_listen_unix resolves it exactly where fs_exists looks. corpus/net/unix_echo.lu is why this had to be fixed rather than noted: the witness sweeps a stale socket with fs_remove, binds it, and ends with cleaned = !fs_exists(path). With the two families disagreeing, that sweep swept a path nothing bound, cleaned was vacuously true — the witness passed while checking nothing — and a stale socket in the real directory would have failed the bind at random.
Rows probed rather than guessed. A read past end-of-file is the eof row; fs_read_text over bytes that are not UTF-8 is utf8, never lossy and never a trap; an append handle does not read (io) and a create-new handle does. No corpus file reads twice, so eof is invisible to the census and is pinned by unit tests instead — [os.fs.open]'s mode-5 prose is the clause that names it.
An observed program gets its own project root, and the tier is why. Corpus programs write REAL files at paths of their own choosing, and this crate runs many programs at once by design: several harnesses walk the whole corpus, cargo runs test binaries in parallel on top, and export::export runs a full walk inside any of them. Three symptoms, all measured: fs/fstat.lu read size=0 off a file another thread had just truncated, two concurrent exports of one corpus appended to one log.txt twice (log=one|one|twotwo size=14), and a bundle's sha256 stopped being reproducible.
So an OBSERVED program's relative paths resolve against a private, empty project root — made once per observation, removed when it ends, and given the one target/ directory that makes it a project root, because the witnesses write there without creating it and both lanes run from a directory that has one. A live lupin run is untouched: a user's program writes in the user's own directory, exactly as wolf run does, and os_cwd answers whichever of the two applies so a program's world is never split.
Ordering was tried first and was the wrong answer. One advisory lock per program does serialise correctly — but memory/byte_producers_ledger.lu takes 74 seconds in a debug build, and serialising it across every walker would have added something like twenty minutes to a CI run already near three hours. The private root removes the contention rather than scheduling it: no waiting, no lock files, reproducible BY CONSTRUCTION rather than by every harness remembering to take a lock. It also stops the corpus walk littering the repository's own target/ with witness scratch files.
Census, predicted before the first edit and measured after. The nine files that were out of scope for the fs tier and nothing else:
| class | baseline (0.1.35) | predicted | measured | | --- | --- | --- | --- | | match | 423 | 432 | 432 | | out of scope | 62 | 53 | 53 | | mismatch | 1 | 1 | 1 | | dynamic counterpart | 21 | 21 | 21 | | conservatism | 38 | 38 | 38 | | reach run | 412 | 421 | 421 |
Six of six exact, including the one figure the prediction flagged as uncertain (whether net/unix_echo.lu, which failed at run rather than at resolve, was already inside the 412 — it was not). The files: fs/bytes_dirs.lu, fs/error_row.lu, fs/fstat.lu, fs/open_nonblock.lu, fs/roundtrip.lu, memory/byte_producers_ledger.lu, net/unix_echo.lu, projects/count.lu, projects/count_dir.lu. The row that was in doubt was memory/byte_producers_ledger.lu, because it pins a REGION-accounting relation and not an fs one: read_tight holds only because fs_read_bytes and fs_read_chunk mint their buffer at exact capacity through region::ledger::byte_buffer_bytes, the way s.bytes() and the net byte reader already did.
Two corrections to the record, found while measuring.
1. 0.1.35's census line was one file stale. It reads "413 -> 422 match … 62 -> 63 out of scope" and asserts "every number above is the pin's, measured before a line was edited and unchanged after". It was not unchanged after: is47's own E0413 mirror moved typecheck/interp_spec_on_union.lu from out-of-scope to match, so the 0.1.35 binary answers 423 match / 62 out of scope, not 422 / 63. The figures in the is47 table in docs/divergence-log.md are correct as labelled — they say "measured with the 0.1.34 binary at the new pin" — and it is only the "unchanged after" sentence that was wrong. The baseline column above is the re-measured one. 2. The sprint's premise did not survive measurement. is48 was written expecting an unsupported count of "87-114 rows, most of them fs/net". Measured at a7f517e it is 62, of which fs, net, os and ffi together are 16; the largest single group is comptime at 21. The tier is worth building on the ruling; it was not worth building on that arithmetic, and the number is written down here so no later lane re-derives it.
Pairing: the pin is unchanged, so the compiler's PAIRING at a7f517e still names lupin 0.1.34. The rows this tier moves are listed above by name, for the next compiler release lane to predict against.
0.1.35 — 2026-09-11
THE REMAINDER (is47). Pin c9237c1 -> a7f517e — the v0.2.12 tag. The delta is two sprints and a release lane: s154 (nine papercuts — E0409 on either operand side, W1002 standing down beside a mode error, a !T tail in a unit context as W0601, W0318 over a swallowed term, E0410 on a let binding's fields, the one-line body-less trait member, and [gram.inv.ctx] naming then), s156 (eleven — fmt's break selection and fallback parens, % on a float as fmod, the checked tier's converting casts, an operator's trait being the one reachable, a container-index store lending, E0413 on a format spec over a !T hole, str.find by needle length, and the two stale seed headers re-pinned) and r17. Ten corpus files join (536 -> 545 entries, 34 -> 35 members, 570 -> 580 files) and four anchors (471 -> 475; key sets diffed BOTH ways — four arrive, nothing drops, no owner changes): [gram.fmt.break], [gram.fmt.paren], [mem.region.edge.elem], [type.float.rem]. No new namespace — all four sit under gram/mem/type and spec/05 is untouched in the range, so anchor::REGISTERED_NAMESPACES holds at thirteen.
then joins the contextual table, and the table is held to §6.2 BOTH ways (wolf-interp#100). §6.2 now reads "… self (receiver), then (after a complete if condition, [gram.expr.if]), …", so lex::CONTEXTUAL goes to twelve in the same change as the pin that carries the sentence. Nothing in the parser moves: then was matched by spelling in the one position after a complete condition from is45, which is what "contextual" means. The test that was supposed to make this impossible to get wrong only ran ONE way — over lex::CONTEXTUAL, asserting §6.2 names each entry — so a word the CLAUSE names and this machine does not list, then's exact shape for two pins, asserted nothing. The reverse loop lands with the two rules that keep a mentioned word out: a reserved keyword (assume, if, in — lex::KEYWORDS decides the token kind), or reg, which no production tests for. #100's other half is a guard nothing in this tree pinned either: a body-less signature followed by another declaration on one line is still E0201 at the fn.
free_names collects a binder it never collected (wolf-interp#45). A match fell to walk_child_exprs, so an arm's OWN binding read as a FREE name and a closure that captured nothing could draw W1102 on a later write to the outer name. The for element and the else handler's binder had the identical hole; all three land as one rule. The #44 tag-arm judgement is SHARED rather than restated — Walk::arm_is_tag_pattern becomes lint::pattern_is_tag, which both walks call — and the else binder is what makes it answerable here, carrying an empty declared row exactly as Walk records one when it cannot see a signature. eval calls free_names twice and is fixed by the same change: the nested-fn capture probe REFUSED a program outright for a "capture" that was a match arm's binding. Five witnesses, since no corpus file exercises any of the three shapes, and two of them are controls in the other direction.
The declared-scalar check learns char (wolf-interp#61). [type.char] and [type.byte] state one rule in two documents — a width-bearing scalar "adopts no numeric literal, in every position" — and is37 paid the byte half as a byte-only lattice, deliberately, naming char's false-positive surface as the reason not to smuggle it in. ByteTy becomes ScalarTy with Char and ListChar; 'a' is a char the way 65 is an int; and the rule reads is_width_bearing rather than naming one type, so is37's two guards are generalised rather than joined by a third sibling. Eight programs, each measured against wolf 0.2.12 conform-run --json --checked, and lupin answers every one with the SAME CODE AT THE SAME SPAN: a char parameter handed 65 [77,79], a -> char returning 65 [21,23], var c = 'a'; c = 65 [44,46] — the one that produced a WRONG ANSWER rather than merely accepting a rejected program, silently retyping a live variable — plus a char in an int slot, the two width-bearing scalars against each other in both directions, a List[char] element and a mixed comparison. Four rows of char_is_not_an_integer move from unsupported to fail(E0401); the two + rows stay dynamic, because [type.byte.op]'s widening rule is about byte and this pass will not invent one [type.char] does not state. The false-positive surface, measured over the whole pinned corpus: zero.
Two refusals name the way forward (wolf-lang#158, B28). when with one operand said "call the method on the sync type" and Mutex has no such method; it states the rule now, as wolfc's own message does. for c in "abc" said "for cannot iterate str" and named no way forward; it names chars(), words() and lines() now, and the test runs all three.
B28's three row classes mirrored as wolf-interp#103 (four programs the compiler refuses statically and this machine runs — if-without-else as a value, call-site unification, the element binding in a loop, an omitted result), #104 (the rendering rows) and #105 (the observability surface, B29 folded in). Two rows of #153 HEALED and are recorded rather than filed: the captured-var write now answers E1101/W1101/W1102 byte-identically to wolf 0.2.12, and the element binding outside a loop runs on both machines. #158's byte-offset row healed too — line:col has arrived, the excerpt has not.
Two divergences retire on the pin. wolf-lang#341 re-pinned both seed headers: wordcount.lu teaches tally[w] = (tally[w] else 0) + 1 and grammar/structlit_paren.lu binds its parenthesized literal instead of comparing it. DIV-2026-022 and DIV-2026-023 both match at first sight and leave differ::FILED_DIVERGENCES.
Census 536 -> 545 entries, 404 -> 412 reach run, 413 -> 422 match; 20 -> 21 dynamic counterparts, 38 conservatism, 62 -> 63 out of scope. Mismatches 3 -> 1: DIV-2026-019 alone. The sprint's own three issues are corpus-neutral by construction — no corpus file exercises a match-arm capture, a declared-char boundary or either refusal message — so every number above is the pin's, measured before a line was edited and unchanged after.
Pairing: this release declares v0.2.12; the compiler's PAIRING at a7f517e names lupin 0.1.34, which r17 closed for the previous release — the next release lane closes this one from the other side.
0.1.34 — 2026-09-11
THE MAP MIRROR (is46). Pin 662b14c -> c9237c1 — the v0.2.11 tag. The delta is the five sprints 0.1.33's pin left past its tag: s149 ([os.fs.open] mode 5, the accept posture), s150 ([type.fn.value], [abi.native.closure], [conc.chan.payload]), s151 ([gram.expr.if] — mirrored at 0.1.33, its twelve witnesses arrive now and match at first sight), s152 ([type.map], [type.map.key], [mem.map.absent]), s153 ([exec.checked.budget], [mem.region.escape]) and s155 ([type.trait.op], [type.trait.op.alias]). Thirty-six corpus files (536 entries), thirteen anchors (471; key sets diffed BOTH ways, nothing dropped), and a new namespace: exec joins anchor::REGISTERED_NAMESPACES in the same change as the clause's list ([conf.anchor.ns.admit]) — without it the walk refused strings/bytes_view_walk.lu, the one thing the pre-bump prediction missed. s154 is past the tag: #99 is mirrored from the trunk clause the way is45 mirrored s151, and then stays out of lex::CONTEXTUAL until §6.2 names it at a pin. Pairing: this release declares v0.2.11; the compiler's PAIRING still names lupin 0.1.32 — the next release lane closes that from the other side.
A trait reached through its module dispatches (wolf-interp#96), and a qualified trait in a bound is a use of its import (#97). tiny.Eq.eq(a, b) after use std.tiny — three segments, the head a module and the middle a pub trait in it — fell to the path evaluator and was unsupported at resolve while the byte-identical trait in the entry file dispatched: one trait, two verdicts, decided by which FILE declared it. The trait-qualified call takes the module road too. And fn total[T: ops.Add] with the bound the file's only mention of ops was E0305 "never used", with a fix-it that deleted the line the bound resolves through; the ref walk visits fn, trait and impl generics now. Both witnesses need a staged module and live in tests/std_root.rs; together they unblock wolf-std's [K: Eq] five and every ops impl.
The key protocol (wolf-interp#91, s152). A Map key was its RENDERING: an int key projected as an ordinal index and "did not denote a place" on squares[i] = …; a char or bool key rendered on the way in and compared against the stored value on the way out, so every read missed. MapKey carries the four [type.map.key] admits, compared by value. m[k] on an absent key answers the none row — never (), the one unchecked, untyped read the language had — and the compound path's absent-to-zero default goes with it: m[k] op= v is E0417 at the whole indexed place, decided from the declaration that made m a Map (an initializer, an annotation, a parameter), and refused in the same voice at run time where sema could not name the binding; a key outside str/int/char/bool is E0418 at the key type, in a signature and in Map[K, V]() alike, a rigid K clean. Five witnesses print the pinned bytes; the two refusals are the compiler's spans, [786,801] and [648,653].
The operator bridge (wolf-interp#92, s155). a + b IS Add.add(a, b) when a is a struct; == is Eq.eq and != its negation — an inverting impl is consulted, nothing synthesized (D49), so a struct's == no longer compares structurally; the ordering family reads Ord.cmp against Less/Greater and <=> is the Ordering itself; prefix - is Neg.neg. Never two primitives, which is also what [T: Num] at int runs. The static half is decided from declarations as the other read-back refusals are: a parameter typed as a bare generic carries its name, and the bound (alias-expanded, a cycle cut) must name the table's trait — E0501 at the operand, "the bounds on T say nothing about +"; a struct's operator needs the trait in scope by name (E0301), an impl for the struct (E0502) and the homogeneous shape (E0514), every span the left operand's — golden_arith, golden_eq, op_eq_no_trait, op_missing_impl and op_hetero_add answer the compiler's codes at its bytes, and so does the golden rule's call shape, Show.show(v) under a bare T (golden_missing_bound, E0501 at the argument). The alias form parses: trait Num = Add + Sub + … + Ord is a memberless TraitDef carrying its bound ([gram.item.trait]); its trait heads count as uses. An enum value with an impl dispatches; one without keeps the structural tag comparison, the conservative side.
A built str has a region (wolf-interp#88, s153 / wolf-lang#310). Value::Str is a newtype: the text and an optional home, equal and ordered by the text alone. s + u, s += u and a holed interpolation are charged to the ambient region at the building expression — the site rule a struct literal follows — and carry it out as their home; a literal, a slice and every view product carry none. A region block whose OWN value lives in the region it frees faults at the } (region_str_concat_return.lu printed regions from freed bytes through 0.1.33); a binding whose str died with its region faults at the read (region_str_concat_send.lu, at got) — E1010's dynamic counterpart, both. The charge is the checked machine's, measured: print("{n}") under cap: 0 is trap(alloc-contract) on wolf --checked at c9237c1 and runs on --native.
Four small mirrors. A struct literal missing a field is E0408 over the literal (#94); an unresolved bound name is E0301 at the name (#95), checked before the bridge reads bounds so it is never a later E0501; a temporary cast to dyn is E0810 over the cast (#98 — wollf's wh-002, and traits/dyn_temp_refused.lu leaves the conservatism ledger for a match); a field write through a let binding is E0410 at the place, one level down too, an index left alone (#99, s154's clause from trunk). Every span the compiler's on its reduction. #100: the body-less member followed by another on one line is still E0201, checked; then waits for the pin that carries §6.2.
Two seed files filed upstream (wolf-lang#341). wordcount.lu's tally[w] += 1 is E0417 under s152 and grammar/structlit_paren.lu's struct == is E0301 under s155; both headers still pin the pre-clause verdicts, and the compiler's ledger stops one rung short of seeing it. DIV-2026-022 and DIV-2026-023, waived; both leave RUN_LEDGER, the CLI's exit(2) probe stands on its own program, tests/s151/ retires into the corpus.
Census 500 -> 536 entries, 383 -> 404 reach run, 380 -> 413 match; 16 -> 20 dynamic counterparts, 42 -> 38 conservatism, 61 -> 62 out of scope. Mismatches 1 -> 5 (the pin, measured before a line was edited, every class and every witness as predicted) -> 3: DIV-2026-019, -022, -023.
0.1.33 — 2026-09-11
THE MIRROR TAKES then (is45). Pin e0ce018 -> 662b14c — the v0.2.10 tag, closing the gap 0.1.32 left at a dev stamp 22 commits short of it. The delta is s148: [type.fn.ret], [type.row.operand] and [mem.str.imm], five corpus files (500 entries), five anchors (458; key sets diffed BOTH ways, nothing dropped). s149, s150 and s151 are all past the tag and wait for the next pin; the clause this release mirrors was read from wolf-lang trunk f608487.
The one-line if (wolf-interp#90, wolf-lang#307/s151, [gram.expr.if]). if c then a else b: then closes the condition and each branch is one expression; the if's own else binds first inside a bare branch, so a defaulting else there is written in parentheses ([gram.amb.else]); the two branches of one if share a form and an else if picks its own; then before a block is optional and dropped; a condition followed by neither { nor then is E0201 naming both spellings; a let in a bare branch is E0201 offering the brace. then is contextual — matched by spelling in the one position after a complete condition, so a bool named then is a condition and less.then(greater) is a member — and it is NOT in lex::CONTEXTUAL, because §6.2 does not name it (wolf-lang#318). A bare branch is a brace-less block holding its one expression, so sema, lint and the evaluator are untouched. s151's twelve witnesses live under tests/s151/ byte-identical to trunk until the pin that carries them: nine run to the pinned stdout, three refuse E0201 at the compiler's own spans.
A comparison on a bare row is E0409 on either side (wolf-interp#85, [type.row.operand]). is43 read the rule off [type.interp.union]'s carve-out and answered E0401 for a comparison; s148 wrote the rule down and ruled one code for every operator with a !T operand. The pin's one new mismatch, rows/negative/row_operand_compare.lu, matches. The second half: s[a..b] = v on a str is E0416 at the place ([mem.str.imm]), decided from the declaration that made s a str; typecheck/str_slice_assign.lu leaves the out-of-scope ledger for a match. s[i] = v stays E0411 first (D25).
One type parameter, one type per call (wolf-interp#84). fn sum_areas[T: Area](a: T, b: T) called with a Rect and a Square ran here to exit 58 and is refused by wolf 0.2.9 — the compiler was right. The declarations-read-back walk now names an argument's struct (a literal, an annotation, a parameter, nothing else) and refuses the second binding of one bare T with E0401 at that argument, wolf's sentence and span ([464,465] on wollf's wh-001). No clause states the rule: filed as wolf-lang#319 with the witness.
A type argument in expression position resolves (wolf-interp#89). List[Nonesuch]() constructed a list; on List, Map, Set and Option the brackets are generic application ([gram.amb.brackets]) and each argument is a type name checked against the annotation check's scope plus the declaration's generics and its impl block's — E0301 at the name. A name a local shadows is left alone. And fn f(p: proc) is E0206, the counterparty's number for a keyword in type position, followed the way E0203 was; E_ASSUME_ARITY moves off E0206 to E0212 (wolf-lang#320 asks for the corpus witness).
wolf-interp#86 (fs_open_mode 5) has nothing to answer at this pin: s149 is past v0.2.10, so corpus/fs/open_nonblock.lu is not in the vendored corpus, and the fs tier still declines by name.
Census 495 -> 500 entries, 383 reach run unmoved, 375 -> 380 match; 61 out of scope, 42 conservatism and 16 dynamic counterparts unmoved. Mismatches 1 -> 2 (the pin, measured before a line was edited) -> 1 — DIV-2026-019.
0.1.32 — 2026-09-10
THE MIRROR TAKES THE RANGE ARM (is44). Pin 4c60946 -> e0ce018 — a dev stamp, s147, because [gram.pat.range] landed after the v0.2.9 tag and its four witnesses cannot be mirrored without the pin that carries them. The delta spans two sprints: s147's range arm and s146's [type.unit] family. Eight corpus files join (495 entries), five anchors (453; key sets diffed BOTH ways, nothing dropped).
Range patterns in match arms (wolf-interp#83, wolf-lang#287). 0..10 and 10..=19 on every integer primitive a literal pattern may already be written against, and 'a'..='z' on char, ordered by scalar value ([type.char.order]). Both endpoints are literals; the arm lowers to two comparisons on the scrutinee, with the exclusive high end excluded. Ranges compose with or-patterns, guards and @-bindings, and bind nothing — a range is a test, not a binding.
No open ends, and the refusal says so. 10.. and ..10 are the slice spellings of [gram.expr.primary], refused in pattern position with E0201 and a note that names the range form. That note is the whole deliverable for one of the four witnesses: grammar/match_range_open.lu was a match before this release *and* after it, because the corpus compares codes and a parser with no range production says E0201 too. The clause is explicit that the diagnostic must say the word "range" — the papercut the book carried — so the ledger for it is a test that reads the string, not a corpus directive.
Empty ranges are E0815. 5..5 stops before its own low end and 9..=3 runs backwards; the endpoints are literals, so the emptiness is decided at compile time and refused at the pattern rather than left as a dead arm no program ever takes. A one-value range (5..=5, 5..6) is legal.
Exhaustiveness as the compiler does it: no union is computed. Integer and char columns stay "infinite" for every domain, bounded or not, so a _ or binder arm is still required after range arms. Overlapping ranges are legal and the first arm wins. An arm whose every alternative lies inside ONE earlier range or literal is E0802; a range covered only by the union of several is deliberately silent. The rule is gated on a range having been spelled, so the 495-entry walk is byte-identical across it. This posture is mirrored from spec/01 — the clause states it in full — and not from the counterparty's checker, which the independence doctrine forbids opening.
wolf-interp#79's method half (sema::MethodDef). is43 closed #79 over signature positions and named the half it left: MethodDef recorded the decl and the trait, not the impl block's generic parameters, so impl[T] Stack's fn push(self, v: T) had no scope the annotation check could read T from. The collector records them now and every impl-block method's annotations are inside the check. Trait DEFAULT bodies stay out — a trait's own parameters are still not collected, and guessing is the failure the sema boundary exists to prevent. Corpus motion: zero.
s146 cost zero source motion. typecheck/unit_context_discard.lu, conc/chan_send_closed_row.lu and conc/spawn_tail_send_raised_row.lu all matched at first sight with the 0.1.31 binary at the new pin, as did grammar/match_switch.lu (#286). [type.unit.discard]'s reading was chosen partly for being non-breaking, and a mirror that already ran every witness is the cheapest evidence for that.
Census 487 -> 495 entries, 377 -> 383 reach run, 367 -> 375 match; 61 out of scope, 42 conservatism and 16 dynamic counterparts unmoved. Mismatches 4 -> 1 — DIV-2026-019, which has nothing to do with this pin.
0.1.31 — 2026-09-10
THE TAIL IS CHECKED (is43). Pin 2c03ed9 -> 4c60946 — the v0.2.9 tag, not a dev stamp. The sprint said the pin need not move, and for three of the four items it did not need to: the clauses items 1 and 3 mirror are already at 2c03ed9. #78's are not. wolf-lang#278 is spec commit 8d1e003, s145 merged after is42 pinned, and it is not an ancestor of 2c03ed9, so the char rows could not be mirrored without the pin that carries them — and the vendored corpus would have kept pinning fail(E0409) on a program the compiler runs. The delta is four files: spec/10-types.md, spec/anchors.json (446 -> 448, key sets diffed both ways — type.closure, type.closure.return arrive, nothing drops, nothing moves), corpus/strings/concat_mix_char.lu (flipped to run), and the new corpus/typecheck/closure_return.lu.
Two soundness rows leave the pairing table (wolf-interp#73, #81). They are one leniency wearing two hats: an annotation the frontend parsed and no rung ever read.
A body whose tail slot is () under a declared -> str / -> !int ran here and exited 0 — a -> !int main ending in print(…) handed the process a status by a path the program never wrote, which is #69's shape one level up. And a !T value used bare as an operand ran here too, which is 56 of wollf wl10's 2,153 verified programs passing this machine and refused by wolf 0.2.9 (E0409 x21 on +, E0401 x17 on <=). Both refuse before running now, at the resolve rung, so the record and the process agree: nothing ran, nothing printed.
Neither is a type checker arriving. Both are decided from declarations alone — a signature's return type against the SHAPE of its body's tail slot, an annotation's ! against the syntactic operand — with no inference, no unification and no literal tier, and every shape the walk cannot spell is left running. The tail's closed unit set: an empty body, a last statement that is a binding or an assignment, a while/for, a call to a function declared to return (), and a nested block. A return, a break, a loop, a match, an if, a bare path, a literal, while true (an unconditional loop diverges, and diverging is not ()), and any declared return type this machine has not resolved are all left alone. The row operand's two shapes: a name DECLARED !T, and a direct call to an item declared -> !T. f()?, f() else 0 and a match binding are never operands, and a local shadowing a fallible item is not a call to it.
[proto.cmp.triage] was run before a code was chosen, and in both rows the spec is not the defendant — which is the less common outcome and worth recording. [gram.expr.block] makes a block's value its optional trailing expression, and [gram.expr.tagident] names "the operand of return (and a fallible function's tail) against the declared return row" a CHECKED POSITION: the clause already said the tail is checked, and the implementation was not reading it. [type.interp.union] gives a !T a rendering inside a hole and marks it a carve-out in the same breath — "a reading rule, not a handling rule: ? and else still decide what the program does with the row" — which leaves an operator no reading at all. So the numbers are the corpus's own: E0401 for the tail and for a comparison (six corpus files pin fail(E0401) at phase resolve), E0409 for arithmetic and the bitwise/shift family, which is the number [type.str.concat.mix] fixes for "this operator is not defined on these operand types". Neither joins diag::UNPINNED_CODES; neither was invented. &&/|| are left alone: no clause and no measurement fixes a number for them.
Predicted walk motion: zero. Measured: zero. The prediction was that no corpus file that ran would start to be declined, because the corpus is compiler-accepted programs plus fail-pinned negatives, and a compiler-accepted program carries neither shape; the three conservatism-class fail(E0401) negatives (typecheck/if_branch.lu, typecheck/coerce_no_widening.lu, typecheck/numlit_ambiguity_named.lu) are a branch mismatch, a widening refusal and a literal ambiguity — none of them a tail or a row. The walk after the two checks is byte-identical, entry for entry: 486 entries, 375 reach run, 365 match, 16 counterparts, 42 conservatism, 62 out of scope, 1 mismatch. Nothing was correct by accident. The four witnesses are filed upstream as wolf-lang#284 so r14/r15 can pin them, with the one thing the spec could say and does not: there is no [type.fn.ret] and no [type.row], and the rule that a !T is not a T in operator position is stated nowhere directly — only implied by a clause about string holes.
[type.str.concat] — a char joins a str by + in either order (wolf-lang#278, wolf-interp#78). s + c is "{s}{c}" and c + s is "{c}{s}", the char contributing its scalar's UTF-8 bytes, and s += c is s = s + c in every place shape an assignment admits. A char is text (D58) with exactly one rendering, which is the deciding difference from an int: [type.str.concat.mix] drops the two char rows and keeps str + int / int + str at E0409, because + is not a formatter and an int has a sign, a radix and a width to choose. The mirror is two arms beside the (Str, Str) block in eval::Interp::binary, Add only; += needed nothing, because assign_binop already maps AssignOp::Add onto BinOp::Add. strings/concat_mix_char.lu leaves the out-of-scope class for MATCH with byte-identical stdout, and typecheck/closure_return.lu matches at first sight — [type.closure.return] cost this machine nothing.
A type annotation's name is resolved at last (wolf-interp#79). fn f(x: Bogus) -> int { 1 } ran here and answered 1; the compiler answers E0301: nothing named Bogus is in scope at the name. Seven book samples rested on the hole — saturating[i32], Scope, Proc — each carrying a lupin-run(…) fence that could never graduate by a compiler move, because the programs are not wolf. The pass is deliberately signature-width: a top-level fn item's parameter and return types, single-segment path heads only, recursing through prefixed types, raw pointers, tuples, error unions, fn types and every generic ARGUMENT (which is where saturating[i32]'s head sits). Methods are excluded on purpose — sema::MethodDef records the decl and the trait, not the impl block's generics, so impl Stack[T]'s fn push(self, v: T) has no scope this pass could read T from, and refusing it would be the wrong-guess failure the sema boundary exists to prevent. Qualified paths, extern signatures, dyn T, region and type are all left alone.
The prompt says did you mean :type (wolf-interp#72). type is the alias item's keyword, so type "length" and type t are both correct parse errors and both useless to the person who meant the directive. The note is appended after the diagnostic, never instead of it — type Name = int is a legal REPL line — and it fires only where the line failed to parse, which is why a bare mem still answers unsupported: mem does not resolve with no note. :help gains a first line saying directives start with :.
0.1.30 — 2026-09-09
THE MIRROR LETS else START A LINE (is42). Pin e9a17cb -> 2c03ed9, wolf-lang trunk past the s144 merge, dev-stamped: v0.2.9 is r13's and is being cut in parallel, and the clauses this sprint mirrors do not exist at v0.2.8, so the sha is the only pin this repository can record. Three rulings, four witnesses, and every one of the four is byte-identical from here. The walk goes five mismatches to one, and the one left is DIV-2026-019, which has nothing to do with this pin.
[gram.lex.newline] gains its second lookahead exception (wolf-lang#276, wolf-interp#75). A line whose first token is else continues the previous statement: at a newline the lexer looks one token ahead and, if what it finds is else, withholds the terminator that would orphan it. The trivia between — blank lines, // comments — does not count, and elsewhere is an identifier, so the four bytes must not be followed by an identifier-continue scalar. Which else it is the *binding* decides ([gram.amb.else]), never the line: } newline else { reaches the parser as } else { and f() newline else 0 as f() else 0, so the parser needed no new case — it needed one deleted. E0005 retires with the rule it enforced, and the aligned if / else if / else a reader writes without being taught to is a legal program.
E0005's *number* does not retire, and predicting that it would was this sprint's one wrong prediction. §9 of spec/01-grammar.md still reserves it — "retired … the number is never reused" — and tests/spec_extract.rs diffs our constants against that list at test time, so deleting E_ELSE_NEW_LINE would have claimed the spec dropped a reservation it deliberately kept. It stays, reserved and unreachable, as E0004 has been since 0.1.7. Retiring a code and freeing a number are two different acts; a catalog only ever performs the first. (The prediction's other miss was smaller and in the same direction: there is no E-table in this repository's manual to move a row out of.)
[conc.chan.close] spells closed and cancelled (wolf-lang#273, wolf-interp#76). is41 filed the posture with four line numbers attached and s144 ruled against this machine's spelling — the same shape as [mem.str.parse] one wave earlier, and W0603's same pact: a payload-free mark is a lowercase bare word, CapCase names a payload's type. recv's row is T ! {closed, cancelled}, {err} renders a caught row by its tag's name ([type.interp.row]), so the spelling was observable and Closed was a divergence rather than a detail. Four literals moved. The rename also found that this machine had never agreed with itself: the net tier has spelled the same condition closed since s39, and err.is_cancelled() compared against "cancelled" — a predicate that could not answer true for any value cancelled_error() ever minted.
[mem.list.pop] — the recoverable List reads answer the none row (wolf-lang#274, wolf-interp#77). The one ruling with real behavioural cost. pop on an empty list is none, not a bounds trap: [mem.ub.defined] rules indices and slices, pop takes no index, and reading it as one was an analogy rather than a clause. get outside 0..len answers the same tag, and first / last are get(0) and get(len - 1) by the clause's own words — they were not implemented here at all, so they arrive as new arms. OutOfBounds retires with them, the second CapCase payload-free mark on the builtin surface and the last. The subscript xs[i] stays the faulting twin, and a test now says so, because "these reads never fault" is the sentence a later reader over-applies. All four cite Rule::ErrUnion, which is what [mem.str.get]'s identical miss has cited since is22; the reason names [mem.list.pop] so a --trace carries the clause.
Two tests lost their provocation and neither lost its claim. Both halves of the lent-receiver proof — a_lend_hands_the_receiver_back_when_the_method_traps and the REPL's a_lent_receiver_is_back_in_its_slot_after_the_trap — rode pop on an empty list because it was the shortest trap reachable through a mut receiver, and the ruling took the only trap either had. They are re-provoked through push, which lends at the same check_home_write site and traps overflow on a literal outside the element type, after the lend and before the store. xs.len answering 0 on the next line is the same proof it always was.
Census at this pin: 520 files / 486 entries / 34 members (517/483/34); 375 entries reach run (372 at the same pin before a line was edited), 365 match (360), 16 dynamic counterparts, 42 conservatism, 62 out of scope, one mismatch — DIV-2026-019, unmoved. Anchors 445 -> 446, key sets diffed both ways: one arrives (mem.list.pop), none leaves, none moves file.
wolf-lang#275 (chan.send) is left open upstream and this pin does not reach it. wolf-interp#73 — a function's tail unchecked against its declared return type — is unmoved and stays open; it is the soundness row on the pairing table and it is the next lane's.
0.1.29 — 2026-09-09
THE MIRROR SPELLS parse (is41). Pin 5c729e8 -> e9a17cb, wolf-lang trunk past the s143 merge, dev-stamped: v0.2.9 is not cut this wave, and the clauses this sprint mirrors do not exist at v0.2.8 — 5c729e8 is an ANCESTOR of the merge that lands them, so the newest tag has nothing to mirror and the sha is the only pin this repository can record. e9a17cb is 3103577 (the s143 merge) plus one bench-only commit that folds the five new witnesses into the per-file IR-volume record; the spec/ and corpus/ trees are byte-identical at the two shas, which is why the pin moved to the later one for free. Two families ruled, and the two halves priced in opposite directions.
[mem.str.parse] — to_int's row is the mark parse (wolf-lang#265, wolf-interp#71). str.to_int() was served by every implementation and ruled by none: this one since before 0.1.13, wolf-std's corpus, the book's first chapter, and both compiler tiers from s142 — which copied the spelling from here. The clause writes the family down and renames the row on the way past. NotAnInt was the ONE CapCase payload-free tag on the builtin surface, and the language's own pact says payload-free marks are lowercase bare words while CapCase names a payload's type — W0603 warns a program that breaks it and lists parse among its examples; the OS families spell not_found, denied, closed; the json builtins already answer parse for exactly this condition, text that is not the shape asked for. One string moved (src/eval/builtin.rs), and rows/to_int_parse.lu — the single witness the two machines parted on at 0.1.28 — matches from here. tests/to_int_range.rs gains the_row_is_the_mark_parse, which reads the tag TWO ways because they fail differently: through a named match arm (a missed rename takes the fallback instead) and through {err}, which renders it by name.
[type.interp.value], .agg, .row, .union, .reason, .none — six clauses, ZERO source motion (wolf-lang#268). spec/10 §4c adopts this machine's Display byte for byte and says so in as many words: "these clauses adopt the interpreter's rendering ... it was the only rendering anyone had written down, in code". The prediction was zero and the measurement is zero. The four witnesses were run against the 0.1.28 binary at the new pin BEFORE a line was edited: strings/interp_values.lu (the whole surface in one file — a struct, a nested struct, a tuple, three List shapes, a !T in both states, a caught row with and without a payload, a qualified enum variant, and every one again in value position building a str), conc/reason_interp.lu (normal(1540), error(Corrupt), killed, fault(bounds)) and conc/chan_param_for.lu printed their pinned stdout exactly; grammar/else_default.lu printed it but for the mark. The only edits under §4c are CITATIONS: Rule::StrInterp retires the forward-namespace str.interp for the registered [type.interp.value] — the is26 move at a second address, and the forward-debt list falls from nine rows to eight — and E0412/E0413 cite the clause that now rules a format spec on a hole. [type.interp.none] is uncitable by construction: it says a conformance witness may not interpolate a value with no promised rendering, and it names channel#3 and region#1@2, this machine's bookkeeping, as exactly that.
This entry is the INVERSE of the usual one. A divergence class normally closes when this machine changes to meet a clause; here six clauses were written to meet this machine and one was written against it, and only the second cost a rename.
Two spellings this pin does not rule (wolf-lang#273, #274). Neither is a walk mismatch — no corpus witness reaches either — and this sprint does not choose for the spec. The closed-channel row is Closed here (src/eval/sched.rs:1340, read back in exactly one place at src/eval/conc.rs:674) and closed on the compiler, with Cancelled riding the same question; List.pop() on an empty list traps bounds here (src/eval/builtin.rs:1181) where the compiler answers the none row. docs/divergence-log.md carries both postures with the line numbers, and the cost of each ruling, so s144 can rule with the numbers in front of it.
Census at this pin: 514 -> 517 files / 480 -> 483 entries / 34 members; 368 -> 371 reach run and 358 -> 361 match — the three new witnesses and nothing else moving, with 16 dynamic counterparts, 42 conservatism and 63 out of scope all unmoved, and the one standing walk mismatch still DIV-2026-019. The file count moves by three rather than four because rows/to_int_not_an_int.lu LEFT: it is rows/to_int_parse.lu now, the same witness under the name the mark it pins has. Anchors 436 -> 445 with the key sets diffed both ways (nine added, nothing dropped, no owner changed) and the ratchet floor 192 -> 199: seven rather than nine, because the §4c section heading [type.interp] is cited by no witness and [type.interp.none] cannot be. Distinct conforms: tags 294 -> 301; bundle 552 -> 555 programs and 518 -> 521 records over 569 -> 572 files; conservatism ledger unmoved at 126.
0.1.28 — 2026-09-09
THE SCHEDULE IN THE MIRROR (is39). Pin 398e5f5, wolf-lang v0.2.6, the tag itself. The delta is one corpus file and two anchors, and the reading that matters is what is NOT in it.
wolf-lang#64 — sched — is deferred, deliberately, and the deferral has teeth. s139 ruled spec/07-schedule-points.md normative and admitted sched (anchors 424 -> 431, additive). That landed at ed8f526, one merge after the v0.2.6 tag: at 398e5f5 the [conf.anchor.ns] list registers eleven namespaces and anchors.json publishes zero sched.*. So this machine's eleven are exactly right at this pin, and a twelfth entry would put this side on the *permissive* half of the very silence [conf.anchor.ns.admit] was written to stop — the #239 shape, which this list wore for four pins with diag, ct, type and os. The clause says a namespace is admitted in one change or not at all; the one change is s139's, and it is not at this pin.
What lands instead is the gate that was missing on both sides: tests/anchor_admission.rs reads [conf.anchor.ns] out of the pinned spec and diffs it against anchor::REGISTERED_NAMESPACES both ways, then diffs the registered set against the namespaces anchors.json actually publishes, both ways again — every registered namespace publishes, every published namespace is registered. Until now that list was a hand-copy kept honest by whoever bumped the pin noticing, which is precisely the defect #120, #239 and #246 each name. The s139 clause text ships beside it as a planted control, so the deferral is checkable *now*, and the moment the pin advances the gate goes red until REGISTERED_NAMESPACES grows sched and its length in the type.
#242 is taken, and is38's reason for deferring it does not survive contact. [os.net.accept] arrives at this pin and names the checked machine directly: "its budgeted accept polls a non-blocking listener and retries would_block against the same budget". Nothing moved to meet it — poll_accept answers Poll::NotYet on WouldBlock and net_park measures one started against one armed(fd), which has been the posture since is18. The clause's arrival is what makes it testable rather than incidental, and it is now tested the hard way: a lost race needs two HANDS, not two PROCESSES. Two tasks in one scope accepting one listener, one dial — one takes it, the other's net_accept finds nothing there and comes back as timeout inside the budget the call began with, not a fresh one (tests/net_accept_race.rs, which asserts the elapsed bound because a count alone would not separate same-budget from re-armed). The corpus witness net/accept_race.lu reaches for os_spawn_with with a non-empty inherit set and is refused BY NAME with s137's own construct string — a verdict, never the unsupported ROW, which would be a claim about the host — so it ledgers out-of-scope, which is the row s138's own directive anticipated for this machine.
Census at this pin: 507 -> 508 files / 473 -> 474 entries / 34 members; 363 reach run and 353 match, both unmoved — the one new file is the only motion, 61 -> 62 out of scope — 16 dynamic counterparts, 42 conservatism, and the one standing walk mismatch is still DIV-2026-019. Anchors 422 -> 424 with the key sets diffed both ways ([os.net.accept], [conf.anchor.ns.admit] added; nothing dropped, no owner changed) and the ratchet floor 186 -> 187: one rather than two, because [conf.anchor.ns.admit] is a conformance clause no corpus program cites. Distinct conforms: tags 288 -> 289; bundle 545 -> 546 programs and 511 -> 512 records; conservatism ledger 122 -> 124. #239 asked nothing of this machine, as 0.1.27 predicted — both standing-waiver lists stay empty and the clause's letter simply caught up to what this side already did.
The sibling-checkout gap gets a named answer, not a build (docs/conformance-bundle.md). wolf-lang#253 names it from the other side; on this side the differential gate wants a wolf binary that CI cannot have — private submodule, a vendored snapshot that carries no crates/ by doctrine, and a second toolchain build nobody wants in a three-host matrix — so CI's only claim about diff-run today is that it correctly refuses to run. The door is already open and already walked: conformance check --replay <RECORDS> takes the counterparty as *data*, and CI feeds it this machine's own records, which is a tautology differ::retired_waivers explicitly declines to score. Vendor the compiler's records.jsonl beside PIN the way spec/ and corpus/ are already vendored and the same job becomes a real gate for one file read — with the pin-equality refusal that keeps it from going stale, the per-host record set the os tier requires, and the honest boundary that it closes the regression half and not the exploratory one.
THE MIRROR WRITES VECTORED (is40). Pin 398e5f5 -> 5c729e8, wolf-lang v0.2.8, the tag itself. The mirror was written against trunk bd7caff (the s141 merge) because v0.2.7 could not carry it: b482c52 is an ANCESTOR of b3208b6, the commit that lands the clauses, so the newest tag at the time had nothing to mirror. r11 cut v0.2.8 during the sprint and the pin moved to it before anything was committed, so the tag is the only pin this repository records — and it brings s142 along, which cost this side nothing it had not already paid.
Census at this pin: 508 -> 514 files / 474 -> 480 entries / 34 members; 363 -> 368 reach run and 353 -> 358 match, which is five of the six new witnesses and nothing else moving — 16 dynamic counterparts and 42 conservatism unmoved, 62 -> 63 out of scope (s142's fs/fstat.lu, which this machine declines by design), and the one standing walk mismatch is still DIV-2026-019. Anchors 424 -> 436 with the key sets diffed both ways (twelve added, nothing dropped, no owner changed) and the ratchet floor 187 -> 192: five rather than twelve, because three of s141's anchors are cited by the witness that ships with each, the fourth is sched.stable whose citation arrives with the pin, and the fifth is s142's os.fs.fstat — coverage counts the CITATION, so a witness this machine declines still covers its clause. The six remaining [sched.*] and s142's [os.fs] section heading are published and uncited, which is a document outrunning the corpus that names it and not a gap. Distinct conforms: tags 289 -> 294; bundle 546 -> 552 programs and 512 -> 518 records over 563 -> 569 files; conservatism ledger 124 -> 126.
s142 arrives with the tag and asks for nothing. strings/to_int.lu and rows/to_int_not_an_int.lu (wolf-lang#263) are twenty-three readings of str.to_int row for row, and both run and match here — because wolf-interp#69 landed first, below. fs/fstat.lu ([os.fs.fstat], wolf-lang#261) is a stat on an open HANDLE, and it ledgers out-of-scope with the rest of corpus/fs/: this machine has no filesystem by design (wolf-interp#18 item 6), which is a verdict and not an absence. Zero source motion for the s142 delta.
net_writev and net_nodelay ([os.net.writev], [os.net.nodelay], wolf-lang#254 / wolf-interp#67). The gathered write sends every part in order as ONE write_vectored, resumed from the byte the kernel stopped at in whichever part it stopped in — a (part, byte) cursor, empty parts skipped rather than handed over as zero-length slices, and a gather that is all empty completing with no syscall at all. Its rows are net_write's exactly, with one deliberate difference the clause dictates: Ok(0) with bytes still to send is io here where poll_write's is closed, because std calls that WriteZero and the compiler's tables answer io. invalid is NOT in the row — the clause says a typed List[List[byte]] cannot present the shape that would raise it — so the untyped shapes machinery can still build are refused BY NAME rather than given a tag no handler's arms could resolve. net_nodelay sets TCP_NODELAY either way on a TCP stream and answers io for everything else the clause lists: a listener, a unix-domain stream (the option is TCP's), a forged handle, a closed one. The DEFAULT half lives in one function, adopt_tcp: every TCP stream this table mints goes through it — accepted, dialed, or taken off the queue by a net_wait answering a readiness question — so a fourth mint site cannot forget the default.
[os.net.io] was supposed to cost nothing, and it cost exactly one thing. The clause names this machine's posture directly ("the reference interpreter polls its non-blocking sockets first and waits only on not-yet"), and that half needed no source motion, as [os.net.accept] needed none at the previous pin. But the clause also puts a write's WHOLE DRAIN under the call's budget, and corpus/net/syscall_first.lu asserts that a budgeted large net_write comes back with net_write's io. This machine answered a bare timeout — a tag outside the row net_write declares, which is wolf-interp#47's defect at a new address, because [gram.expr.tagident] makes a handler's arms exactly as wide as the DECLARED row. budget_row is the fix and the whole of the cost: a fired net_deadline answers timeout where the call declares one and io where it does not, read out of builtin::declared_row so a call cannot hold two opinions about its own row. The clause states the coarsening in those words; the bug was older than the clause that exposed it.
sched is admitted, in the same change as the pin that carries it. is39 deferred the namespace because s139 admitted it one merge AFTER the v0.2.6 tag, and mechanized the deferral so it could not go inert. This pin is past ed8f526: the pinned [conf.anchor.ns] registers twelve namespaces, anchors.json publishes seven sched.*, and corpus/test/conc_schedules_test.lu now carries a real conforms: … sched.stable tag instead of the prose comment it wore before. Both gates went red on the pin bump alone and green on the admission — which is the deferral working, not a lucky catch — and the planted s139 control is kept rather than deleted, with its assertion flipped.
wolf-interp#68 — the D57 suffix can go stale, and in a WORKTREE the invalidation set was empty outright. r10 found target/release/lupin announcing the bare lupin 0.1.27, a release claim, from a tree that had moved off the tag. This lane did not reconstruct which build produced r10's particular binary; what it did do is find a hole that is total and reproducible on demand. The probe was Path::new(".git/HEAD").exists(), and in a git worktree — which is how every lane in this org builds — .git is a FILE (gitdir: …), so the probe missed, the loop continued, and build.rs declared nothing but itself. Not a weak set: an empty one, so a branch move, a tag landing and a re-vendor were all invisible there. The metadata directory is now ASKED FOR (git rev-parse --absolute-git-dir) and a worktree's split between its own HEAD and the repository's shared refs is followed through commondir; vendor/upstream/PIN joins the set, because a re-vendor is a change of identity. tests/build_stamp.rs runs #68's own sequence against the real build.rs — build at a tag, move HEAD, rebuild, see +dev appear — once in a plain checkout and once from a worktree, with a negative control asserting the script does NOT re-run when nothing changed (a script that re-ran always would make the whole file vacuous).
What cargo cannot be made to re-run on, stated rather than implied. rerun-if-changed is an mtime comparison against a path, not a hook on a git event, and D57 asks a question no path answers directly: does a tag point at HEAD right now? The paths cover how that answer changes in practice, because each writes into .git. They do not cover a change whose mtime is not NEWER than the cached output — a fresh clone, a checkout restoring an older stamp, a coarse-granularity filesystem — nor anything outside .git and the package. So the honest fallback is not a cleverer path list: the release build is made fresh, at the tag, in a clean tree (r01's ritual, and CI's release job runs on a runner with no target/), and WOLF_INTERP_STAMP_NONCE is a declared rerun-if-env-changed escape hatch for a human who knows better than the mtimes.
wolf-interp#63 — diff-run refuses an unoptimized build of itself. The harness scores a timeout as a verdict, and that is right for a program the counterparty finishes and this machine does not. It is not right when the only thing that produced it is the profile: memory/byte_list_ledger.lu is under a second at --release and was still running at 60 against a 30-second budget under debug, and is38 wrote that row down as a load artefact because nothing in the output could tell it apart from a divergence. Of #63's three dispositions, a per-row budget is the weakest and the issue says so — it makes one row pass and leaves the cliff unnamed for the next allocation-heavy witness. Stating the profile in the verdict makes the record honest but still produces the record, and #63 exists because somebody read one and got it wrong. Refusal is the only one that makes the failure mode unreachable, and it is this repository's own precedent: the toolchain tools refuse on identity drift rather than reporting a drifted answer. The refusal lands AFTER the counterparty is resolved, so a lane with no compiler still SKIPs loudly and green whatever it was built at; --allow-debug-self is the named door, and taking it does not restore the silence — the warning names the build, every divergence carries x-self-profile, on the release side too, and the harness profile: line prints on every run.
wolf-interp#69 (wolf-lang#263, s142) — str.to_int minted an int that is not one. int is 64 bits, signed and checked; to_int parsed into an i128, so "9223372036854775808" answered a value no int can hold. Nothing rejected it at the mint site, and the program's first arithmetic on it trapped with an overflow it never wrote — a range error at the parse displaced into a trap somewhere else. Parsed as i64 now, which is what the type is, and one past either extreme is the NotAnInt row the compiler already answers (X3 forbids a quiet wrap, and there is no int the text names); the row already means "this string is not an int", which is exactly true of a number that does not fit in one. wolf-lang held the overflow input in its own crate tests so the corpus pinned neither side while the two disagreed; it can move into the corpus from here.
0.1.27 — 2026-09-06
THE CORES IN THE MIRROR (is38). r08 shipped a server's other half: several processes holding one listening address, a parent handing a listener down to its children, one call that waits on a whole SET of sockets instead of time-slicing across them, and a program that can ask how many cores it may actually be scheduled on. This release is the reference interpreter catching up to all five anchors at once, and the half worth reading is that catching up does not mean serving everything. Two of the five are refused BY NAME here, in s137's own words, because the reason the checked machine refuses them is the reason this machine does.
Released against pin 6ade878, wolf-lang v0.2.5, the tag itself; trunk's head 6263ffa carries a byte-identical spec/ and corpus/, so the check-the-tag pattern lands on the tag as it did at v0.2.4. Census at this release: 507 files / 473 entries / 34 members; 363 reach run (360 at 0.1.26), 353 match (350), 16 dynamic counterparts, 42 conservatism, 61 out of scope, and the one standing walk mismatch is still DIV-2026-019. Anchors 417 -> 422 with the key sets diffed both ways (five added, nothing dropped) and the ratchet floor 182 -> 186. Bundle 545 programs / 511 records; conservatism ledger 120 -> 122.
Three corpus files newly reach a verdict that matches, and none stopped.
wolf-lang v0.2.6 tagged while this sprint was in flight and is NOT taken here. r09's delta is one corpus file (net/accept_race.lu), spec/05's [conf.anchor.ns] amendment for #239, and a new [os.net.accept] in spec/11 for #242, the fair-accept re-wait, which is an implementation surface and not a mirror of anything in this release. A pin bump lands in its own commit (docs/manual/00-building.md), and taking one mid-sprint would have moved every census, anchor and ratchet number in the two commits already gated against 6ade878. #239 asks nothing of this machine either way: anchor::REGISTERED_NAMESPACES has carried os, type, ct and diag since the 90c90df, 77466a3, da8582d and f0da6e6 pins, so the clause's letter is catching up to what this side already did, and both standing-waiver lists (export::FILED_REGISTRY_FINDINGS, FILED_REGISTRY_HOLES) are empty.
The count of cores, and this machine's own path ([os.cpus], wolf-lang#233)
os_cpus() answers how many cores this process may actually be SCHEDULED on, which is the whole content of the clause: on linux the number honours a cgroup cpu quota and a cpu affinity mask, so a container given two cpus of quota on a sixty-four-core host answers 2, where counting processor rows in /proc/cpuinfo answers 64 and starts sixty-two workers that will never get a core between them. available_parallelism reads exactly the sources the clause names on every host, which is what makes the clause's stronger claim true here: every tier answers from the same source, so wolf run, wolf run --checked and lupin agree on the NUMBER on one host and not merely on a relation. A host that cannot answer is the io row and never a silent 1; the distinction #233 was filed about, kept where a program can act on it.
s90's os_exe() lands beside it, because the corpus calls it for the first time at this pin: net/inherit_listener.lu re-executes itself. The honest answer here is this binary (lupin is the process that is running, and a wolf program it interprets has no separate image) and the witness asserts a predicate over the answer, never a path.
Readiness over a set ([os.net.wait], wolf-lang#127)
net_wait(fds, deadline_ms) is the one s137 clause that names no host and no refusal, so it serves on every lane this machine has. There is no poll(2) to reach from here (this crate forbids unsafe and std::net exposes no readiness surface), so the question is asked of each socket in turn, on the non-blocking handles this machine has held since is18:
- a listener by accepting and HOLDING the connection:
net_accepttakes it back before it touches the socket; - a TCP stream by
peek, which leaves the bytes where they were; - a unix stream by reading and holding the bytes, because
UnixStream::peekis not stable;net_readdrains what was held before it asks the socket again, so the byte stream a program sees is unchanged.
All three keep the sentences a program can actually check, which is what the clause is made of: an empty answer is the deadline expiring with nothing ready, an answer, not a failure; readiness is level-triggered, so asking consumes nothing and two waits answer the same until something is actually read or accepted; a stream whose peer has gone is READY and the net_read that follows is what reports closed. io is a forged handle anywhere in the set (the set is validated WHOLE before a single socket is asked, so a bad member never leaves a held connection behind on a good one) or an empty set with an unbounded deadline, which nothing could ever end.
A listener several hands can hold ([os.net.listen.opts], wolf-lang#234)
net_listen_with(addr, reuse_port, backlog) serves, and the group is a MODEL that is written down where it lives rather than a SO_REUSEPORT this machine cannot reach (the option must be set before the bind, and setsockopt needs unsafe).
What the clause promises a caller is narrower than the option, and that is the opening: every dial is accepted by SOME member, and the survivor takes every dial after the others close. What the kernel does INSIDE a live group is explicitly the host's (linux distributes by 4-tuple hash, macOS hands every SYN to the newest bound socket) and a program may not depend on it, which is why corpus/net/reuse_port.lu asserts the two guarantees and prints nothing about delivery. So a group here is ONE listening socket with a duplicated handle per member (TcpListener::try_clone), which is [os.proc.inherit]'s own shape (one queue, several accepters) and both guarantees hold by construction: every member accepts from the same queue, and the queue outlives any member that closes.
Two smaller things the clause decides and this release takes: net_listen_with( addr, false, 0) is net_listen(addr), call for call, on the ordinary listener every other net call already serves; and this call admits a fixed LOOPBACK port where net_listen refuses one, because exists and denied are rows about a NAMED port and a machine that refuses every fixed port can never answer either. Non-loopback stays refused by name, and backlog is a hint std gives no setter for and no observation in the protocol can see.
The two constructs refused BY NAME ([os.proc.inherit], wolf-lang#235)
net_adopt_listener(fd) and a non-empty os_spawn_with inherit set decline here, with the construct strings s137 published for the checked machine, verbatim:
listener adoption in checked execution
fd inheritance across os_spawn_with in checked execution
Never the unsupported ROW; that would be a claim about the HOST, and both hosts this machine runs on serve the handoff. The reason the clause gives the checked machine is the reason here: this is a binary INTERPRETING a program, so a descriptor handed to "the program's child" would be handed to the interpreter's child. The mechanism seals it independently; the handoff is a pre_exec hook running dup2 and the adopt is FromRawFd, both unsafe, which this crate forbids. [proto.cmp.defined-divergence] never compares an unsupported, so nothing in the differential would ever notice these two strings drifting; tests/cores_s137.rs asserts them on the words.
An empty inherit set is served, exactly as the clause requires: it is os_spawn with the program named apart, so it reaches the spawn and answers the spawn's own row.
wolf-lang#228 / DIV-2026-021 — the locus row gets a gate, not a ruling
is38 offered two branches: implement the span comparison at that rung so the row becomes measurable, or rule the divergence and close it.
The first is already done and has been since is01. compare::compare and differ::compare_deep compare the first diagnostic's code AND span at every rung through mem, and lupin diff-run reports the row today. r08's "no harness surfaces it" is true of the harness it was written about (wolf-lang's pairing ritual compares codes at parse) and not of this one. Re-measured at the v0.2.5 pin, neither locus moved: lupin E0201 [364,365) at the comma, wolf 0.2.5 E0201 [374,375) at the end of the initializer list.
The second this lane may not take. CONTRIBUTING's standing divergence-filing rule is [proto.cmp.triage] in one sentence (neither implementation is patched to match the other before the clause is fixed) and [gram.item.let] still says what a D63 let-group IS and what the bare-tuple shape is not, without saying where refusing it reports. Moving this machine's locus onto the counterparty's would close the row with the clause having decided nothing.
So what landed is the two gates the row actually lacked, and neither touches the locus:
tests/let_group_locus.rspins this machine's half hermetically,E0201at[364,365), with the byte sliced back out of the source and asserted to be the comma. Nothing incargo testhad ever checked it: the corpus directive ischeck: fail(E0201)and the walk compares CODES, so a drift to byte 374 would have closed the divergence in silence and left the ledger asserting a disagreement that no longer existed. The counterparty's half is re-measured beside it when awolfbinary is present.differ::retired_waiversreports and GATES on a filed divergence that a FOREIGN counterparty compared clean. wolf-lang#177 taught that a waiver outliving its divergence is a hole exactly where a gate used to be, and both times a human noticed rather than a gate. The self-differential and a--replayretire nothing, because a machine compared against itself agrees with itself everywhere.
A divergence no gate can see is the shape this project keeps finding the hard way; so is a waiver no gate can retire. Both are closed, and the locus stays where the clause left it.
zext from bool, and why there is nothing to do about it here
v0.2.5's second fix makes zext from bool legal WIR, so channel[bool] stops compiling to an internal compiler error. This machine has no site for it: there is no WIR here and no lowering to verify, channel is one of the names carried in the ambient stub with no pinned semantics (so a program that uses one is declined by name rather than guessed at), and neither zext nor channel[bool] appears anywhere in the pinned spec/ or corpus/ at this pin. Recorded rather than absorbed: a fix on the other side with no mirror image is still a fact about the pair.
0.1.26 — 2026-09-04
THE BYTE HAS A DOMAIN (is37). Last release gave this machine the byte TYPE and left the DOMAIN to the compilers. sc35 measured what that cost: push(256) into a List[byte] stored 256, an un-cast int flowed into a byte slot, and eight wolf-std rows carried divergent(…), the word for "the compilers refuse this at typecheck and the interpreter runs it to an honest end", used for the first time outside the take-mode pair. All eight flip to agreement here, and two more rows with them.
Released against pin 982f857, wolf-lang v0.2.4; unchanged: s137 has not merged, and trunk's head (1323c4e, the r07 merge) is tree-identical to the tag, so there was nothing newer to take. Census at this release: 503 files / 469 entries / 34 members; 360 reach run, 350 match (348 at 0.1.25), 16 dynamic counterparts, 42 conservatism, 60 out of scope, and the one standing walk mismatch is still DIV-2026-019, filed. Anchors 417 / 182 covered and bundle 541 / 507, both unchanged; no corpus moved, only this machine's answers.
- A
bytevalue is0..=255BY CONSTRUCTION, and the refusal is what says so (wolf-interp#62).Value::Byteis au8, so 256 was never representable; what was missing is the refusal at the boundary.[type.byte]gives abyteno numeric-literal adoption "in any position" and makes it not an integer type, so anintreaching a byte slot is a type error whatever its value, a domain, not a range check. Two rules at two rungs: -sema::byte_check, atresolve, refuses anintin a byte slot and abytein anintslot with E0401 at the counterparty's own span. The two pinned witnesses now answer wolfc byte for byte:typecheck/byte_narrow_fail.lu[588,590](the65of `let b: byte = 65, not the annotation) andtypecheck/byte_elem_arith_fail.lu`[849,850](theboftable[b]). Six slots are covered (a binding's annotation, an assignment, aList[byte]push, a struct field, a call argument and a declared return type), the last two being wolf-interp#61's parameter and return boundaries answered statically, where the compilers answer them. The pass fires only where BOTH sides are known:Unknownis the default and no rule fires on one, so the sema boundary is restated for one type rather than abandoned. - The list knows its element, atrun. Until now the element context wasOption<IntTy>(integer widths and nothing else), soList[byte]()and a bareList()were the SAME VALUE at runtime, which is #62's mechanism in one sentence.ElemTysplits them,str.bytes()andnet_read_byteshand back lists carrying it, and anintreaching a byte element is refused by name. A static property never becomes a trap here, so the answer isunsupportednaming E0401, the same posture is36's byte-place rule takes, which stays where it is for the flows the static pass declines to guess about. [type.byte.op]'s widening is the guard rail, and it was measured. `b + 1is anint` by clause, so the pass says nothing about it (verified against wolfc, which accepts it), whileb += 1is refused, because the compound form stores thatintback. Comparisons do not widen:b == 119is E0401 at the119, the operand that has to change, which is what wolfc spans.- sc35's eight rows, and the two corpus twins. Re-measured on both machines with
--std-root stdat wolf-stdd71776e, every one code-and-span identical to the counterparty's first diagnostic:hex/encode_non_byte_refused,net/write_bytes_invalid_row,x/crypto/sha2|chacha20|curve25519/…,x/tls/record|handshake|cert/…(all eightdivergent(…)words retire), plusfs/invalid_row.luandx/crypto/p256/non_byte_refused.lu, ledgeredunsupportedfor reasons that had nothing to do with bytes (the fs tier is declined by design; p256's ladder is outside the modelled surface) and now answering theircheck: fail(E0401)because the refusal arrives before the decline.cargo xtask std-testover all 376 files on all three lanes reportsdivergent rows: 0and names exactly these ten as moved, nothing else on any lane. - E0301 tells the two mistakes apart (wolf-interp#60). At the 0.1.24 pin
200 as itn(a typo) and200 as byte(a scalar the pinned specification declares, which that release did not carry) answered byte-identically, and the sentence sent the reader hunting a misspelling that was not there.bytearrived at 0.1.25, so that pair is gone, but the pin runs ahead of this implementation BY DESIGN, so the collision recurs at every tag where the compiler lands a type first. The note now names the closed set of built-in type names this release carries, rendered from the check's own table so it cannot drift, and states both readings instead of asserting one. - wolf-interp#59 closes by measurement, not by assertion. D74's three layout rules landed at 0.1.25; this release verified them on both machines rather than taking the release note's word, and pinned the result in a test that reads the corpus. All five of s136's witnesses answer wolfc's code at wolfc's span: E0103
[437,441]and[598,601], E0104[520,526], E0105[575,583], E0101[1566,1568]. The issue's own table (this machine's invented E0109 at[31,49]against wolfc's E0103 at[31,35]) has no surviving row. The BOM's two positions hold: a leading mark is stripped and never a diagnostic, and mid-file the same three bytes are E0107 at the mark.
0.1.25 — 2026-09-03
THE BYTE ARRIVES (is36). Last release deferred byte by name: D72 had ruled the scalar in and s135 had not merged, so this machine answered E0301 ("nothing with this name is in scope") to every program that wrote the type. s135 and s136 merged together, and this release is the mirror catching up to both halves at once: the type, its two casts, its widening operators and its octet compares; the producers that speak it; the 1-byte ledger slot the whole ruling exists for; the unix-domain socket family beside them; and D74's string-layout codes, which this implementation had exactly one of.
Released against pin 982f857, wolf-lang v0.2.4, the tag itself, trunk's r07 merge (1323c4e) carries a byte-identical spec/ and corpus/, so the check-the-tag pattern lands on the tag as it did at v0.2.3. Census at this release: 503 files / 469 entries / 34 members; 360 reach run, 348 match, 16 dynamic counterparts, 42 conservatism, 62 out of scope, and the one standing walk mismatch is still DIV-2026-019, filed. Eight files newly reach run and not one stopped.
byteis a type here now ([type.byte], D72/s135).Value::Byte(u8), whose Rust domain IS the clause's0..=255, so an out-of-range byte is unrepresentable rather than checked. The posture ischar's, which is the ruling's own point, one rule for width-bearing scalars instead of two: no numeric-literal adoption in either direction, no closed arithmetic, nobyte as f64, and the name resolves in type position without joining[gram.inv.kw]'s closed set of 50 keywords.int as bytetruncates to the low eight bits and is the only narrowingasin the language that never traps (255/256/300/-1 → 255/0/44/255, and no W0401);byte as intwidens by zero-extension, so200 as byte as intis 200 and never -56. Every arithmetic and bitwise operator widens first and yieldsint(`200 + 200` is 400 because the term is int's), while the comparisons do NOT widen: they are total, closed, and octet order, which is the unsigned one.{b}prints the number.- Two programs the compiler rejects used to RUN here. The permissive divergence is the one that is hard to notice, and the type closed two of them at once.
typecheck/byte_narrow_fail.lu(let b: byte = 65and `let c: byte = nwithn = 300), ran to completion at 0.1.24 and printed65 300`, a "byte" holding three hundred, because an annotation naming no known scalar simply left the value alone.typecheck/byte_elem_arith_fail.luused a.bytes()element as an index into an empty list and trappedbounds, which is a true statement about a program that should never have been admitted. Both are refused by name now, which is where this machine's type mismatches live (itschartwins have sat there since s121). - The producers speak
List[byte](wolf-lang#231).str.bytes(),str_from_utf8,net_read_bytesandnet_write_bytes(the four of the eight this machine serves) are declared over the octet type, so a net echo hands its read straight to its write and converts nothing, because the types agree. The other four are thefs_*byte calls, which are not here and never were: this machine declines the whole s38 fs surface by design (wolf-interp#18 item 6), sofs/bytes_dirs.luandmemory/byte_producers_ledger.lustay out of scope for the FS tier and not for the byte one. - A
List[byte]strides by 1, and that is wolf-lang#203's whole point. sc32/sc33 measured a 64 KiB buffer held asList[int]at 16× its payload, which made a region cap on io-heavy code a fiction.memory/byte_list_ledger.lupins the RELATIONS rather than the units and every one holds here: the ledger is at least the payload, at most twice it plus a header (the width multiplier gone, only the growth history left), and the same 65,536 pushes asList[int]charge at least seven times more. A producer that already knows its length mints at EXACT capacity and pays no growth history at all. The relations are what the clause compares and the units are per-tier, so it is a coincidence worth recording rather than a claim: this machine's 64 KiB byte push charges 131,120 (the number s135 measured on the NATIVE arena, to the byte) and its exact-capacity read charges 65,568 against native's 65,584, the two headers differing by sixteen. - The phantom 16× (wolf-lang#232).
s.bytes()CONSUMED on the spot (aforhead, an index, alen) is the receiver's own view and allocates nothing, where aletbinding materializes. At 0.1.24 this machine passed the witness's three consumed relations for the wrong reason (nothing ever charged) and failed its fourth,bound_view_charges, which is the contrast that proves the region is being read at all. Only the parent expression knows which position its child sits in, so the parent records the child's span and thebytesarm asks. - D74's string-layout codes, one rule per code (wolf-lang#230). This implementation had one of the three multiline rules and spent an invented E0109 on all of it, with the whole text chunk under the span. E0103 is now a
"""delimiter sharing its line with text, BOTH sides, the fold the ruling performed; E0104 is a content line sitting left of the margin; E0105 is a margin with the right number of whitespace bytes and the wrong ones, compared byte for byte so eight tabs against eight spaces is a fault even though the widths agree. All five of s136's witnesses answer wolfc's code and wolfc's span exactly ([437,441],[598,601],[520,526],[575,583],[680,694]), measured against the counterparty at the pin, andmultiline_mixed_margin.luis the one that RAN here before, silently eating eight tabs. - The bare
{ran away to the end of the file, and a generalized literal is why. lupin had E0102 right (the family D74 confirms) and reported it at the wrong span, because[gram.lex.newline]says a newline inside an interpolation never terminates andworld"inside the open interpolation spells a *generalized* literal, whose body this lexer let cross lines.GEN_TEXT ::= (SCALAR - ('"' | NL))*excludes the newline; `RAW_TEXT ::= SCALAR*does not, sor"…"` still spans lines and the neighbouring rule is read off the production rather than assumed. The generalized refusal is E0109, the meaning D74 leaves that code. - A byte order mark is stripped at byte 0, and it broke 32 files first.
[gram.lex.source]makes a leading BOM tolerated and never a diagnostic (this machine rejected every one under an invented E0105, colliding with the margin code); anywhere else it is E0107, a stray character. The finding the pin produced is bigger than the lexer: the corpus DIRECTIVE reader did not strip it either, sogrammar/bom_at_start.lu(the first corpus file whose first three bytes areEF BB BF), lost its whole//!header, was therefore not a standalone entry (D59), joined its directory's module, and collided itsmainwith every sibling's. Thirty-twogrammar/files answered E0302[mod.dup]at the raw pin.[gram.lex.source]governs how the FILE is read, and the header is a run of comments in that file. - Unix-domain sockets (
[os.net.unix], wolf-lang#227).net_listen_unix/net_connect_unix, with the rest of the tier serving the fd call for call. The clause's point is the row vocabulary and it is implemented as the distinction it was filed to make:unsupportedis the HOST, refused by name and never a bareio; every other row is the path (exists,not_found,denied,refused).net_porton one isio(a path has no port) andnet_closeof a LISTENER unlinks its path while a stream's close never touches it. A socket path that is absolute or climbs out of the working directory is refused BY NAME, the path-shaped twin of the TCP family's loopback + port 0 discipline. A plain regular file at dial is neither of the clause's two dial cases, so the kernel decides and the hosts split (ENOTSOCK/ioon macOS,ECONNREFUSED/refusedon linux) and the test pins the four rows the clause rules while reading the fifth.corpus/net/unix_echo.lustill does not run here (its first statement isfs_exists), so the witness's three lanes are carried bytests/net_unix.rsinstead, and the tension between serving a family made of filesystem paths and declining the filesystem is named rather than papered over. - A
byteis Copy-shaped, caught the day the type landed.let b = atrappeduse-after-moveon the next read ofa, wolf-interp#50's shape one type over, wherecharassignment moved here through 0.1.16 while the compiler printed the char twice. D72 rulesbytea SCALAR, "ani8-shaped storage cell at every tier", so it joins[mem.tier0.move.3]'s POD set. Measured againstwolf 0.2.4 --checked, which prints the octet twice. - Two wrong answers the clause found and the corpus never would (wolf-interp#61 opened).
[type.byte]says a byte adopts no numeric literal "in every position", which is four positions; this machine enforced one. A literalmatcharm against a byte scrutinee took the wrong branch (match b { 65 => "sixtyfive", _ => "other" }printedotherfor a byte that is 65) andb = 66/b += 1silently retyped the variable, so every later{b}rendered an int. Both are refused now. Parameter and return positions still accept what the compiler rejects and compute the right answer anyway, which is this machine's ordinary conservatism class; thechartwin is identical in all four, so the fix is one rule at every typed boundary and it is filed rather than widened into this release. No corpus file measures any of them. - The eighteenth corpus differential, and it did not move. Counterparty built at v0.2.4, all three run-reaching tiers over 469 entries: 8/5/5 divergences (
checked/native/release), 2 filed, 6/3/3 gating, file-for-file the same table as 0.1.24's seventeenth over 452. Not one of the seventeen corpus files this pin adds diverges on any tier, and neither the byte flip set, nor the unix family, nor D74's five witnesses contributes a row. Conservatism 251/215/215 -> 257/221/221. Everything still gating is older than this sprint and older than the last one. - The unix family broke clippy on a host that has no unix family. Three
std::pathimports and onelet Some(sock)binding are used only inside#[cfg(unix)]arms, so on windows they are dead and-D warningsis a gate rather than a preference. Local green is not green, is35's lesson in a different shape, and the answer is the same one: the check that finds it runs BEFORE the push. `cargo clippy --target x86_64-pc-windows-msvc --all-targets -- -D warnings` reproduces the runner's verdict on a laptop in a minute, and linux's target does too. - Pin, census, ratchets. Pin
3befc3e->982f857, wolf-lang v0.2.4. Corpus 486 -> 503 (469 entries / 34 members): D74's five layout counter-examples,grammar/bom_at_start.lu, fourtypecheck/byte_*, twomemory/byte_*_ledger,memory/consumed_walk_charges_nothing.lu,strings/bytes_roundtrip.lu, and three undernet/. Anchors 411 -> 417 ([type.byte]with its three children and[os.net]with[os.net.unix], the net tier's first clause anchors), key sets diffed BOTH ways, six gained, nothing dropped (the #177 check run rather than assumed). Coverage ratchets 176 -> 182. Bundle 524/490 -> 541/507.
0.1.24 — 2026-09-02
THE BYTE IN THE MIRROR (is35). A diagnostic code is a name two machines say to each other, and this release is about one that meant two things. lupin answered E0103 for an escape wolf does not have ("a\qb"), while [gram.lex.str.escape] had read "any other \ is E0101 at the escape" since v0.2.2, and while wolfc's catalog spends E0103 on something else entirely: text after a multiline's opening """. A program refused for a bad escape and a program refused for a badly shaped """ were the same record here, so the differential could not tell them apart. r06 found it the moment a corpus file carried an unknown escape (wolf-lang#225).
The span was identical on both machines before the change and after it. Only the number moved, which is exactly why 484 files never showed it: #198's two witnesses pin the \u{…} DIGIT bound, where both machines already agreed at E0101, and the corpus walk compares the check: code rather than the span.
Released against pin 3befc3e, wolf-lang v0.2.3, the tag itself: trunk and the r06 merge add nothing to spec/ or corpus/ beyond it. Census at this release: 486 files / 452 entries / 34 members; 352 reach run, 335 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing walk mismatch is still DIV-2026-019, filed. Every one of the 482 files carried over from 0.1.23 is verdict-IDENTICAL, class for class, the single field that moved anywhere is one file's human-only x-unsupported sentence.
- The escape's code is E0101 (
[gram.lex.str.escape], wolf-lang#225). Not a rule this implementation had to write; a number it had to stop choosing.\q, a\xshort of its two hex digits, and a\uwith no braces all answer E0101 now, at the escape, in a plain string and inside a"""alike (MULTI_PARTreaches the sameSTR_ESC, which is the point #215's witness exists to make). Thecharliteral's own E0110 did not move with them:'\q'is still one report over the whole literal. Both codes leaveUNPINNED_CODES; they are the corpus's now.grammar/multiline_bad_escape.luarrived with the pin carrying a MEASURED divergence and flips to agreement in the commit that takes the clause. - E0103 and E0104 are now unspoken here, and that is asserted rather than documented. Freeing them showed why they were free: this implementation does not implement the layout rules they name. v0.2.3's
[gram.lex.str.multi]gives the multiline three side conditions with three codes; lupin has one,E_DEDENT_UNDERRUN(E0109), and it disagrees with wolfc on both the code and the span for each. Filed as wolf-interp#59 with the measurements, andtests/str_escape_code.rsfails if either number comes back in the meantime. - What
mainmay return is decided beforemainruns (wolf-interp#57).typecheck/main_returns_str.luused to execute its whole body and writehito the process's stdout before declining thatmainreturnedstr. The record saidunsupported@resolve, which was true; the invocation had a side effect the record did not report, which for an observation tool is a hazard rather than a wart.sema::main_return_checkruns on the admission ladder now, beside the other declaration refusals. Verdict and rung unmoved; the claim is true. §6.18 declares the boundary: the check refuses only spellings it can NAME as non-statuses, through any number of!wrappers, and leaves an unresolved path (a user alias, a struct) to the dynamic backstop, because refusing a spelling it has not resolved would be a guess that stops a program running at all. - A trap's output bytes compare when both machines hold them (wolf-lang#216), and it moved NOTHING.
[proto.cmp.phase]rules the run rung "fortrap, compare kind only", and wolf-lang#209 is the proof of what that costs: a divergence made of nothing but trap-path output, which survived from D66 to r05 with the two records verdict-identical. The widening is applied todiffer(the instrument) and not tosrc/compare.rs, which still holds the clause as written and says why. It is safe ahead of the ruling because a widened comparison can only ADD rows, and because it is gated on both sides HOLDING the field (Noneon either is honest-absent, the posture[proto.cmp.warn]already takes). The measurement: 63 trap records in the bundle, 61 write nothing before the fault; the two that do agree byte-for-byte with every wolfc tier that runs them. Zero new rows on any lane. The class is empty because the file it was built for was fixed at 0.1.23, so the counterfactual is pinned as a unit test with the real digests, because "it would have caught #209" is a claim and not a comment. The clause text is proposed on the issue. - The lex exemption is the corpus ledger, not two lists (wolf-interp#58). A
phase: noneledger means no rung completed and the ladder starts atlex, so the file is refused BY the lexer and itscheck: fail(CODE)is the code the corpus pins.tests/lex_corpus.rsand the workflow'sconform-run rungsstep both derive the set from that header now; neither keeps a name. It earned that three commits later, when the pin broughtgrammar/multiline_bad_escape.lu: under the old shape, that was the third consecutive CI red on one cause. - A lint that read the checkout path.
conc/proc_cross_module/main.lusaysuse work; GitHub checks this repository out under/home/runner/work/wolf-interp/…; W0316's ancestor walk had no stop condition that fired for a file sitting directly in the entry directory, so it climbed the whole filesystem path, foundwork, and warned, on two runners and on no developer's machine. The corpuswarns:ledger caught it. The walk stops at the package now,lints/ancestor_import/still warns at the same ident, and the regression test BUILDS a package under a directory namedworkrather than trusting a comment. Local green is not green. - Two waivers retire; the eighth row gets its own name. DIV-2026-020 closes where wolf-lang#220's own closing comment said it would, D71 ruled the span IS the offending token, s134 aligned wolfc, and seven of its eight files are byte-identical here now. The eighth was never the width question:
grammar/let_group_bare_tuple.luisfail(E0201)@parseon both machines, ten bytes apart, on every tier, this one at the comma in `let a, b`, the counterparty at the end of the initializer list. It becomes DIV-2026-021 and is filed as wolf-lang#228. DIV-2026-017 goes too: wolf-lang#76 is closed andlints/raw_interp_braces.luanswers{who}on every lane. It had stopped diverging by v0.2.2, so that waiver outlived its divergence by a release (wolf-lang#177's lesson in a smaller shape) anddiffer's test now asserts the retired entries are GONE. - The seventeenth corpus differential. Counterparty built at v0.2.3, all three run-reaching tiers: 8/5/5 divergences (
checked/native/release) against 0.1.23's 15/12/12, 2 of them filed, 6/3/3 gating after filing. Minus seven on every tier, and every one is DIV-2026-020 closing, nothing this sprint wrote caused the drop, taking the pin did. Conservatism 251/215/215. Everything still gating is older than this sprint. byteis DEFERRED to is36, by name. D72 rules the byte-width scalar and assigns the landing to wolf-lang s135, with this lane mirroring it. s135 has not merged, checked at the pin step and again at the release commit:origin/trunkat5241ab7, nos135branch, no open PR, wolf-lang#203 still OPEN, so nothing here parses the type name, sizes a 1-byte ledger slot, or implements the casts. is36 takes it at whichever pin carries s135.- Pin, census, ratchets. Pin
8cda3aa->3befc3e. Corpus 482 -> 486: #215's multiline pair and s134's two-fileconc/proc_cross_module/, the first MULTI-FILE addition in four pins, so members move too (33 -> 34). Anchors 411, diffed both ways, nothing gained, nothing dropped, which is the #177 check run rather than assumed. Coverage ratchets 175 -> 176:gram.lex.str.multi, cited by anything for the first time, because before #215 the multiline had no productions to write a counter-example against. Bundle 520/487 -> 524/490.
0.1.23 — 2026-09-02
THE LETTERS IN THE MIRROR (is34). Three letters came back from r05 with v0.2.2, and the one that mattered was a sentence about death: a trap runs no defer or errdefer, anywhere. is33 implemented the proc half of [conf.trap.exit] and left the ROOT path alone by name, because the clause ruled the proc and was silent about the root, the correct posture when the spec is the defendant. wolf-lang#209 closed that silence the consistent way, and lupin's root domain took it: faults/trap_skips_root_defers.lu printed inner inner-defer before-trap root-defer at 0.1.22 and prints inner inner-defer before-trap here, which is what all three wolfc lanes print. The inner block's defer still runs, at that block's own exit, before the trap; that is an ordinary scope exit and not a trap path at all.
The second letter is why nobody could see the first. Through 0.1.22 this implementation reported stdout_inline: null on every trapping program, so the two machines were verdict-identical whatever they printed, and #209 (a divergence made of nothing but trap-path output), survived unmeasured from D66 to r05. wolf-interp#55 is fixed: the record now carries the output for every verdict that reports a completed run. Two records moved over the 487-record bundle, both traps, and both agree.
The third is a kindness. wolf-interp#56: the D67/D69 comma refusals now say what to write and point at where it goes, which the strict implementation (the one that has always demanded the separator) was the one not saying.
Released against pin 8cda3aa, wolf-lang v0.2.2, the tag itself: trunk and the r05 merge add nothing to spec/ or corpus/ beyond it, so the check-the-tag pattern lands on the tag rather than a merge sha. Census at this release: 482 files / 449 entries / 33 members; 350 reach run, 332 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing walk mismatch is still DIV-2026-019, filed. Every one of the 479 files carried over from 0.1.22 is verdict-IDENTICAL, class for class.
- A trap runs no defers, anywhere (
[conf.trap.exit], #209). One guard, one sentence.eval_block's trap arm drops itsinside_proc()half, so the same rule governs both domains: inside a proc the trap is contained at the boundary and runs the killed-proc sequence below it (s132's amendment, unchanged); in the root domain death is immediate and the pending scope-exit effects of the trapping scope and of every scope enclosing it are abandoned. A trap is not an error value and never unwinds ([abi.native.nounwind]); effects that had already run, ran.inside_prochad no other caller and goes with it, andSignal::TrapjoinsProcKilledandExitin the unreachable arm because a trap now returns before the error path is ever computed. Three unit litmuses carry the claim where the corpus cannot reach: the reduced witness, the multi-FRAME form (callee defer, caller defer AND caller errdefer, all abandoned), and the boundary case. - The observation the ruling took away, given back. A method call may LEND its receiver (the slot holds a placeholder while the builtin runs) and the lend must end whatever the call did, a trap included. That was tested from a
deferon the trap path, which no longer runs, so in a compiled program the restore is not observable from wolf code at all. It moved to where a session outlives its trap: the newrepl_session::a_lent_receiver_is_back_in_its_slot_after_the_trap([repl.trap.alive]) traps onpopof an emptyListand asksxs.lenon the very next line. It answers 0; a leftover placeholder would refuse with "()has no memberlen". - The record carries the trap's output (wolf-interp#55).
[proto.record.fields]'s "whenever the program wrote output" is read as a floor rather than a ceiling: the digest and the inline text are present forexit,trapandubalike. The movers, all of them, compared field for field with thecommitstamp excluded,faults/trap_skips_root_defers.lugains"inner inner-defer before-trap"androws/handler_diverge_trap.lugains"FAILED: neg\n", each byte-identical to thestdout=its corpus directive pins for the counterparty. 63 trap records in the bundle; the other 61 trap before writing anything, and none of the 8ubrecords writes first. So the answer to "is anything else hiding behind the null?" is, at this pin, no. What did NOT change is the comparison:[proto.cmp.phase]still says "fortrap, compare kind only" andcompare/differstill implement exactly that, because widening it by private agreement is what the independence doctrine forbids, filed as wolf-lang#216 instead. - A conservatism, declared and filed rather than papered over (§6.17). Dropping the verdict condition entirely would move a third record:
typecheck/main_returns_str.luwould gain"hi\n", because this machine evaluatesmain's body and only then declines thatmainreturnedstr. A record whosephase_reachedsays the run did not complete carries no output. That it printed at all is a real defect (the decline belongs on the admission ladder) and it is filed as wolf-interp#57 with a standing test, not smuggled onto the wire. - The comma refusals teach the comma (wolf-interp#56).
Diaggainshelp: a zero-width insertion point and the text that belongs at it, rendered as a second line. All five D67/D69 list closers use it, struct literal, struct pattern, tuple and tuple-struct patterns, closure parameters, inline-C capture lists:
E0201: expected }, found identifier y; the members of a struct literal are separated, add the comma [gram.expr.primary] at 18:26 the comma goes here at 18:25
Span parity holds and is asserted structurally: every primary span is byte-identical to 0.1.22's, because the note is additive and never a relocation. The multi-line form is the one that needed care (D69 reports at the FIELD the missing comma should precede while the comma belongs after the previous field, one line up), so the insertion point is captured before the terminator run is skipped. The note is not a blanket suffix: an empty list, a list that ended at its closer, and end-of-input (E0203) get none, because "add the comma" would be teaching a lie there. Wording is outside the differential protocol (D22), so no record moved.
- #198's string half answered at first sight.
\u{…}inside a string literal at every width the bound admits:strings/str_uni_leading_zeros.luruns (\u{41},\u{0041},\u{000041}all spellA) andgrammar/str_uni_seven_digits.luis refused E0101 at the escape, line 22 column 14, the same column, code and message itschartwin gets at 21:14. The spec now derives both from one production (STR_ESCcarries the set,CHAR_ESC ::= STR_ESC | '\''), so the "binds in string literals too" prose is a claim read off the grammar; this lexer implemented the prose at 0.1.21 and the pair proves it. The corpus now has two files that stop at the lexer,UNI_ESC's two doors. - The sixteenth corpus differential, the first since 0.1.11. The counterparty was built at v0.2.2 and all three run-reaching tiers were run: 15/12/12 divergences (
checked/native/release), 9 of them filed, 6/3/3 gating after filing. The three letters agree on every lane, the four new witnesses appear in no divergence report at any tier, so #209 is closed against the real counterparty and not merely against r05's transcript. Everything gating is older than this sprint. DIV-2026-020 (wolf-lang #220) is the dominant class: eightgrammar/files, same code, same byte where the refusal starts, different span WIDTH, this machine spans the offending token, the counterparty emits a zero-width span at its start. s132/D69's "byte-for-byte where lupin points" is true of the offset and not of the span, and nothing measured it because the walk compares codes. Neither side moves until the clause rules. The residue is written into a "triage owed" table rather than filed half-analysed. - Pin, census, ratchets. Pin
2bfbe5e->8cda3aa. Corpus 479 -> 482, all three entries. Anchors 411, unmoved; the amendments are sentences inside clauses that already existed. Coverage ratchets 173 -> 175, and both new anchors are clauses the corpus had never cited:conf.trap.exit(that no file anywhere named it is precisely how its root sentence stayed unwritten until #209) andgram.lex.str.escape. Bundle 517/484 -> 520/487.
0.1.22 — 2026-09-01
THE MIRROR HOLDS (is33). The region cap lands whole-pipe, and with it the last thing lupin 0.1.21 deferred by name: region r(cap: n) bounds a region's ledger, a charge past the budget is trap(alloc-contract) at the allocating site, and inside a proc that trap is contained at the boundary and reaches the join as fault(alloc-contract), s132's ruled shape for [mem.region.cap.1-3] and [conc.proc.exit] (D68, wolf-lang#187, now closed). All three of the wave's cap witnesses flip from parse-refusal to running, and they compare relations rather than byte counts for the reason the account witnesses did: the clause denominates the budget in each tier's own ledger units, and wolf-lang#203 measured an order of magnitude between ledger units and payload bytes. Released against pin 2bfbe5e, wolf-lang trunk's head, the s132 merge; no v0.2.2 tag existed at the release step, so the merge sha is the pin (the is31 check-the-tag pattern). And the release now HAS binaries: wolf-interp had no dist workflow at all, so 0.1.22 is the first lupin with archives on its release page for all four tier-1 hosts, including the lupin.exe the learners' path has been waiting for. Census at this release: 479 files / 446 entries / 33 members; 348 reach run, 329 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing mismatch is still DIV-2026-019, filed. Every one of the 474 files carried over from 0.1.21 is verdict-IDENTICAL, class for class.
- The cap (
[mem.region.cap.1]), one field and one compare. A region may carry a creation-time byte budget, writtenregion r(cap: n) { … }(the cap parenthesis follows the NAME) orregion(cap: n)/ `region(rc, cap: n)(strategy first, cap last).Region.capis the field;Store::admits` is the compare, written once and called from both charge sites, the allocation charge and the growth realloc. The test is strictly `charged + bytes > cap`, so a ledger standing exactly at the cap is not a breach and the next byte is. A refused charge does not move the ledger and does not tick the allocation counter: the trap fires at the site, so the allocation never happened. Growth is judged against the container's birth region, because that is where[mem.region.account.1]attributes the charge, a root-born list grown insidein r { … }is measured by the root's budget, never byr's. Caps are per-region, never per-forest. - Its domain (
[mem.region.cap.2]).cap:takes anint, evaluated ONCE at creation and before the region exists, so nothing the budget expression does can charge the region it bounds. A negative budget istrap(alloc-contract)at the *creating* site, one span, nothing allocated.cap: 0is legal, and a region that never charges lives happily under it. The process root and a proc's own arena carry no cap, because the clause puts the budget where a program writes one. - An anonymous sugar block cannot carry a cap, and not by choice: the value arm claims the parenthesis whenever no name precedes it, so `region (cap: n)
IS the value form and no other reading exists ([gram.expr.region]`'s own disambiguation).capstays contextual ([gram.inv.ctx]), only the two-token shapecap:inside a region's parenthesis opens the clause, solet cap = 7andregion cap { }are untouched. - The contained trap is
fault(kind)([conc.proc.exit]). This machine has contained proc traps since is06 and spelled the reason `error("trap", kind), which made a crash answeris_error()` true, a class error the amended clause names. The fifth member of the closed set is now its own reason: the join readsis_fault()for the class andis_alloc_contract()for the one kind a budget contract names. The predicate reads the payload rather than minting a tag per kind, because[conf.trap.set]is the closed vocabulary the payload is drawn from. wolfgang's wire packs (class 4, kind 9); lupin's reasons are structural tags, so the PREDICATES are the comparison surface and the wire is not, which is exactly why the witness pins booleans. - Teardown is free-then-deliver (
[mem.region.cap.3]). The dying proc's regions are reclaimed wholesale BEFORE the reason publishes, so a join readslive_region_bytes()at its pre-spawn value and no postmortem query can observe a dead proc's charge. The scheduler cannot free anything (it never touches the store) and so delivered first; reasons are now PARKED with the exit's message and vector clock, handed back with the regions, and published once the memory is back. s132 measured the reverse order as a live monitor race on the native machine; lupin's cooperative baton HID the same window rather than closing it, and a hidden window is still a window. - Below the boundary, no further user code. Containment runs the killed-proc sequence (
[conc.proc.kill]), so the frames between the trapping site and the proc root unwind silently,defer/errdeferincluded.eval_blocktreated a trap as the error path and ran them. In the ROOT domain a trap is still process death by[conf.trap.exit]and that path is untouched: the clause carves out the proc, not the program. - A late monitor gets the real reason. Monitoring an already-exited proc delivered a reconstruction from a stored LABEL, which meant
normal(unit)for anerror(e)proc, and would have meant it for a faulted one.Proc.exitednow holds the whole reason. - D69's twin check, no code. lupin already refuses lax literal and closure separators, and the span parity is the interesting half: `Point { x: 1 y: 2 }
reports aty(18:26) andfn(a b)atb` (14:18), the token the missing comma should PRECEDE, byte-for-byte where s132 measured wolfc pointing. This parser's letter was the measured one, as it was for D67's trio at the previous pin. - The dist lane exists (D57). wolf-interp had no release workflow; every lupin release since 0.1.8 was a tag with no binary behind it.
.github/workflows/release.ymlmirrors wolf-lang's r03 dist matrix at all four tier-1 hosts and adds what that stub leaves to each repo: the D57 stamp is ASSERTED (the tag is matched againstCargo.toml, and the BUILT binary's--versionis read back and refused if it carries+dev.), and every archive is unpacked outside the checkout and smoke-run before it can reach the release page..zipon Windows,.tar.gzelsewhere, one directory each; on Windows the barelupin.exeis uploaded beside the archive so a learner-facing page can link one URL (wolf-web ww09). - The differential, before and after. The five entries the pin adds, at 0.1.21's binary and at this release:
| witness | 0.1.21 | 0.1.22 | |---|---|---| | memory/region_cap_boundary | fail(E0201)@parse — MISMATCH | exit(0) "at-cap true \| zero-cap true \| query true" — match | | faults/region_cap_breach | fail(E0201)@parse — MISMATCH | trap(alloc-contract) — match | | conc/proc_cap_fault_join | fail(E0201)@parse — MISMATCH | exit(0) "fault true \| alloc-contract true \| reclaimed true \| main-defer-ran" — match | | grammar/struct_literal_no_separator | fail(E0201)@parse | fail(E0201)@parse — match | | grammar/closure_params_no_separator | fail(E0201)@parse | fail(E0201)@parse — match |
Three witnesses flip (every one of them the sprint's own work, since 0.1.21 deferred the cap by name and cap: was unknown syntax) and two answer at first sight, at the same code and the same span.
- Ledger movements. Anchors 407 → 411 (
+mem.region.capand its three children; the key SETS were diffed both ways, nothing dropped, the wolf-lang#177 lesson). Coverage ratchet 168 → 173: the three cap clauses, plus two the wave cites for the FIRST time ever,conc.proc.1, the failure-domain clause every proc program leans on and none had ever named, andgram.expr.closure, which D69's closure pin brings. Bundle programs/records 512/479 → 517/484. The trap vocabulary's reachable set grows to nine of twelve:alloc-contracthas a program at this tier now.
0.1.21 — 2026-09-01
THE LEDGER IN THE MIRROR (is32). The region machine learns to say how much it has charged: region_bytes(r) and live_region_bytes() land against [mem.region.account.1/.2] (s131, wolf-lang#187), and the interesting part is *what the two machines are allowed to disagree about*. The clause leaves the UNITS per tier ("what charges, and by how much, are implementation-measured facts per tier, not comparison surface") and pins four RELATIONS instead. So lupin's ledger is its own honest arena model, the native tier's is alignment-rounded arena storage, the checked machine's is its shadow memory, the three numbers differ, and the two witnesses still agree byte for byte, because what they print is booleans. That is the differential working as designed rather than in spite of a divergence. Released against pin e6cf24e, wolf-lang trunk's head, the s131 merge plus the 2026-09-01 ledger ritual; the newest tag v0.2.1 sits seven commits behind it, so the merge sha is the pin (the is31 check-the-tag pattern). Census at this release: 474 files / 441 entries / 33 members; 345 reach run, 324 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing mismatch is still DIV-2026-019, filed. Every one of the 463 files carried over from 0.1.20 is verdict-IDENTICAL, class for class.
- The byte ledger (
[mem.region.account.1]). Every region carries a cumulative count of the bytes charged for allocations placed in it, readable throughregion_bytes(r)over a sugar block's name or a first-class region value. The three contractual relations fall out of the arithmetic rather than being asserted on top of it: zero at creation is a fresh region'sbytes: 0; monotone within the lifetime is never subtracting, a growth realloc charging the whole new buffer while the abandoned one stays charged; stable between allocations is a push inside the already-granted capacity charging nothing at all. Reading a ledger is a *touch*, so a region value that outlived its region's wholesale free faults atregion_bytesexactly where it would atin r { … }. - The live total (
[mem.region.account.2]).live_region_bytes()sums every unfreed named region's ledger; the process-root arena is never counted, and a free takes its whole row at once, soregion scratch { … }returns the total to its entry reading by construction. AFrozenregion keeps contributing: it is never freed, which is its specified end state ([mem.region.freeze.1]), not a leak. - Birth-region attribution. A container's storage charges the region that was ambient at its ALLOCATION site, not the one ambient at the push,
[mem.region.create.3]'s rule, which this machine has carried onValue::homesince is16. Growing a root-born list insidein r { … }movesr's ledger not one byte. - What a byte means here, written down. There is no arena in this machine: is02 made a
Valuea plain owned Rust tree, so the real bytes are the Rust allocator's business and would be neither stable across builds nor meaningful to a wolf program.eval::region::ledgertherefore models the storage the same program would take in a compiled arena (a 16-byte grain, a 32-byte allocation header, 16 bytes a value slot, astr's UTF-8 length, container capacity in powers of two from four (this machine's own policy, never Rust'sVec)), so the same program charges the same bytes on every host and every build. It is a high-water accounting, never an address, a placement, or an RSS proxy. One measured units divergence, recorded:strcharges the ambient region here where the native tier charges the process root (wolf-lang#191, the c09 seam). The clause anticipates it in as many words, and the relations are what the witnesses compare.docs/approximation-contract.md§6.15 is the contract. - The cap half is DEFERRED BY NAME. wolf-lang ruled it D68 on 2026-09-01 (a cap breach is
trap(alloc-contract)at the allocating site, contained at the proc boundary per[conc.proc.1], no unwinding and no catchable row) but no cap syntax, no fault semantics and no clause have landed on wolf-lang trunk at this pin, so there is nothing here to implement against. NoRegion.capin 0.1.21; it is the is33-era twin, and wolf-lang#187 stays open tracking it. - wolf-interp#53 closes, both gaps.
--explore --jsongains a per-outcomeschedule(the decision stream asev:c0,c1,…, the spelling--schedule=takes and the only one that survives a stream too deep for a packed seed) besideseedand the existingreplay; the human line and the JSON member share one renderer, so they cannot drift. And a seeded record echoes its request asx-seed/x-schedule, soconform-run --json --seed=S > artifact.jsonnames the schedule it replays instead of leaning on the invoker. They arex-EXTENSION keys:[proto.record.fields]fixes the record's field set and only a wolf-lang clause may grow it,[proto.record.ext]exists for exactly an implementation fact the counterparty need not match, and[proto.cmp.defined-divergence]rules an absentx-key never a divergence. lobo'slobo-replaydouble-explore-and-awk workaround is the acceptance test, run without either workaround. \u{…}'s digit count is E0101 at the escape. The pin's only other implementation work, and the corpus caught it: r04's amended[gram.lex.char](wolf-lang#189) makes the one-to-six bound the ESCAPE's shape rule rather than the char literal's, so seven digits or none is E0101 at the escape where this lexer filed E0110 over the whole literal, and the clause says the bound "binds in string literals too", where lupin had no bound at all and quietly decoded"\u{0000041}"toA. The spec is unambiguous, so the implementation was the defendant ([proto.cmp.triage]). E0101 leavesdiag::UNPINNED_CODES, and the corpus gains its firstphase: noneentry, a file the LEXER refuses.- The differential, before and after. The eleven entries the pin adds, at 0.1.20's binary and at this release:
| witness | 0.1.20 | 0.1.21 | |---|---|---| | memory/region_bytes_query | unsupported@resolve — "live_region_bytes does not resolve" | exit(0) "create true \| grew true \| stable true \| live_up true \| reclaimed true" — match | | memory/region_bytes_value | unsupported@resolve — "region_bytes does not resolve" | exit(0) "create true \| attributed true \| birth true" — match | | grammar/char_uni_seven_digits | fail(E0110)@lex — MISMATCH | fail(E0101)@lex — match | | lints/region_call_allocates | exit(0) | exit(0) — match | | memory/region_unit_tail_call | exit(0) | exit(0) — match | | grammar/defer_loop_turn | exit(0) | exit(0) — match | | grammar/match_arm_or_over_product | exit(0) | exit(0) — match (c06 residue) | | grammar/match_arm_or_inside_product | exit(0) | exit(0) — match (c06 residue) | | grammar/struct_pattern_no_separator | fail(E0201)@parse | fail(E0201)@parse — match | | grammar/struct_pattern_rest_bare | fail(E0201)@parse | fail(E0201)@parse — match | | grammar/tuple_pattern_no_separator | fail(E0201)@parse | fail(E0201)@parse — match |
Two witnesses flip (the ones this sprint exists for), one is fixed (the escape), and eight answer at first sight against work already in this tree, D67's comma trio included, whose letter this parser already held, and #196's two or-pattern residue pins, where the compiler's native pipe refuses by name and this machine runs: a divergence wolf-lang filed on purpose, carried in the differ's ledger rather than papered over.
- Ledger movements. Anchors 404 → 407 (
+mem.region.accountand its two children; the key SETS were diffed both ways, nothing dropped, the wolf-lang#177 lesson). Coverage ratchet 165 → 168 (+mem.region.account.1,+mem.region.account.2, and+gram.lex.char, a clause the corpus had never cited until the escape witness). Bundle programs/records 501/468 → 512/479.
0.1.20 — 2026-08-31
THE ARMS AGREE (is31). Match arms take the product domain, and with them the last pattern asymmetry between the two machines closes: the c06 refusal family s130 retired on the compiler's side had a deliberately symmetric twin here, and it dies in the same motion (s130, wolf-lang#179). Released against pin b80d239 (wolf-lang's s130 merge; no v0.2.1 tag existed at the release step, so the merge sha is the pin, and the spec tree it carries is BYTE-IDENTICAL to 0.1.19's 83f83bb (404 anchors, nothing gained, nothing dropped), s130's whole delta is lowering, corpus and CHANGELOG). Census at this release: 463 files / 430 entries / 33 members; 338 reach run, 313 match, 16 dynamic counterparts, 42 conservatism, 58 out of scope, and the one standing mismatch is still DIV-2026-019, filed. Every one of the 455 files carried over from 0.1.19 is verdict-IDENTICAL, class for class.
- Struct patterns work in
matcharms ([gram.pat.struct], s130/#179). An arm is a CONJUNCTION of field tests over the value's own shape, exactly as a tuple arm is a conjunction of element tests: literal fields test, shorthand and renamed fields bind,..ignores the rest on purpose, and sub-patterns nest, through enum payloads (Dot(Point { x, y: 0 }), `S((a, b))), through@-bindings over products (q @ Point { x: 0, .. }`, whose binds a guard can then read), and through each other. The field-set rules hold in arm position exactly as they do in a binder, so an unknown field still declines by E0403's name and a missing-without-../ duplicate / empty one by E0814's, never guessed past. Where 0.1.19 answeredunsupported("deferred with the product match domain"), the arm now runs. - The arm boundary takes the WHOLE scrutinee.
[mem.tier0.move.1]'s initialization reading is what gives a BINDER its field-wise story (0.1.19's element-move work); no clause extends partial moves to arms, so neither machine invents finer-grained arm semantics. An arm that binds a non-Copypiece moves the scrutinee whole, and the field no arm touched is use-after-move afterwards, E1001's dynamic counterpart at the same site. Testing is not taking: the scrutinee is only read to run the arm chain, an all-Copyarm leaves it live, a failing guard takes nothing, and a scrutinee that is no place moves nothing. - E0802 reaches product arms (
[ty.match.reachable]). The reachability walk widens column-wise: an earlier unguarded arm kills a later one when it covers it column by column, an all-binder product is the catch-all later arms die behind, and the scalar_-after-true/falserule generalizes to "two arms that split a bool COLUMN and constrain nothing else close the shape". Literal precision is kept: every column this static walk cannot judge is opaque, so it neither covers nor is covered, and a guarded arm still covers nothing. - The differential, before and after. The seven struct-bearing witnesses of #179's table, at 0.1.19 and at this release:
| witness | 0.1.19 | 0.1.20 | |---|---|---| | grammar/struct_pattern_match_arm | unsupported@resolve (out-of-scope) | exit(0) — match | | grammar/match_arm_product_nested | unsupported@resolve | exit(0) — match | | grammar/match_arm_at_binding | unsupported@resolve | exit(0) — match | | grammar/match_arm_deep_tree | unsupported@resolve | exit(0) — match | | typecheck/match_arm_product_unreachable | unsupported@resolve | exit(0) + E0802 — match | | memory/match_arm_whole_move | unsupported@resolve | trap(use-after-move) — E1001's counterpart | | typecheck/match_arm_product_nonexhaustive | unsupported@resolve | exit(0) — conservatism |
The tuple twin (tuple_pattern_match_arm) and match_arm_str_in_product already agreed and still do. Six of the seven join the agreement class outright; the seventh is honest conservatism, not agreement; exhaustiveness is the type checker's and E0801 has no dynamic half, so match_arm_product_nonexhaustive sits beside match_missing and match_str_nonexhaustive in the same column they have always occupied.
- The c06 residue, stated row by row. The compiler's NATIVE pipe still refuses four shapes by name; this machine runs all four, and the checked lane runs the first two, so the first two are a recorded non-nesting rather than a divergence: an enum or row test inside a product (deep trees), a str literal at product depth, a float literal at product depth. The or-pattern rows (
(0, true) | (1, false)over a product, and `(0 | 1, true)` inside one) are the flagged pair: refused by name natively, run here, and the checked lane's posture on them is not something this repo can measure. Filed on wolf-lang#179 for the residue's own sprint, not fixed silently in either direction.
0.1.19 — 2026-08-31
THE SHAPE BINDS (is30). Struct patterns land whole-pipe from the [gram.pat.struct] text alone (s129, wolf-lang#179), the destructure tier they generalize learns the element-move discipline it owed since s128, and the s106 net byte tier resolves at last (F-0102). Released against pin 83f83bb (wolf-lang's s129 merge, two bumps past 0.1.18's addcd7f) and the pin question 0.1.18 left open is closed twice over: the first bump (v0.2.0, c88ab64) retired the FILED_REGISTRY_HOLES waiver for wolf-lang#177 the day r03's spec-extract fix re-gained gram.lex.ident (403 anchors, zero export notices), and the second bump took the s129 merge itself the moment it reached origin, so the full witness set rides the census (404 anchors, +gram.pat.struct). Census at this release: 455 files / 422 entries / 33 members; 329 reach run, 306 match, 15 dynamic counterparts, 41 conservatism, 59 out of scope, and the one standing mismatch is still DIV-2026-019, filed. Every pre-existing file is verdict-identical across the span except the two movers named below.
- Destructures move element-wise (
[mem.tier0.move.2], the s128 discipline).let (x, _) = pmovesp.0ONLY: each element is its own place, a wildcard touches nothing, copy-shaped leaves still copy, and the untouched elements stay readable, where 0.1.18 moved the whole tuple and trapped the sibling read. The corpus twins pin both halves (destructure_partial_liveruns to "1 2";destructure_partial_movetraps use-after-move at the element that DID move, E1001's counterpart), and the differ's standing "element-story gap" asymmetry closes with them. - Struct patterns (
[gram.pat.struct], s129/#179).Point { x, y: p, .. }in every binder position (let/var, D63 comma groups,forheaders) with shorthand binding the field's own name, explicit sub-patterns nesting through structs and tuples both ways, fields in any order, and..ignoring the rest on purpose. Named fields consume their ownProj::Fieldsub-place per the tuple precedent: omission,..and wildcard fields stay live, and the fault twin traps at the field that moved with the counterparty's span. The refusal classes decline by the code that owns them upstream (unknown field (E0403), missing-without-../ duplicate / empty (E0814)), never guessed past. Match ARMS defer symmetric with the compiler's own c06 product-domain refusal: the arm witness answersunsupportedon both machines, and the two advance together the day the product match domain lands. The ten-file s129 witness set compares clean across the machines: six byte-identical agreements (the binder sweep and partial-live among them), one dynamic counterpart, two refusals-by-name, one unsupported-both-machines, zero divergences. - The #184 twin joins the agreement class. lupin ran the lent-view byte slice all along; with fd42622's compiler fix in the pin,
byte_view_slice_lentanswers the same bytes on both machines (4 119 4 108 2 0) and the whole slice quartet agrees, fault twin included. - The net byte tier resolves (wolf-interp#52, wolf-std F-0102).
net_read_bytes/net_write_bytesland as the str calls' own shape withList[int]marshalling: one receive of up tonraw bytes with NO utf8 row (a lone0x80is data), whole-or-raise writes behind the WHOLE pre-write check (an element outside 0..=255 is theinvalidrow and nothing reaches the wire, §14's fs vocabulary, which wolf-std's facade adopts verbatim), rows declared for #47's arm discrimination. The two byte-tier corpus witnesses (net/byte_roundtrip,net/line_reader_bytes) leave the conservatism ledger for the match column (the release's two deliberate verdict movers) and wolf-std's compiler-lanes-only rows can go three-lane at its next pin.
0.1.18 — 2026-08-30
THE FIRST ARM YIELDS (is29). The two thrice-measured lupin-side correctness debts behind wolf-std's four divergent(…) ledger rows, paid from the spec clauses and the F-0079 lineage, never from the compiler's source: a multi-arm handler over a BUILTIN-raised row took its first arm for every tag (#47, wolf-std F-0097), and take-mode reuse (a static E1001 on both compiler rungs), executed here to its dynamic outcome (#48, wolf-std F-0098).
Released against pin addcd7f, unchanged from 0.1.17; wolf-lang has no v0.2.0 tag at this release, so the FILED_REGISTRY_HOLES waiver for wolf-lang#177 (gram.lex.ident) carries to is30 and the pin question re-opens there. Every pre-existing corpus file is verdict-identical before and after every commit in this span: the judge counts are 310 run / 287 match / 13 counterpart / 41 conservatism / 55 out of scope / 1 mismatch, byte-for-byte the 0.1.17 per-file table, three times over.
- The row rides with the raising builtin (#47). The #29 mechanism at its third address: entry-file raises discriminated (s70), imported-module raises were fixed by 0.1.13's arm-selection pass (the row travels with the value), and a BUILTIN's raise carried
row: [], so a handler's every lowercase arm read as a binding and the first arm matched every tag, silently, exit 0. Builtin error values now mint with the raising builtin's WHOLE declared row (eval::builtin::declared_row, the net and process prelude signatures the module docs pin, the env/json/cwd/utf8 mint-site closures), so sibling arms resolve as tags and the value's own tag finds its own arm in either order. Witnessed in both arm orders on the issue's reproducer (one dead port, dialed twice:-1/-1where 0.1.17 answered-1/-9) and hermetically onenv_get's two-tag row. The lint walk'soperand_rowreads the same table, so the static arm rule and the dynamic one keep answering alike. The spec pins none of these signatures, filed as wolf-lang#181 per the is26 pattern rather than absorbed. - Take-mode reuse joins the static rung (#48). The E1007 discipline at the moved place: an explicitly moded argument or receiver over a whole binding an earlier call-site
takemarker consumed isfail(E1001)at the reuse argument (the counterparty's code, span and message shape, observed ataddcd7f), instead of executing to the trap map's answer. Straight-line certainty only: re-initialization and shadowing clear ([mem.tier0.move.4]), moves inside branches, loops, closures anddefernever leak past them, field-granular takes are not tracked, and a bare unmarked READ of a moved-from place stays[mem.tier0.move.2]'s dynamictrap(use-after-move), which is what keepsmemory/move_use_after.luon its DynamicCounterpart verdict andfaults/use_after_move_field.luon its pinned trap, unmoved. - W0317 lands the lupin half of wolf-lang#167's D61 row. The kindness lint (
[gram.expr.index.origin]): an int literal fed to a List local's.getinside a 1-origin scope warns at the literal, span parity with the compiler on the corpus witness ([468,469]onlints/index_origin_get.lu), the statement marker narrows and the innermost wins, a non-literal index and a user type'sgetstay silent (probed: the compiler does not warn there either). The lint replays the parser's ownindex_origin_ofreading, one marker grammar for two consumers.
Downstream, pre-recorded in wolf-std's row comments and verified on this build directly against wolf-std's tests/ before tagging: net/refused_row and net/closed_row print their directives' stdout (handled: -1 / peer-gone: handled, then the propagated tag), and net/use_after_close + process/use_after_wait are fail(E1001) where 0.1.17 trapped use-after-move / diverted at start(), the four divergent(…) words flip to run at sc-track's next binary bump, which is the red the runner was built to read.
0.1.17 — 2026-08-29
THE INTERPRETER KEEPS TWO PROMISES (is28). Two user-visible fixes, both measured live on 0.1.16: a scratch directory where every file says //! member: false still collided on main (#49, the run-door half of D59 was never landed), and the origin marker the language ruled in D61 either failed at lex (#![index(1)] → E0101) or, in the dangerous class, was a silently ignored statement attribute that ran grammar/index_origin_scopes.lu to the WRONG answer (wolf-lang#169). Both promises implemented from the ruling texts and the spec clauses, never from the compiler's source.
Released against pin addcd7f (the s126 wave; one bump, e561c6f → addcd7f). Corpus 422 → 430 files (397 entries + 33 members); coverage ratchets 157 → 159; anchors 396 → 397. All six run-reaching index_origin_* witnesses MATCH at first sight (including the byte-exact stdout pins on the file-wide and scopes witnesses) and the two fail pins land the spec's own codes (E0211 by position, E0813 by name). Every pre-existing corpus file is verdict-identical before and after: the judge counts moved 304→310 run / 279→287 match / 13/41/55/1 unchanged, exactly the eight new witnesses, nothing else.
- The four standalone spellings, in module formation (
[conf.directive.standalone], D59, #49). A file opts out of its directory's module by an explicitmember: false, thecheck:+phase:entry pair (0.1.16's only exclusion), a script announcement (#!line orpkg { … }frontmatter), or a_test.luname; an explicitmember:key always decides; the named entry always compiles; std/dep trees stay whole-package. The asymmetry is kept: a standalone mark opts the FILE out and never shrinks anyone's build, a plainmainbeside a standalonemainstill collides, and the E0302 note now names the escape. - The three E0301 situations, this machine's voice. A name that IS defined next door in a standalone entry answers with the file, the marker, and the fix; an import whose files all opted out lists them; a directory with no
.lufiles gets a formation note instead of a layout assertion. #![and#[index(…)]lex, parse, scope (D61, wolf-lang#169).#![is one token and the shebang narrows around it (#!not followed by[; the script witness stays green); the file-wide form is legal only as the file's first non-trivia construct (E0211 anywhere else, by position); the statement form scopes the annotated node's full lexical extent, nesting legal, innermost wins,#[index(0)]restores the default; bad arguments, duplicates, and unknown INNER attributes are E0813 by name, never ignored, the faulty marker takes no effect.- The shift, exactly per
[gram.expr.index.origin]. Subscript reads, writes, and slice PLAIN starts lower by one CHECKED subtraction; plain ends lower unchanged (the inclusivity coupling;..=is redundant-but-legal);^n, open sides,.len, bare ranges,.get, map keys, pool handles, tuple members and the unsafe tier do not move.xs[int.min]under origin 1 trapsoverflowbefore the bounds question (X3, D56's kind). The human trap line renders the WRITER's numbers inside a 1-origin scope (index 0 is outside…,2..9 (origin 1)), the writer's-mode duty D61 puts on the machine whose report prints numbers. charassignment copies (#50, D58).d = cthen{c}printedxxunder wolf and trapped use-after-move here; acharis now a copy value in the tier-0 move discipline exactly asintis.- Comma-grouped binders (D63 rider).
let/varadmitbinder (',' binder)*, each binder with its own=; the group is the left-to-right sequence of single bindings. One-initializer- many-names and Python's bare tuple are refused by name with both correct spellings offered;constkeeps its single-binder production. - D62 witnesses (rider). Nothing to build,
+/+=on two strs is the language and this machine's behavior IS the ruling. The legal-chain run witness and the three refused mixes (str + int,str + char, `int + str, refused by name) land in-repo (tests/d62/`, upstream-ready) as the differential counterpart waiting for the compiler half (wolf-lang#172); #51 closes from that side. - An upstream finding, filed. c1f54f2's anchors regen DROPPED
gram.lex.identfromanchors.jsonwhile spec/01 §1.3 still defines it, wolf-lang#177, carried as aFILED_REGISTRY_HOLESnotice on every export until the registry re-gains the anchor.
0.1.16 — 2026-08-28
THE INTERPRETER NAMES THE LINE (is27, the lupin half of s125's trap-site pair, sequenced after 0.1.15 as that contract required). At 0.1.15 a trap said at 51..58, byte offsets no editor jump-to and no human counts out (#158, measured in the s125 report). The renderer HAD the span; the gap was purely rendering. This release renders it: implemented from [conf.trap.render], never from the compiler's rendering (the independence doctrine).
Released against pin e561c6f (the s122–s125 wave; one bump, a900b8c → e561c6f). Corpus 403 → 422 files (389 entries + 33 members, including the corpus's FIRST four bare members, D59); coverage ratchets 153 → 157; anchors 393 → 396; twelve of the thirteen new run-reaching entries match at first sight.
- The file doors say
line:col. Every human fault linerun,check,lex,parseandconform-run's human mode print (trap, UB finding, static diagnostic, secondary spans included), spells its locationline:col: 1-based line, 1-based column counted in characters, the spelling the fault snapshots pinned first (one span grammar per tool,[conf.trap.render]).examples/overflow.lunow says `[arith.checked] at 6:5where 0.1.15 saidat 107..113`. - Byte spans stay on the structured door, untouched.
--jsonrecords are byte-identical to 0.1.15's (identity fields aside):[proto.record.diag]spans andx-trap-spanremain byte offsets. wolf-std's runner parses records only, and a filteredstd-testsmoke against this build stayed green. - The two machines name the same place.
tests/faults/trap_site.lupins the s125 witness shape: the trap expression sits at 6:5; the compiled tier reportsat ./trap_site.lu:6:5([conf.trap.report], exit 134) and this machine[mem.ub.defined] at 6:5(exit 3); the KIND is the contract, the statuses are per-machine documented facts ([conf.trap.exit], D60; exit 3 does not move). The differential runner's trap-map site column stays a named follow-on. - A module sibling's fault names its file. A diagnostic raised in a
used module file rendersline:colagainst THAT file's text and says so:entry.lu: E0201: … at 4:13 (in module filemangled.lu). The record's reason now names the module file relative to the entry's directory, an absolute path there made byte-identical re-exports impossible the moment the corpus grew a broken-sibling witness (records travel; the export gate diffs them). - The REPL keeps entry-relative byte offsets. A session has no stable line numbering (each entry restarts at offset 0 and an ownership fault's second span may point into an earlier entry), so
docs/repl.mddocuments the offset spelling as the prompt's own coordinate;[conf.trap.render]is a file-door clause (its exit-status sentence cannot even apply to a session that survives the trap,[repl.trap.alive]). - D59 membership lands in the walk (
[conf.directive.standalone]). An explicitmember:decides; otherwise the entry pair does, a plain.lufile with no directives is a member of its directory's module by default (resolve/bare_sibling/is the witness; half an entry pair stays an error either way). The corpus-walk half of #49; script-header and_test.lumodule formation remain open there. - DIV-2026-019 filed. The broken-sibling witness pins the counterparty's
fail(E0202)(it reads the junk to EOF) where this machine stops at the first bad token (fail(E0201)@parse), same rung, span-or-code class, and the spec assigns neither code to junk recovery: filed as a spec gap, waived by the filing, gating resumes when it resolves. E0202 itself is corpus-pinned now and leftdiag::UNPINNED_CODES.
0.1.15 — 2026-08-28
THE SCALAR RELEASE (is26, one sprint behind s121/D58). At 0.1.14 the lexer refused ' outright (fail(E0101), "' begins no token"), so every char witness the compiler landed was wolfc-lane evidence only: wolf accepted programs its oracle could not even lex, the weakest position a differential oracle can hold. This release teaches lupin the scalar, independently, from [type.char], [gram.lex.char] and [mem.str.chars], never from the compiler's source.
Released against pin a900b8c (the s117–s121 wave; one bump, 90c90df → a900b8c). Corpus 385 → 403 files (374 entries + 29 member files), all 14 new run-reaching entries matching at first sight because the scalar landed here BEFORE the bump; coverage ratchets 144 → 153. The seven char witnesses (char_battery, char_order, char_interp, chars_walk, and the three faults/char_cast_* twins), compare for the first time: 0 → 7, and the walk holds 0 mismatches.
- Char literals lex (
[gram.lex.char], D58.5).'a'at every UTF-8 width, with the string escape set plus\'(`\n \t \r \\ \' \" \0 \xNN \u{1–6 hex}`). The malformed shapes are E0110 named refusals, one report per literal: empty, multi-scalar (a base-plus-combining-accent pair is two scalars (a char is a scalar, not a grapheme), unterminated before end of line, and a\unaming a non-scalar), the surrogate gap and past-0x10FFFF are refused AT THE LITERAL, the lex-time twin of the cast's trap. E0110 moved to this clause from the unpinned unterminated-interp code, which yielded and became E0112.
charis a value, and not an integer (D58.1/.3).Value::Charcarries the scalar; equality and order are total, by scalar value, locale-free ('z' < 'é'). Arithmetic, mixed comparisons, and numeric-literal adoption (let c: char = 65) are refused by name, the permissive-direction divergence that is hardest to notice is the one this machine refuses loudest.matchover char rides scalar identity; CHAR_LIT parses in primary, pattern, attr and const-argument positions.
- The casts, with the trap (
[type.char.cast], D58.4).char as intis total;int as chartrapsoverflow(D56's closed family) on negative, on the surrogate gap0xD800..=0xDFFF, and above0x10FFFF, with the gap edges0xD7FF/0xE000and the last scalar0x10FFFFlegal and witnessed. Everything else is refused by name: onlyintbridges intochar.
chars()yieldsList[char]([mem.str.chars], D58.7). The scalars in string order; the width identity holds, the byte extent of a scalar is a function of its value, and a cursor advanced that way lands exactly on the boundariesgetaccepts (chars_walkwitnesses it over 1/2/3/4-byte scalars).{c}interpolation prints the CHARACTER, never the number (spelled{c as int}); a spec on a char hole takes the str surface, width in bytes.
- The spec tension is filed, not settled.
\u{…}'s one-to-six digit cap is prose-only againstCHAR_ESC's unboundedHEX_DIGIT+, and the string tier states no cap at all; this machine takes the prose reading and records the choice (gram.lex.charin the CHOICES register) rather than copying the compiler.
- Portability honesty (macOS). The net edge probe asserted the write-after-peer-close row on the very next write; the failing write actually waits on the peer's RST, which macOS delivers a few ms late. The probe now retries on a bounded clock, same transcript, no race assertion. First release cut on macOS arm64.
0.1.14 — 2026-08-27
THE CATCH-UP RELEASE (r02, sprints is14 → is25). 0.1.13 shipped on 2026-08-15, and then twelve sprints landed, 58 commits, with the upstream pin re-vendored eight times, while the version, this file, and the tag never moved. Two materially different interpreters could both answer lupin 0.1.13 while declaring different conformance pins: the version had stopped identifying a state. This entry is the correction, and it is honest about the gap: one release covering the whole span, grouped by theme, not twelve retroactive versions pretending each had shipped.
Released against pin 90c90df (the s114–s116 wave). The span advanced the pin 02c1e88 → c9da6d9 → b522b8a → 1b149ba → 87405ac → 21b129e → da8582d → 77466a3 → 90c90df; each re-vendor sits under its sprint's merge in the log, and each registered what its wave demanded (the ct namespace at da8582d, type at 77466a3, os and pkg at 90c90df). Corpus 294 → 385 files, the walk green at the pin (385 file(s), 0 failure(s)), and the run-ledger holds 0 mismatches throughout.
- The version now tells the truth (D57, this release's own change).
--versiondistinguishes a release build from everything else: built exactly at itsv{version}tag, lupin prints the bare version; built anywhere else (trunk, a branch, no git at all), it prints0.1.14+dev.<commit>. A trunk build never claims to be the release again, and wolf-lang's PAIRING gate now compares the declared pin whenever the sibling reports a release build, which is the check that would have caught this whole gap on day one.
- The divergence families closed from the slower side (is14–is17).
?keeps its widening promise (#33); the dispatch floor under animplis the trait's default (#32); the list spine is copy-on-write and every write diverges its copy (#28); a container knows its home, so freed-region access faults (#25) with the home consult gated on region teeth (one atomic load until a free or freeze); a returned region transfers instead of tearing down (#35); amut-receiver method demands the call-site marker (#37); a stale captured-place read refuses instead of answering (#36); prim impls dispatch, so the trait-qualified call reachesimpl Text for int(#34).
- The interpreter crossed behind: net, json, process (is18). The s39/s40 std families run, sockets over
std::net(nonblocking polls under the baton), the process trio overstd::process(wait reaps, kill never tombstones), and the query tier on lupin's own RFC 8259 reading. Plus the twins: a nested namedfnbinds like alet(#38) and module identity is the full path, so same-leaf modules coexist (#39).
- The literal tier and the width rules (is21, is23, D52/D54). Int literals adopt a float expectation and the operator bridge propagates it (D54); a declared row resolves its tags one position wider, the D52 mirror; wrapping shift counts mask to the type's bit width (#42); the expected-type flow reaches container-argument position, so a full-range wrapping literal no longer traps there (#43).
- The lint and arm corrections (is19, is24). E0812 lands (explicit-application arity is one count against another); W0316 is implemented, not absent; a match arm over a tag-shaped scrutinee resolves as the tag, not a binding (#44); the capture law healed with the arm fix and the
free_namesgap was filed rather than papered over (#45).
- The scheduler keeps time (is20). A sleeping task parks and the park bound is the earliest pending wakeup (#40); provenance prune revisits only dirty allocations, not the program's whole history (#41). The witnesses moved 503ms → 66ms and 279s → 3s.
- The line editor (is25). The REPL prompt gets readline at a TTY: history (JSON-line, capped, deduped, in the platform state dir), three-source TAB completion (surface names, never
f#N), word/kill/ case ops,Alt-.yank-last-arg cycling, and a Ctrl-C that cancels the pending line instead of killing the session (#46). Pipes keep the dumb reader verbatim;--no-editandTERM=dumbkeep it too; a raw-mode failure degrades with a note. rustyline 18 (MIT, no default features) carries the glue; the editor's own layer is pure logic with PTY keystroke evidence behind it.:keyslists the bindings. Also from the span: the REPL's trap-survival reminder prints once per session, not once per trap (is22).
0.1.13 — 2026-08-15
THE ARM-SELECTION PASS (sprint is13). One silent wrong answer and one complexity bug; no corpus verdict moves, and every entry file of the pinned corpus records byte-identically before and after; the only file whose record changes is the one added to catch the miss.
Released against pin 02c1e88, which advances upstream across the s88/s89/s90 wave: the corpus grows 283 -> 294 files, the bundle 298 -> 308 records, anchors 316 -> 323, and coverage ratchets 107 -> 114. grammar/range_bare.lu pins E0201, so that code left this implementation's UNPINNED_CODES; the corpus is its authority now. The differential is GREEN at the new pin: 308 entries, 0 divergences.
- wolf-interp#29 FIXED (wolf-std F-0079): a multi-arm
else-match handler took its FIRST ARM for every tag when the row was raised in an imported module. No diagnostic, exit 0, a confident wrong answer; the shape that reads correctly answering the wrong question. The same program gave `10 20 30under wolfgang and10 10 10` here.
Whether a bare lowercase identifier in a pattern is a *row-tag pattern* or a *binding* is a question about the scrutinee's row, and the checker answers it from the row type. This machine asked the matching module's own signatures (sema::Module::row_tags, collected per module from that module's own fn headers). A handler in the entry file over tagmod.miss()'s row therefore found no declared alpha, read every arm's pattern as a fresh binding (and a binding matches anything), so the first arm won for every tag. Reversing the arms reversed the output, which is what tells "first arm always" from "one tag happened to be right".
The row now travels with the value it was raised through (ErrorValue::row, recorded at the raise site from the enclosing function's declared return row, and carried through ?, through payload application, through every copy). A row that crosses a module boundary is still the same row, so arm resolution asks the value which row it came from instead of asking the far side's declarations again. The module's own vocabulary stays in the union behind it, so a tag with no declared row still resolves where its module declares it, and a lowercase name the row does *not* declare still binds: else |err| keeps its binder and an other => rest arm keeps catching.
corpus/rows/cross_module_arms/ is the witness, three arms in both orders over an imported module's row, a ? hop on the far side, and the two binder shapes as the counterweight. It runs 10 20 30 on both compiler rungs and here; before the fix this machine printed 10 10 10 forward and 30 30 30 reversed. rows/propagate was the corpus's only cross-module handler and it took else |_|, which is why nothing noticed. The unit-sized half, which does not wait on a pin bump, is tests/rows_option.rs's a_row_raised_across_a_module_boundary_still_dispatches_by_tag.
- wolf-interp#28 half-FIXED (wolf-std F-0078): the index read no longer copies the container.
xs[i]evaluatedxsin order to pick one element out of it, and evaluating a place-valuedxsdeep-copies the whole thing: O(n) per read, O(n²) per walk. The same lend as #24's receiver fix, on the index-read path; the read is charged at exactly the moment and in exactly the order it always was, the base's own fuel step included, and the container steps out of its slot only for the length of onebuiltin::indexcall, which cannot re-enter the machine. Slices keep the copy (s[a..b]reads its endpoints off the *syntax*, the exclusionlendablealready makes forstr.get(a..b)), as does a base that is not a plain place path; there would be nowhere to put the value back.
Measured, a List[int] of N built by push then read back by index (release build, CPU seconds, min of three, this box):
| N | before | after | |---|---|---| | 2 000 | 0.156 s | 0.042 s | | 4 000 | 0.576 s | 0.080 s | | 8 000 | 2.317 s | 0.157 s | | 32 000 | 21.500 s | 0.333 s |
Four times the work per doubling down to under two (the curve the issue's for v in xs walk already had) and 65× at 32k.
One shape moved, and it moved onto the compiler's answer. The copy path snapshotted the container *before* evaluating the index, so a write hidden in the index expression was invisible to the read that followed it: xs[bump(mut xs) - 1] trapped bounds against the stale length in interpolation position, while the identical let v = xs[bump(mut xs) - 1] answered 9, this machine disagreeing with itself over two spellings of one expression. The lend is taken *after* the index is evaluated (#24's ordering), so both answer 9, which is what wolfgang answers. Pinned in eval::tests.
The other half of #28 stays open: a read-mode List argument still copies, on the way in and again on the way out (call-by-value-result reads every parameter's final value back). That is the same value-model question as #25 and is not a lend away; a callee runs arbitrary user code, which can reach the caller's place by other names.
- wolf-interp#25 not taken. Closing it means giving region-homed containers identity in the value model, a change to how values are represented, not an arm selection or a lend. It stays declared in the approximation contract, §6.13 and §6.14.1.
0.1.12 — 2026-08-13
THE OWN-LEDGER PASS, then the re-pin. Two of this machine's own defects first (wolf-interp#24 fixed, wolf-interp#25 measured and deliberately deferred) and then pin f8dca42 → 4e316ad (latest green trunk, CI run 31742412670, headSha matching). Corpus grows 3: 280 → 283 files. Ratchet holds 107, anchors hold 316 (the range touches no spec/ file), bundle 320 programs / 298 records.
- wolf-interp#24 FIXED:
List.pushwas quadratic, and the cost was never inpush.items.push(Slot::live(arg))is an amortized O(1)Vecappend and always was. The cost was the *call*:eval_methodcopied the receiver out of its slot (read_path), copied it a second time to compare against (original_receiver), compared the two whole values to decide whether the method had written, and copied the result back, four traversals of the whole list to append one element. So everyList-returning std function was quadratic on the reference lane, and wolf-std could not write around it.
The fix is to lend the receiver instead of copying it (Lend in src/eval/mod.rs). A builtin container (List, Map, str), always dispatches to builtin::method (method_of answers only for Value::Struct), and no container arm of builtin::method re-enters the machine, so the value can leave its slot for the duration of the call and return to it. Nothing observable moves: the read is charged at the same moment in the same order, and the lend is taken *after* the arguments are evaluated so xs.push(xs.len) still reads xs through its parent while the receiver's tag is Reserved (corpus/memory/prov_two_phase.lu's two-phase window, untouched).
Two details carry the correctness. Whether the lend ends in a *write* is decided by an O(1) witness (List.push and List.pop are the only two arms of builtin::method that mutate a receiver (builtin::mutates_receiver), and both change the element count exactly when they change the value), so [mem.region.freeze.4] (issue #20, a read-only method must not write back) still holds without the whole-value comparison. And a *mutating* lend asks writeback_would_trap first: write_path faults before it stores, so on that path the receiver must survive the call unchanged, which only a copy can promise; the lend is declined there and the old path runs. In a debug build the whole-value comparison is kept as a debug_assert_eq! against the witness, so the entire test suite and corpus check the classification on every method call rather than trusting it.
Measured, same program at four sizes (var xs = List[int](), N pushes, release build, this box):
| N | before | after | |---|---|---| | 4 000 | 0.40 s | 0.038 s | | 8 000 | 1.44 s | 0.055 s | | 16 000 | 6.18 s | 0.100 s | | 32 000 | 30.33 s | 0.191 s |
Quadratic (4× per doubling) to linear (~1.9× per doubling); 159× at 32k, and the compiler's two run-reaching rungs do 32k in 0.14 s, so the oracle is now within a small factor of the thing it is oracle for instead of 200× off it. The gate that matters is not the clock: the full lupin corpus report (human and --json) is byte-identical before and after the change.
- wolf-interp#25 MEASURED, and deferred as a named design task. A container escaping its region still runs to
exit(0)here and is E1010 upstream. The issue diagnosed this as containers lacking identity in the value model; re-measured, that is half the story, and the other half is more useful: the struct sibling escapes too (memory/region_escape_local.lu, alsoexit(0)), andValue::Struct*already carries* `home: Option<RegionId>`. The real gap is that nothing ever checks a tier-0 value's home against its region's liveness;homehas exactly one reader today (frozen_container, on the write path, asking only aboutfreeze), andRegionState::Freedis consulted for pools and handles and never for a value.docs/approximation-contract.md§6.14.1 now carries the three-part design that closes it, concretely enough to execute: giveList/Mapahome, givehomea second reader on both the read and write paths, and hand-writePartialEqso identity never leaks into a value's equality. NOT done here on purpose, it changes the trap surface, and landing it in the same pass as a rewritten method-call path and a pin bump would make any resulting divergence un-bisectable.
- The re-pin:
str_from_utf8is the only implementation work the wave asked for. Three new corpus files, one per sprint. s80'smemory/foreign_root_aliasing.lu(the witness for a real miscompile, where the release tier answeredx=5 y=5for a program whose answer isx=5 y=7) runs clean here at first sight and always did: there was never a bug on this side, because the aliasing question the optimizer got wrong is one an interpreter never has to ask. It is the cleanest demonstration of what the oracle is for that this project has produced. s81'sstrings/equality_lanes.lulikewise ran clean. str_from_utf8, implemented against a spec that does not mention it. s81 adds the language's first bytes-to-strpath (the byte SOURCE to match s77's byte VIEW) and it validates, because an unchecked one is the forging hole for "everystris valid UTF-8". It has nospec/clause: it is specified by its prelude signature (List[int] -> str ! {utf8}), its doc comments, and one corpus witness. That gap is declared rather than papered over. Beyond the witness's seven refusals, 38 ugly inputs were probed against both machines and all 38 agree byte for byte: lone continuations (0x80, 0xBF), truncations at 2/3/4 bytes, overlongs (C0 AF, C1 BF, E0 80 AF, F0 80 80 AF), surrogates (D800, DFFF) against valid U+D7FF, past-U+10FFFF (F4 90 80 80, F5, F7), never-bytes (FE, FF, C0, F8), non-byte elements (256, −1, −128, 1000000), interior NUL, the empty list, and the boundary scalars U+0080/U+07FF/U+0800/U+FFFF/U+10000/ U+10FFFF. The failure is theutf8row with an empty payload, tag identity confirmed on both machines, and the counterparty's own unreachable-arm warning proves the row is exactly{utf8}.- All three counterparty tiers GREEN, and the lanes grew.
checked,nativeandreleaseeach report exactly one divergence, the same one, byte for byte: DIV-2026-017. Both machines now execute 117 (checked), 119 (native) and 109 (release) of the 261 entries, up from 107/107/98, the counterparty reachesrunon 130/125/115, up from 120/113/104. Upstream s82 consumes these numbers; they moved, and the release lane moved most. - Known, and NOT this release's doing: the mutation arm of
fuzz_smokegot slow at this pin.mutated_corpus_files_never_crashmutates 3000 randomly picked corpus files and runs each through all four rungs; the pinned seed is fixed but the FILE SET is not, so 280 → 283 files reshuffles which mutants get generated, and this pin's draw includes some that run toMachine::FUEL's 50-million-step rail. The rail is the wrong size for a harness that runs 3000 programs: one mutant that reaches it costs 6.1 s in a release build and 37 s in the debug buildcargo testactually uses, so ~40 runaways out of 3000 spend the whole budget. Measured: the test does not finish inside 25 minutes. Verified NOT to be the lend's doing by re-running it withsrc/stashed at this same pin: equally slow. CI sets no step timeout, so it slows the build rather than failing it, but `cargo test` is no longer a gate anyone will wait out. Fix before the next pin: a per-case wall-clock (or step) bound inexercise, well under the language's own rail; the harness is testing that the frontend ANSWERS, not that the fuel rail works, and[gram.lex.rails]has its own litmus for that. - #76 (DIV-2026-017) re-confirmed OPEN and unchanged.
lints/raw_interp_braces.lu:r"{who}"prints{who}here and"{who}upstream, whose raw-literal decode keeps the opening quote of the two-characterr"delimiter. Identical on all three run-reaching tiers, which is still how it is known not to be the mid-end's doing.[gram.lex.str.raw]and the corpus header's ownstdout="{who}"both agree with this machine. Unfixed upstream across three sprints.
0.1.11 — 2026-08-13
THE ORACLE HELD: the semantics-wave re-pin. Pin bumped 613c3dc → f8dca42 (latest green trunk, CI run 31676830124; the two red runs in the range were not taken). This is the largest semantic movement the compiler has had in one wave, s74 (the correctness cluster), s75 (List element access lowers to a load, bounds checks caller-side and eliminated when a relational range channel proves them), s76 (containers allocate in the ambient region at the allocation site, dynamically scoped per D12), s77 (s.bytes() is a view over the receiver's own storage), s78 (an affine relational channel in the range analysis), plus s53 (script mode) and the D43/D44 rulings. The corpus grows 13: 267 → 280 files. Ratchet 103 → 107, anchors 315 → 316, bundle 317 programs / 295 records.
- The wave moved the compiler's lowering out from under three semantic areas, and this machine's independent reading already agreed on all three. Ten of the thirteen new corpus files reach
runhere at FIRST SIGHT with no new semantics written on this side, including all four of the wave's own semantic witnesses (memory/region_container_reclaim.lu,memory/region_container_freeze_ok.lu,strings/byte_view.lu,strings/slice_boundary_sweep.lu). The corpus-wide differential found no new divergence at any tier. - Probe 1, region-scoped container lifetime (s76): agree on every defined shape, one gap declared. A container freed with its region, a callee allocating into its *caller's* region (D12), growth across region chunks,
freezeoutliving the building block, nested regions: identical onlupin,--nativeand--release. s76 moved *toward* this machine's dynamic reading. The escape is the gap:memory/region_escape_container.luis E1010 on every compiler lane andexit(0)here, and (unlike the handle/pool escape, which trapsregion-fault), this machine does not catch it dynamically either. Conservatism in the ledger's sense, since no conforming program can observe it, but a real modelling gap, now declared as approximation contract §6.13 rather than left implied. - Probe 2, byte views and the slice domain (s77): agree, exhaustively. The domain was swept, not sampled: all 100 endpoint pairs of
s.get(a..b)over the mixed-widthé€from −2 to 7, including the entire negative half the corpus file does not reach, are byte-identical acrosslupin/--native/--release, six defined pairs, and a *miss* rather than a wrap-around for every negative endpoint, which is the `lo <=u hi <=u len` unsigned reading agreeing on both sides. The trapping forms[a..b]was swept over 29 ugly pairs (negative, inverted, mid-codepoint, past-end, degenerate-empty, open-ended, inclusive,^nfrom-end): 28 of 29 agree exactly.bytes()is unsigned 0..=255 on both, byte-length on both. - Probe 3, line-atomic print (D43): agree, and this machine was the prior art. The interpreter renders a whole line and hands it to one
out()call, with no yield point inside aprint, so it was line-atomic by construction before D43 was ruled. Measured rather than asserted: eight tasks × 40 long multi-segment interpolated lines, 20 runs of--native= 6400 lines, 0 torn, across 20 DISTINCT interleavings (the interleavings differ every run, which is what proves the threads race and the probe is not measuring a serialization). Same program here: 3200 lines, 0 torn, one interleaving. Nothing to file. - DIV-2026-018 FILED as wolf-lang#88, the compiler admits a bare
..range that the grammar excludes. The 29th ugly pair:s[..]isfail(E0201)@parse here andexit(0)printingok 5there, on all three run-reaching tiers.[gram.expr.primary]'s `range_expr ::= r_end (('..'|'..=') r_end?)? | ('..'|'..=') r_end` has two alternatives and neither admits a bare.., the first needs a leading endpoint, the second a trailing one. Triage case 2, compiler bug, and the over-acceptance is in the parser, not the slice path:s[..=]also parses and answerstrap(bounds), whiles.get(..),let r = ..andxs[..]parse and then declineunsupported@resolvewith no diagnostic. Classverdict, not a soundness candidate. No corpus file witnesses it, so it takes noFILED_DIVERGENCESentry and is recorded in the log until one exists. The counter-reading (that the grammar should gain..) is recorded with it; this machine is not moving its parser ahead of the ruling. - wolf-lang#76 CONFIRMED, unchanged. DIV-2026-017 re-verified at the new pin against a counterparty rebuilt from a deleted
target/: still"{who}there and{who}here, byte-identical shas to the original filing, still the same on--checked,--nativeand--release. Nothing in the wave touched the lexer's literal decode. Still one-sided in this machine's favour (the corpus header's ownstdout="{who}"agrees with us) and the file'sphase: wirpin still masks it. [gram.lex.shebang]READ, the one clause in the wave that needed work here, and it broke fourteen files on arrival. s53's new spec delta makes a#!line at byte offset 0, and no other offset, trivia.grammar/shebang.luarriving meant the wholegrammar/directory module failed to resolve, every one of its 14 sibling files answeredfail(E0101)at the stray#, a 14-file corpus regression from one unread clause. Read in two places, because a shebang is trivia to the language *and* to the corpus tooling: the lexer skips it to end-of-line, leaving the\nfor[gram.lex.newline]'s terminator machinery exactly as a//comment does; anddirective::parse_headerskips it so the//!block can start one line down. Both key on offset zero (`self.pos == 0/index == 0), so a file that opens with a rejected BOM puts its#!` at offset 3 and does not get one.- wolf-lang#71's interpreter half LANDED. 0.1.10 deferred it on purpose; "it lands once wolf-lang#71's fix fixes the span to match". s74 landed the compiler's half, so this round landed ours.
lint::Walk::capture_lendroutes both lend spellings through the same door as assignment: the X1 moded receiver(mut xs).push(1)and the call-site argument mode `f(mut n)`, which "must never drift apart again". Spans are the counterparty's byte for byte,[913,915],[562,563],[545,546]. W1101 is deliberately NOT emitted for the lend spellings (its text is about a write landing on the task's own copy, an assignment's shape), matching both the counterparty and the corpus headers'warns:lines. This also retires the second finding recorded under #71: the mut-lend program printed0here and2there, and neither machine runs it now, both reject it with the same code and span, so the stdout divergence is unreachable. - The
--counterparty-tierlane audited, independently of itself. The lane that found the entire dynamic corpus going uncompared was re-verified by measuring both sides'phase_reachedper entry per tier outside the runner, deriving the audit from the lane it audits would be circular. It is still comparing what it claims. It also showed something the 0.1.10 table did not: the three run-reaching lanes are not nested.checkedreachesrunon more files thannative(127 vs 122) yet compares fewer (114 vs 116), becausecheckeddeclines the conc tier whilenativedeclines most of the unsafe/region/shared tier. The honest coverage figure is the union, 129 files, with 101 compared by all three; one lane alone would miss up to 15. Of this machine's 185 run-reaching entries, 56 are met by no counterparty lane at all, the residue to drive down. - Surfaces:
--versionprints `lupin 0.1.11 (wolf-interp, reference interpreter at pin f8dca42)`. Corpus walk 280 files / 0 mismatch (170 match, 8 dynamic counterparts, 33 conservatism, 47 out-of-scope); bundle 317 programs / 295 records, anchors covered 107 of 316; differential GREEN at all four tiers, one filed divergence at the three run-reaching ones. Full suite 588 tests green.
0.1.10 — 2026-08-12
THE RUN TIER FINALLY COMPARES: the mid-end/whole-program re-pin. Pin bumped 0b4e79c → 613c3dc (latest green trunk, CI run 31651577695: the wave landed s42 (the mid-end optimizer), s43 (whole-program, clusters, body dedup, a frozen summary index) and s63 (diagnostics polish: 144 codes / 30 warnings, error cascades capped with --error-limit=N), plus the dv01 prose pass). The corpus grows 4: 263 → 267 files (conc/select_two_timeouts.lu, the #64 GVN cross-arm-dominance litmus, and the s42 kernel tier kernels/hot_counter.lu, kernels/hot_scale_versioned.lu, kernels/churn_b3.lu). All four reach run on this machine at first sight and match their check:, so the run ledger grows 4. Coverage ratchets 102 → 103: [conc.select.timeout] is covered for the first time, by the new select litmus alone. spec/ is UNCHANGED in this range (anchors stay 315), so no clause needed a new reading. Bundle 304 programs / 282 records.
diff-run --counterparty-tier=default|checked|native|release, and the gap it closes. The counterparty'sconform-runis one process contract over several engines, selected by flag. The runner passed no flag through 0.1.9, so the compiler stopped atunsupported@wirand the counterparty reachedrunon 0 of 245 entries: the entire dynamic half of the corpus was ledgered as conservatism without ever being compared. The gap was named for--nativeat 0.1.9 and stayed open a round; it covered--checkedtoo, which nobody had noticed. Now measured: the counterparty reachesrunon 120 files atchecked, 113 atnative, 104 atrelease, and both machines *execute* 107, 107 and 98 respectively. Our own side is always invoked plainly; this machine has one engine.- THE RELEASE LANE IS COMPARABLE, AND THE MID-END CHANGED NOTHING OBSERVABLE.
conform-run --releaseruns s42's optimizer and s43's whole-program layer; compared against this machine over all 98 files both execute, it agrees everywhere, and the one divergence it reports is the same one--checkedand--nativereport, byte for byte. A transformation that altered behavior would have surfaced as a release-only finding; there is none. Honest scope: linux x86-64, one platform, unseeded, and thekernels/tier is three programs; this shows that 98 observable behaviors survived optimization, not that the optimizer is correct. The release lane declines the conc tier by name (9 files), the compiler's documented posture, conservatism never divergence. - DIV-2026-017 FILED, the first finding a run-reaching lane ever produced.
lints/raw_interp_braces.lu:r"{who}"prints{who}here and"{who}on the compiler, whose raw-literal decode keeps the opening quote of the two-characterr"delimiter.[gram.lex.str.raw]is explicit and the corpus header's ownstdout="{who}"agrees with this machine, so triage case 2, compiler bug, filed upstream. Identical on all three of its run-reaching tiers, which is how it is known not to be the mid-end's doing. The file'sphase: wirpin was written when BOTH executors had the bug; it now masks a one-sided one and should advance torunwith the fix. - wolf-interp#16 FIXED; an enum variant is a value, not a raise. The silent-wrong-answer of the pass. A declared enum's variant and a structural error tag are both tag-shaped, and the machine built them as the same value, so
fn id(v: W) -> W ! {none} { v }had its ordinary return read as an error:?propagated it andelsefired on the VALUE path, so the miss always won and the two paths were indistinguishable to the caller.ErrorValuenow records where its name resolved (enum_variant), sema'sModule::variants(the same table the variant-pattern rule reads), decides it at the two construction sites, the flag rides through payload application, andis_error(the only question?andelseask) reads it. Equality, patterns and rendering ignore it. Confirmed against wolfgang's--nativeand--releaselanes, which both printkind numfor the reproducer. - wolf-interp#17 FIXED; the cast target is resolved, and
stris not a cast source.s as nonsenseran toexit(0)with the string passed through unchanged: the type expression was never resolved, so a typo in a cast target was invisible. Now E0301 atresolvespanning the type name, matching the counterparty span for span ([55,63]fornonsense,[55,60]forbytes, nobytestype exists in this language either). The judgement is narrow on purpose: only a single unqualified lower-case path that is neither a built-in scalar nor a name the module declares, so2 as Meters,x as Tin a generic body and every qualified path still decline to be judged.s as intis E0805 at the whole cast expression ([50,58], the counterparty's span); the shape sema-lite cannot classify (astrfrom a call return, which is the typecheck rung this machine does not perform), declines atruninstead of passing a string off as a number. - wolf-lang#71's premise corrected: this machine never said E0202. The issue records lupin rejecting the
(mut xs).push(1)two-task program with E0202.E0202is this machine'sE_UNEXPECTED_EOF(src/diag.rs:186) (a *parse* code, never a capture-law verdict) and at this pin lupin does not reject the program at all: it runs and prints0. On the assignment spelling (n = 1) both machines already answer E1101, at their own rungs, which[proto.cmp.rung]makes agreement. The proposal: E1101, on both sides, this machine wants no distinct code, and the interpreter half is to treat a(mut x)receiver-lend of a captured binding as a write and emit E1101 there too. Deliberately not done this round: the code is corpus-pinned and landing a span before the compiler's would trade a missing diagnostic for a span divergence. A second finding recorded as this machine's own: closures capture by value, so the mut-lend program prints0here against wolfgang's2, a real stdout divergence on a program no corpus file witnesses. - The record's
commitstamp stops going stale.build.rswatched.git/HEAD, which on a branch holdsref: refs/heads/<branch>and does not change when a commit lands, so the stamp froze at whatever commit last forced a rebuild and records claimed a revision that had not produced them, the dishonesty[proto.record.fields]asks that file to prevent. It now also watches the ref HEAD points at, andpacked-refs, each only when present. - The three s71 clauses, verified clause by clause at the new pin.
[mem.str.empty]:count("")is 0,split("")yields one piece and that piece is the whole string,replace("", t)is the identity,repeat(0)is"", conforming, no lane refusing, no lane trapping.[mem.str.repeat]:repeat(-1)istrap(assert), notbounds. Theelse |Tag(p)|row-coverage rule:rows/negative/handler_uncovered.luisfail(E0809)@resolvespan[518,523](the counterparty's span) and the payload-binding run halfrows/else_tag_payload.luruns with its exact expected bytes. All three conform; nothing needed changing. - Surfaces:
--versionprints `lupin 0.1.10 (wolf-interp, reference interpreter at pin 613c3dc)`. Corpus walk 267 files / 0 mismatch (158 match, 8 dynamic counterparts, 32 conservatism, 47 out-of-scope); bundle 304 programs / 282 records, anchors covered 103 of 315; differential GREEN atdefaultwith an empty filing list, and filed-green at the three run-reaching tiers. Noted, not acted on: upstream's own--versionstill nameslupin 0.1.8 … pin 7886559as its paired reference interpreter, two releases behind.
0.1.9 — 2026-08-12
THE CONC TIER RUNS ON BOTH MACHINES: the c09-wave re-pin. Pin bumped 26fa98e → 0b4e79c (latest green trunk: the wave landed s41 (release tier), s51 (package manager), s73 (native concurrency) and four fmt idempotence fixes; trunk head 17ea078 is CI-red on both test lanes and was not taken). The corpus grows 1: 262 → 263 files (test/conc_schedules_test.lu, the s73 --schedules=N dogfood witness); nine files' headers advance to phase run (8× conc/ + procs.lu; wolfgang executes concurrency natively now, and this machine's run-ledger claims for all nine predate the wave, so both machines now execute the tier), memory/prov_holy_grail.lu moves typecheck → mem, and two conc files gain -> !int rows (E0604/D30). Anchors stay 315, ratchet stays 102; bundle 300 programs / 278 records.
- The conc tier, machine to machine: 10 of 10 agree. The wave's first comparison over a corpus tier where BOTH implementations execute concurrency.
wolf conform-run --native --seed=Nvs `lupin conform-run --seed=N` over all ten run-phase conc-tier files at seeds {0, 42}: every pair agrees on verdict, exit and output bytes, both records"seeded": true, a[proto.seed.equal]comparison, honored end to end. Honest scope: two seeds, linux x86-64, wolfgang's debug tier; the harness's own diff-run lane still invokes plainconform-run(wolfgang's native rung is opt-in) and ledgers the ten as counterparty-unsupported conservatism. The native comparison ran direct, documented in the thirteenth differential entry. - Thirteenth corpus differential: 241 entries, 0 divergences. Counterparty built CLEAN at the pin with
libwolf_rt.aprovisioned; 395 conservatism-ledger entries (66 rejects-beyond, 130 run-unmatched, 152 counterparty-unsupported, 47 interp-unsupported).FILED_DIVERGENCESis empty again. - DIV-2026-016 RESOLVED; wolf-lang#61 closed. The CLEAN build answers
fail(E0809)@typecheck, same span[518,523]. The E0806 answer does not reproduce at the release sha or this pin, agreeing with the issue's own reproduction attempt.[proto.cmp.rung]agreement with this machine's resolve rung; the 0.1.8 observation is attributed to a stale/intermediate-pin counterparty build. An evidence-hygiene lesson (fresh counterparty target per re-pin), not a semantics one. - E1014's trap-map row landed upstream: 0.1.8's filed nit, closed. The pinned
[conf.trap.map]exclusivity row now names "E1014's read-mode write barrier, D39" outright, so this machine's D39 trap mapping (ledger::dynamic_meaningE1014 →exclusivity) is document-stated rather than family-inferred. No behavior change; the citation argument in the comment retired. - Surfaces:
--versionprintslupin 0.1.9 (wolf-interp, reference interpreter at pin 0b4e79c). Corpus walk 263 files / 0 mismatch (154 match, 8 dynamic counterparts, 32 conservatism, 47 out-of-scope); bundle 300 programs / 278 records, anchors covered 102 of 315; differential lane GREEN with an empty filing list. Open lupin issues #16/#17 re-verified at this pin: #16's shape now fails loud (unsupportedat the else handler) but the handler still fires on the value path, still open; #17's unknown-as-target pass-through still reproduces, still open.
0.1.8 — 2026-08-11
The v0.1.0 RELEASE PAIRING build: this is the version wolf 0.1.0's --version line names as its reference interpreter. Pin bumped e94b879 → 3d5cee6 (wave seven: r01 prep + s71 release polish; the corpus grows 5: the [mem.str.empty]/[mem.str.repeat] witnesses, the else |Tag(p)| pair, the ctfe fold witness; the spec grows [mem.str.empty], [mem.str.repeat] and §10 [gram.version] (grammar/1)). Then the ONE lawful mid-pass re-pin 3d5cee6 → 26fa98e when s72's mode teeth merged upstream (the corpus grows 3 more: the D39/D40/overlap fail-files memory/read_param_write.lu E1014, memory/mut_read_overlap.lu E1002, memory/list_mutate_while_iter.lu E1013; the spec grows [mem.iter.excl] and the trap map's E1013 row). Net: 254 → 262 files, 306 → 315 anchors, ratchet 97 → 102. The D39/D40 mirrors below were built on the rulings' authority (wolf planning 02-decisions.md, 2026-08-12) while s72 ran concurrently; the re-pin brought their spec text and fail-files into the pin, and all three fail-files pair with this machine's traps as dynamic counterparts (ledger::dynamic_meaning gains E1013/E1014 → exclusivity; the pinned map prose names E1013 but not yet E1014, a noted upstream-worthy nit).
- D39's dynamic mirror: a write through a read-mode binding traps
exclusivity. Every call frame now carries its read-mode parameter list andwrite_pathis the barrier: whole-parameter stores, projection writes (p.x = 9), compound assigns, and a mutating method's receiver write-back all trap with the parameter's declaration as the second span and amut-plus-call-site-spelling teach. The kind isexclusivity,[conf.trap.map]'s family for mode violations ([mem.tier0.excl.1]already gives every read/write conflict that kind; D39 names no new one). A body local shadowing the parameter's name stays an ordinary local. The caller-side overlap half (f(mut a, a.x)) was verified and kept. approximation-contract §6.12. - D40's dynamic mirror: mutating a container while a
forloop iterates it trapsexclusivityat the mutation (S-11 RESOLVED; closes wolf-interp#9 as fixed).for x in xsholds a read claim on the container's place for the loop's whole extent; push/pop/clear, an element write, a whole-container assignment, or amutpass inside the body conflicts with it and traps, the message naming the loop and teaching D40's fix-its (collect-then-apply, or the index loop; wolfgang's E1013 row in[conf.trap.map]). The wolf-interp#9 program that ranexit(0)over the loop-entry snapshot traps now; reads beside the claim stay legal; the claim dies with the loop on every exit path. approximation-contract §6.8 rewritten to the ruled semantics; the S-11 filing closes in docs/divergence-log.md. - The s71 clause backlog.
[mem.str.empty]: the searching family is defined on an empty needle, socount("")is 0,split("")yields the whole string as one piece, andreplace("", t)is the identity; the threeunsupporteddeclines die.[mem.str.repeat]: a negative repeat count is a caller contract violation, taking the deterministicasserttrap and retiring the sc03-eraboundsspelling (the 0.1.7 corpus mismatch onfaults/repeat_negative.lucloses). E0809 at the resolve rung: anelsehandler pattern must cover the operand's whole error row, judged where sema-lite can see the row (a direct unshadowed call to a same-module fn with a declared closed row, the E1007 discipline);rows/negative/handler_uncovered.lupins it, and the payload-binding run half (rows/else_tag_payload.lu) already agreed. - comptime posture unchanged, verified at the new pin: every corpus comptime file that calls a
comptime fndeclines by name (`expandis acomptime fn; … the compiler's engine (s16)`), including the newcomptime/fold_reaches_lane.lu. The fold table is the compiler's; nothing here half-implements it. - One divergence found, filed: DIV-2026-016 / wolf-lang#61. wolfgang's own
conform-runanswersfail(E0806)@typecheck(the generic refutability diagnostic) on `rows/negative/ handler_uncovered.lu, the file its own corpus pinsfail(E0809)` for; this machine matches the pin at resolve, same span. Codes differ, so[proto.cmp.rung]cannot bridge it; the counterparty is the defendant (both records attached to the issue). The lane is GREEN with the filing; the entry closes when wolfgang's conform-run answers its own pin. - Surfaces:
--versionprintslupin 0.1.8 (wolf-interp, reference interpreter at pin 26fa98e). Corpus walk 262 files / 0 mismatch (153 match, 8 dynamic counterparts, 32 conservatism, 47 out-of-scope); bundle 299 programs / 277 records, anchors covered 102 of 315; differential lane GREEN, and the one divergence is DIV-2026-016, filed.
0.1.7 — 2026-08-11
The release-runway pass for r01 (wolf v0.1.0 names this version as the reference interpreter). Pin bumped 13b811f → e94b879 (wave six: s40 os/time/json, s70 match tier + X3 value paths, s69 idiom lints; the corpus grows lints/ ×15, os/ ×4, json/ ×2, faults/ ×5, time/ ×1 and the match-tier witnesses: 221 → 254 files; the registry grows [proto.cmp.rung], 305 → 306 anchors, ratchet 96 → 97). Two issues closed, every open divergence family closed, the differential compares CLEAN.
- #21: container-element literals adopt
int, andList[i32]checks its writes. Both directions of the s70 reassignment. A literal pushed into aListadopts the locked 64-bitint(theRangeprecedent) instead of staying literal and defaultingi32at the pop-binding, sopush(2147483647); pop; +1prints2147483648now, as wolfgang does. The constructor's bracket type argument (List[i32](), read off the callee's syntax; alsoList[T]annotations throughcoerce) travels on the value, so pushes range-check at the element's width, element loads feed checked arithmetic ati32, and the compoundl[0] *= 2traps BEFORE the write lands. The fivefaults/overflow_*litmuses andmemory/list_elem_assign.luall pin, both lanes. - #22: the explorer runs the same admission ladder as
run. The--explorepath bypassed the E11xx statics and certified programs "observably deterministic" that the same binary refuses to run (rp01's finding; the ch13 lost-update shape).frontend::admitis now the ONE admission question (module laws, then the pin statics, then the raise check), asked byobserve_with,explore_fileandexplore_sourcealike, soconform-run --explorerefusesstore_buffer/chan_unsendable/when_nestedwith the run door's own diagnostics (exit 2, no certificate).--explorealso honors--std-rootnow. [proto.cmp.rung]adopted; DIV-2026-011/-012/-014/-015 ALL CLOSE. The ruling the last four rounds routed upstream landed in spec/06: fail(code+span) parity at any shared-ladder rung is agreement, exactly one verdict wide.compare_deepandcompare::compareimplement the clause, the eleven rung-placement divergences compare clean, andFILED_DIVERGENCESis EMPTY for the first time since the fourth round. En route the fail comparison learned to read the first error-severity diagnostic. A counterparty may interleave lints ahead of its rejection indiagnostics([proto.record.warn]), and a lint's span is[proto.cmp.warn]'s surface, never the rejection's (theresolve/cycleshape at this pin).- The s69 idiom lints. Ten of the eleven run here with counterparty-identical spans: W0310–W0315 (naming, docs, module shape), W0603/W0604 (rows), W1002/W1003 (mode hygiene). E0802's literal-precise dead-arm analysis lands with them (duplicated str/bool/int literals,
_after bool's two literals, thepattern_shapedegradation). W0316 (ancestor import) is honest-absent: its only witness shape needs the dotted nested-module loading this machine's loader does not perform. - The s40 tier, postured. env and time are implemented on the checked-lane posture: overlay env (never the host's), empty argv (the stdin posture mirrored), cwd as process state, X12 monotonic time, and
os_exit's defer-skipping termination (Signal::Exit). Soos/args_cwd,os/env_roundtrip,os/exit_codeandtime/monotonicrun to their pinned outputs. The process trio is exec surface declined by design; the json kernels are the counterparty's reference parser, declined instead of guessed. All fifteen names resolve, so every refusal is "unsupported feature", never "unknown name". - Counts. 254 corpus files, 232 entries: 149 match, 5 dynamic counterparts, 32 conservatism, 46 out of scope, 0 mismatches. diff-run at the pin (CLEAN wolfc build at
e94b879): 232 entries, 0 divergences. Bundle: 291 programs, 269 records, 97/306 anchors. - Surfaces.
--versionnames the pairing posture per r01 row 7:lupin 0.1.7 (wolf-interp, reference interpreter at pin e94b879).
0.1.6 — 2026-08-11
The wave-four re-pin and the lint wave (issues #19, #20). Pin bumped f0da6e6 → 13b811f (#41 capture law, #42 checked-lane determinism, s34 procs, s35 io reactor, s39/s40/#40 native str/List/fs, s68 lints: the corpus grows lints/ ×12, conc/ ×3, projects/ ×3, net/ ×2, comptime/ ×1, test/ ×1: 199 → 221 files; the registry grows [conc.chan.default] and [mem.region.freeze.4], 303 → 305 anchors, and the test namespace joins the reserved forward set). Two issues closed, one divergence family filed, one closed.
- #20: reads through frozen containers are legal. The spec ruled (
[mem.region.freeze.4], appended for exactly this defect): every read through frozen data is an ordinary read, and a value-semantics machine must not count writing back an *unmodified* method receiver as a write.eval_method's write-back is now conditional on the receiver actually changing, so the book's ch10 shape (frozen[0].body.words()) reads legally where 0.1.5 trappedregion-fault; a genuinely mutating method through a frozen home still traps (region_freeze_write.luunchanged). - #19: the s68 lint wave. This machine now runs warning analyses: the eleven shared-analysis lints (W0304–W0309, W0401, W0602, W1101, W1102, W1302) plus the
#[allow]self-lints W0302/W0303, in the newlintmodule. Every fixture span is byte-identical to the counterparty at the pin,#[allow]suppresses identically (item- and statement-granular), the record'swarningsarray is populated per[proto.record.warn], and the same observations ridediagnosticsat warning severity. The compiler-only four (W0402, W0601, W0801, W1001) stay honest-absent, written down inlint::HONEST_ABSENT; the corpuswarns:ledgers are enforced for the implemented set. The same walk carries the pin's static realignments: E1101/E1102/E1103 (the #41 capture law: a task's write to a captured name withwhenbodies exempt, a spelledList/Mapchannel payload, a lexically nestedwhen) and E0004 (1.e5stays an *error*, the s68 correction) all reject at this machine's resolve rung with the counterparty's codes and spans, sostore_buffer/chan_unsendable/when_nestedleave the run ledger for the fail column, and the last E000x unsupported(interp) conservatism row closes. §7.8's[ub.assume.noalias]citation is confirmed[mem.unsafe.raw.2], with W1302 as its compile-time face. - Realignments at the pin:
[conc.chan.default]adopts this machine's rendezvous default as normative (channel[T]()was already capacity 0 here; the clause cites the behavior, and the ctor now cites the clause); E0412/E0413 spans realign to the counterparty's:specshape; thetestanchor namespace lands (test/assert_test.luwalks); the s34 proc pair and two of the three P-project witnesses run (projects/count.luneeds the fs tier this machine declines by design). - Tenth differential: 203 entries, 11 divergences, all filed, every one the same finding: same code, same span, this machine at resolve vs wolfc at typecheck/mem (DIV-2026-011/-012/-014, plus new DIV-2026-015 for the four realigned statics; one
[proto.cmp]ruling closes all four families). DIV-2026-013 closes (wolfc's s38 conform-run wiring landed;unsupportedthere now, never a divergence). 325 conservatism entries. Bundle: 258 programs, 240 records (the issue #20 freeze-read twin joins the suite tier); coverage ratchet raised 90 → 96 over 305 anchors.
0.1.5 — 2026-08-11
The unsafe-tier batch (issue #18, the book's ch09 differential) and the five-lane re-pin. Pin bumped ad6cef7 → f0da6e6 (s32 tasks, s33 channels, s37 str core, s38 fmt/io/fs, s67 warnings: the corpus grows strings/ ×9, lints/ ×4, fs/ ×2, io/ ×1: 183 → 199 files; the registry grows the diag.* block, [mem.str.get] and [proto.record.warn]/[proto.cmp.warn], 290 → 303 anchors). One issue closed (#18, six items), three divergences filed.
- #18 (1): the unsafe ring is enforced. Raw-tier operations outside
unsafeblocks reject at this machine's resolve rung with the counterparty's code and span: C calls (E1301 at the call,[384,395]onunsafe_raw_outside.lu, byte-identical), raw reads and writes through pointer-holding locals (the place,[452,456]shape), int→pointer casts,borrow … from,assume noalias, and the provenance operations. Sema-lite tracks what it can *see* (literal-bound locals, allocator calls, the book's launderedunsafe { … }initializer) and never guesses. Rung placement vs wolfc's mem emission is DIV-2026-012 (the DIV-2026-011 question). - #18 (2): nothing casts to
bool. The cast matrix's bool column closed:n as boolis E0805 at the whole cast expression, insideunsafetoo (observed parity at the pin). §7/T1's one modelled production door closes with it: the T1 trigger/twin retired,UbRow::T1isCoverage::Unreachablewith its reason, and the detection logic stays for frontend-bypassing callers.bool as _and_ as strreject statically where the class is visible (typecheck/cast_bad.lunow matches its pin). P1's protector-form suite pair retired with its*u8signatures; the protector acceptance evidence moved inline (machine-direct), and P2's trigger/twin rebuilt onfreeze r, in-language, same row. - #18 (3):
*Tnever crosses a signature. E1302 at the parameter name ([329,330]onunsafe_sig.lu, byte-identical), return types at the type span. Also DIV-2026-012. - #18 (4): the C intrinsics check their arguments. Exact arity for the modelled five; size/count arguments must be non-negative integers;
c.memset's byte argument no longer defaults silently, and every refusal names the construct. - #18 (5): the §7.4 format specs, to parity. New
fmtspecmodule:[[fill]align][+][0][width][.precision][type], with zero-pad AFTER the sign ({n:08}is the flag plus width; the absorb-into- width reading was the filed bug),+with zero taking it, sign-magnitude bases,e/Esigned two-digit exponents, str precision on code-point boundaries, shortest-round-trip f64 default (thestd.fmt.decimal.to_strlayout; floats render3, not3.0). Malformed specs are E0412 and type-mismatched specs E0413, statically at the literal where sema-lite sees the hole's class; E0411 statically refusess[i]char indexing. The three corpus fail-files match their pins; wolfc's conform-run at this pin cannot reach its own emissions there, filed as DIV-2026-014, counterparty suspected. - #18 (6): the fs/io posture. No filesystem by design: the s38
fs_*family andread_lineresolve and decline with the construct named.eprint/eprint_raware real: one fmt machinery, two fds, stderr live-gated like stdout's pass-through and never hashed;io/eprint.luruns to its pinned stdout. wolfc's conform-run at the pin E0301-rejects its own s38 files, filed as DIV-2026-013. - Realignment: the s37 str surface lands in full (
get, the[mem.str.get]boundary primitive, oob = reversed = split-code-point =none, hits bit-identical to the checked slice; thenfind/rfind,bytes,split/count/replace,strip_prefix/strip_suffix,trim_start/trim_end,ends_with, negativerepeattrappingbounds),^nend-relative endpoints resolve before the domain question, and[proto.record.warn]is wire-complete: the additivewarningsarray (schema, comparison per[proto.cmp.warn], honest-absent, since this machine runs no warning analyses yet and says so by omission), thewarns:corpus directive, and thediaganchor namespace. The lints tier runs warning-clean; spec/01 §9's bare[diag]heading token is exempted from the registry cross-check as a namespace, not a clause (routed upstream). - Ninth differential: 181 entries, 18 members, 10 divergences, all filed (011 open; 012 rung placement ×4; 013/014 the counterparty's conform-run surface lagging its own corpus at the pin), 289 conservatism entries. Bundle: 235 programs, 217 records; coverage ratchet raised 86 → 90.
0.1.4 — 2026-08-10
The held maintenance wave, released once s30 shipped upstream. Pin bumped d147a54 → ad6cef7 (s29+s30: the corpus grows memory/unsafe_c_alloc_native.lu, rows/eu_main_err_exit.lu, typecheck/float_nan_cmp.lu and resolve/same_name/: 177 → 183 files; anchors hold at 290; the two E0410 fail-files re-pin phase: resolve → parse). Four issues closed, one divergence resolved, one filed.
- #15 (silent-wrong, ba:blocker): the X1 call-site mode law has its missing half.
f(x)where the signature demandsf(mut x)ran to a wrong answer silently (the writeback never happened). The book caught it teaching chapter 7. E1007's static rule is now sema-lite's at the resolve rung, all four disagreement shapes (missingmut/take, extra mode, wrong word), matching wolfc's code, span, and message shapes, exactly as E0410 in 0.1.2;[conf.trap.map]gives E1007 no dynamic meaning, and a wrong answer is not a semantics candidate, so rejection is the honest stop. The dynamic residue, calls through function values, is refused at the call, never run wrong.memory/mode_missing_mut.luleaves the run ledger; the book's ch07 repro is a regression test. Rung placement vs wolfc'smememission is DIV-2026-011 (same code, same span; routed upstream). - #13:
c.calloc(n, size)allocatesn * sizebytes. The modelled C heap gave itn; s29's native differential (real glibc) caught the disagreement, the first soundness candidate it produced, and a lupin bug. Overflow in the size computation isunsupported(real calloc says NULL; no null surface is pinned).malloc/memset/memcpyaudited correct.unsafe_c_alloc_native.lurunsexit(0). - #14: integer literals consult their context.
-9223372036854775808is writable in every annotated spelling: literals stay unconstrained through negation and literal-only arithmetic (i128-checked), a declared return type types the value a call returns, and[arith.literal.default]'s i32 rule (with a range check) applies where the literal meets its binding.var k = 0remains i32, the rule wolfc implements, now documented (approximation-contract §6.11). - #11: closed after the sc04 reopen. The 0.1.3 cast matrix holds at this pin: the reopening program prints
3.0/convertsand exits 0;(3 as f64) == 3is false. What the reopen's evidence showed the matrix still misses is filed separately as #17: cast target types are not *resolved* (s as nonsenseno-ops). - DIV-2026-010 closed. s29 moved wolfc's E0410 to the resolve rung (with the
[conc.when.body]exemption this machine flagged as wolf-lang#21) and re-pinned the corpus directives; the eighth differential compares both files clean. Eighth round: 165 entries, 18 members, 1 divergence (DIV-2026-011, filed), 268 conservatism entries. - Realignment:
float_nan_cmp.lu(IEEE!=is unordered; this machine's f64 model already agreed, and wolf-lang#22 was the compiler's half),eu_main_err_exit.lu(error: Boom+ exit 1, agreeing with the native shim bit-for-bit),resolve/same_name/(twolens stay distinct). Bundle: 222 programs, 204 records; coverage ratchet raised 84 → 86.
0.1.3 — 2026-08-10
The rows half, and the s27/s28 catch-up. Pin bumped a0c4564 → d147a54 (the corpus grows rows/qmark_defer.lu and faults/assert_msg_holds.lu: 175 → 177 files; the spec grows [mem.iter.*], [mem.str.*], [conf.trap.assert] and the postfix-row type grammar: 281 → 290 anchors). Three issues closed, one divergence resolved, one filed.
- #12: postfix rows, all three halves.
type ::= type '!' error_rowparses in every type position (param,let/varannotation, nested); a bare lowercase name at a raise site resolves against the enclosing function's declared return row (return noneunder-> int ! {none}raises the tag); and resolution is eager: sema'sraise_checkrefuses an unresolvable tag at the resolve rung whatever path the input takes, so the sc02 false-certification trap (unsupportedonly when the raise was *hit*) is structurally closed. Lowercase identifiers over tag-shaped scrutinees dispatch as row-tag patterns when they name a module-declared row tag;else |err|keeps its binder. The acceptance: wolf-stdstd.option's six helpers (or,expect,flatten,to_list,exists,is_none, the F-0002 family, unwritable since sc01) execute under lupin, lowercasenoneincluded (tests/rows_option.rs). - #11 (silent-wrong): numeric casts convert.
n as f64produced an int that compared equal to ints and unequal to the float it claimed to be.asbetween numeric types now converts in every direction: int→float exact, float→int truncating toward zero with an X3 range check (NaN/∞/out-of-range trapoverflow), int→int narrowing range-checks,wrapping[T]/saturating[T]targets reduce by their mode,as f32rounds through f32 precision (the one-f64 float model, approximation-contract §6.9). The non-bridges refuse like wolfc's E0805 (no truthiness, noint as str).tests/cast_matrix.rspins the matrix, both directions of every pair. - #10: slice-of-binding receivers.
d[0..1].upper()refused at resolve (d["0..1"]does not denote a place) because the range key was stringified into a map-key projection. A slice expression is a value, not a place:place_ofrefuses it, the method call falls into the by-value receiver path, andbinding[range].method()runs exactly likeliteral[range].method()always did. - The s27 spec realignments.
[mem.iter.for]:forover animpl Iter for Tvalue desugars to the clause's drive loop (next(mut self) -> T ! {done}through call-by-value-result; range-for unchanged). Impl-block method dispatch lands with it, s17 resolution order included (inherent wins;Speak.speak(d)reaches the shadowed trait method; trait default bodies stayunsupported).[conf.trap.assert]:assertis an intrinsic, never shadowed by a module fn, two-arg form's message evaluated only on the failing path, rendered to stdout before the trap (the counterparty's #19 shape, from this side).[mem.str.order]: the executed byte- lexicographic ordering is now clause-backed and witness-tested. Twelve more corpus entries reach the run rung than at 0.1.2 (114 of 161; matches 76 → 84, out-of-scope 46 → 36, 0 mismatch). - DIV-2026-010 re-verified: still open at this pin. A CLEAN wolfc build at
d147a54reportsfail(E0410)@typecheckwhere the corpus pinsphase: resolve, and the sixth round's two divergences stand unchanged, filed and non-gating. The fix is in flight upstream (s29's resolve-rungletcheck, landed past this pin with CI still running at the close of this pass); wolf-lang#21 carries this machine's heads-up that the new walker must exemptwhen-body assignments per[conc.when.body](its draft E0410-rejectswhen (a, b) { a += 10 }onlet-bound Mutex operands thatconc/when_multi.luandprocs.lupinrun(exit=0)). The seventh corpus differential is GREEN-with-filing: 161 entries, 2 divergences, both DIV-2026-010. - Bundle: 216 programs, 200 records, 290 anchors, ratchet floor 84 (coverage +1:
[mem.model.order]throughqmark_defer's ownconforms:line; the nine new s27 anchors enter the debt list: their behaviors are exercised intests/, and lifting them intoconforms:-tagged suite programs is the next bundle's work).
0.1.2 — 2026-08-10
The lupin maintenance pass: five filed issues, four fixed, one routed upstream. Pin bumped cbde620 → a0c4564 (the corpus grows the E0410 fail-files and the unsafe/checked memory tier: 164 → 175 files).
- #5 (silent-wrong): bare-ident match patterns dispatch. An identifier that names an in-scope enum variant is a *variant pattern* (matching the tag spelled bare or enum-qualified, payload half included); a capitalized identifier over a tag-shaped scrutinee is a structural row-tag pattern (D30); everything else binds, including a capitalized name over a non-error scrutinee, which is the counterparty's reading too. First-arm-always is dead:
match Ordering.Greater { Less => 1, Equal => 2, Greater => 3 }yields 3,corpus/typecheck/match_missing.lumoves to its honestexit(1)in the run ledger, and a match no arm of which applies isunsupported, never a wrong answer. The out-of-grammar mirror image, bare *dotted* path patterns (Ordering.Less =>), now rejects at parse with the counterparty's exact E0201 shape (zero-width span at the token after the path). Approximation-contract §6.7 records the dynamic approximation. Same-scopeletshadowing also reads the *latest* binding now (therpositionrepair), which islet_shadow_var_ok.lu's pinnedexit(0). - #7 (false UB): both
ub(mem.ub)shapes of wolf-std F-0013 were one defect inProvenance::drop_frame: it still parsed the pre-task<frame>:<path>place-key shape (keys aret<task>:<frame>:<path>since is06) and so dropped *nothing*: a callee's parameter binding survived its call, and the next call reusing that frame index and parameter name resolved its accesses through the stale tag: a Disabled read (shape a, the interpolatedmutargument) or a protected-sibling foreign write (shape b, the allocating read-mode call).drop_framenow takes(task, frame)and forgets that task's frame and deeper, exactly. Both filed shapes are staged as regression tests (tests/std_root.rs), the transition table is untouched, and the ub matrix + ok-twins stay green, with no true detection weakened. - #8:
letreassignment rejects at the resolve rung. Sema-lite tracks binding mutability (params and pattern bindings are the mode system's business;whenbodies assign through the acquired cell; shadowing rebinds): plain and compound assignment to alet-bound name is E0410 with avarfix-it, span on the assigned place, byte-identical code+span to wolfc on the pin's fail-files. The interpreter half of wolf-lang#2, closing the last half of the divergence. - #6: lupin has a std root.
--std-root DIRonrun/check/conform-run,LUPIN_STDas the flagless spelling:use std.X[.Y]resolves<DIR>/X[/Y]/through the normal loader (nested paths included;std/x/deque_intships at that depth), the path's last segment is the bound name, and without a root the flat<package root>/<last segment>fallback keeps mirrors and sibling modules working. Mirrors wolfc's s26--std-root/WOLF_STD; the wolf-std rig's flat-mirror interim can retire. - #9, mutation during
foriteration: routed upstream, not legislated. The pinned spec says nothing aboutfor's operand (no move, no extent-hold, no copy), and the implementations picked different readings (wolfc: E1001 static move; lupin: loop-entry snapshot, runs). Filed as S-11 in the divergence log with both behaviors and the three candidate rulings; approximation-contract §6.8 documents the machine's snapshot semantics. Compiler half wolf-lang#15; wolf-std keeps the divergence visible in CI. - Conformance bundle: 214 programs / 198 records at pin
a0c4564; anchor ratchet 79 → 83; manual, export ledger and corpus-harness ledger updated in the same breath.
0.1.1 — 2026-08-10
The bs00x maintenance pass: four filed issues, three fixed, one routed upstream.
- #1 (top severity): the DPOR closed-frontier miss is fixed. Two scheduler defects starved the explorer's backtrack sets: a send that committed a
selectarm consumed the selecter's registration on every channel of the select but recorded only the sent-on channel (the conflict alphabet missed the coupling;op_select_consumeis the fix), and channel sends published the sender's vector clock without ticking it past the snapshot (spawn and mutex release already ticked), so the sender's later ops carried false happens-before edges. The filed reproducer (wolf-book ex17-1, the lost-update server) now shows both outcomes (balance=50andbalance=100) inside a *closed* frontier, identical to naive DFS, and is pinned as a regression litmus (tests/explore_machine.rs::the_select_coupled_lost_update_is_inside_the_closed_frontier). The pinnedconc/exploration ledger re-ran with no count movement and every verdict-stability oracle still green. - #2: write-after-freeze now traps on value paths. Struct values carry the region charged at their allocation site (
Value::Struct::home), andwrite_pathrefuses a write through a container homed in aFrozenregion: `region-fault [mem.region.freeze.1]`, before anything is mutated. That is E1012's shape executed, agreeing with wolfc's static rejection. Reads and rebinding stay legal (tests/faults/region_freeze_value_write.lu+ itsregion_freeze_rebind_ok.lutwin; approximation-contract §6.1 records the remaining list/map gap).memory/region_freeze_write.lumoves fromexit(7)totrap(region-fault)in the run ledger. - #3:
when-arity code aligned to wolfc. The malformed-whensentence now carries E0201 (the established generic expected-token assignment) instead of the invented E0203, which collides with wolfc's toplevel-decl family. Message and[gram.expr.conc]anchor unchanged;diag::UNPINNED_CODES(the published choices table) updated. - #4, cross-task capture-by-copy: routed upstream, not patched. spec/03 makes the mut-capture shape a compile error (E1101) and states no runtime meaning; inventing a spawn-time trap would be legislating. Capture-by-copy stays the documented interpreter semantics (approximation-contract §10.2, corpus exemplar
conc/store_buffer.lu), and the gap is filed as S-10 in the divergence log for a spec ruling (the E1004/E1005 precedent). - Conformance bundle: 203 programs / 187 records (the two new freeze litmuses); manual and export ledger updated in the same breath.
0.1.0 — 2026-08-10
The binary is named lupin; the package and repository stay wolf-interp.
lupin FILE.luruns a file;lupin -reads a program from stdin; barelupinopens the REPL. The exit code reports the program: its ownexit(N), 2 on a static-phase rejection, 3 on a trap or UB finding, 4 onunsupported.runis the explicit subcommand spelling and carries--seed,--scheduleand--json(the spec/06 observation record at the front door). A subcommand name wins over a file of the same name.lupin eval 'CODE'(short spelling-e) evaluates a snippet in a fresh session and prints what the REPL would print.lupin check FILE...runs the frontend only (lex, parse, resolve).- Observation records and bundle manifests report
"impl": "lupin"withimpl_version0.1.0;--versionprints the version, the package, and the upstream pin.