The two loud siblings filed in d3822d77: (a) an auto-deref *[N]T
indexed base (hs[k].result) failed cgplaceaddr's N_INDEX arm, which
knew only TY_SLICE/TY_ARRAY bases — add the one-pointer hop (the
(*hs)[k] form made implicit; load shape is the slice .ptr word's);
(b) a bare slice-element aggregate source (use(sl[k])) fell off
aggarg_srcaddr's ident-array-only N_INDEX arm into the #271 loud
stop — route the miss through the same cgplaceaddr fallback the
N_DOT arm uses. Both stages, byte-identical (probe matrix: let-copy,
field-assign, call-arg, return, indexed-store all IDENT).
After this, every ADDRESSABLE aggregate source resolves: ident
(local/module-let), deref, dot-chain, and index over array/slice/
ptr-array spines recurse through cgplaceaddr; the remaining loud
paths are rvalue sources (#40 family) and the #274 const-def arg,
both documented divergences. 8 rows added to idxdot_aggcopy_test.
The last Open-driver-work bullet. `-c -o <name>` replaces the fixed
<package>.test stem for exactly one package; the coordinator rejects a
multi-package fan-out ("cannot use -o with multiple packages", Go's
`go test -o` rule) and -o without -c is rejected at the driver
("needs -c for a package target" — a plain run executes from the temp
root, so a caller-owned name has nothing to name). Both driver stages
byte-identical wording; -S keeps its single-file-only reject.
package_test gains the contract row (naming, fixed-stem absence,
artifact runs, both rejects, both drivers); the tree-mode -o row's
pinned wording follows the contract.
The dot-chain SOURCE-address walkers (cg_dotchain_addr /
dotchainaddr) deliberately have no index hop — their AX-clean spill
contract serves the chained dotbase arm — so an aggregate copy from
elem[k].field (N_DOT over N_INDEX) fell out of the walk. Every
aggarg_srcaddr consumer loud-stopped (arg push #271, aggregate
return #272, field assign, structlit fill, ident reassign) EXCEPT
the indexed element-store arm, which left the walker's result
UNCHECKED and copied through a stale SI — a silent both-stage
miscompile (out[j] = hs[k].result read garbage). Task #6.
Both stages, two twin edits each: the aggarg_srcaddr N_DOT arm falls
back to cgplaceaddr (the C4/#40 place resolver, emission-free-on-
failure so the fallback starts clean), and the indexed store site
checks the walker before the same fallback.
Still loud (filed follow-ups): auto-deref pointer-to-array indexed
bases (hs: *[4]T; hs[k].field — cgplaceaddr's index arm takes no
deref hop; the explicit (*hs)[k].field form works), and the bare
N_INDEX slice-element aggregate source.
Evaluate the call pre-grow into a per-site scratch, receiving by the
N_LET matrix (sret / float / odd-tail / GP), then grow, slot, sized
ladder. Both stages, byte-identical. The two reject pins graduate to
16B GP accept rows; three new fixtures cover 24B GP, 3B/4B tails,
16B float, and 40B sret.
25 renames (git mv, content untouched). _test.ww is what the package
coordinator's test detection and the sep loader's canonical exclusion
key on; the old *test.ww spellings survived only through the
line-leading-@test compatibility scan. Consumers updated in place:
LIBRARY_TESTS, the libbyteid roster, the 901/974/975/976 carriers that
copy or invoke these files, and the check.c/check.ww + path/ftos
comments that cite them. Closes the open-driver-work migration bullet.
A -w DIR workdir replaces the fresh .sepwork scratch with a caller-owned
persistent package-artifact tree. A package is reused only when its
freshly composed unit byte-equals the committed unit and byte copies of
the compiler/assembler recorded in the dir equal the live tools — pure
content identity, no mtimes, no hashes, every decision reproducible
with cmp against plain files. Recompiles stage at .new names and commit
by rename, unit strictly last, so an interrupted build forces a
recompile and can never leave a committed unit vouching for uncommitted
artifacts; .o/.a additionally reject zero size (ELF/ar are never
empty), while .s/.wwi accept legitimate empties (FFI-only rt). A mode
stamp pins the -T/-S shape and the artifact protocol revision. Classic
scratch keeps its exact acquire/refuse/cleanup contract; run rejects
-w; dir-mode test rejects -w; both driver stages implement identical
behavior and wording.
PROJECT_PLAN.md was a historical disclaimer wrapping five still-open
T1 driver items — those move to docs/test-system-v2.md (Open driver
work); the rest is dated 2026-08-05 design history, archived in git.
Also retired-mechanism cleanup: both drivers drop the *.combined.ww
enumeration skip (the amalgamator is gone; leftover debris now fails
loudly instead of being silently accommodated), the consumer-less
m4_combined_epoch.md5 pin is deleted, and the wwi.ww/ww.h/check.ww
comments that still described combined.ww as the live path are
re-cited to the sep reality.
x.slicefield[:] / x.strfield[2:] emitted MOVQ $0 for the omitted hi
bound on BOTH stages (N_IDENT-gated dispatch; symmetric, so byte
identity never caught it) in all four sites: cgexpr N_SLICE + pushargs
(cstage), cgslice + pushargsrev (wwstage). The new arm re-evaluates
the pure field read for its {ptr,len,cap} header and takes .len,
covering local, viaptr, dot-chain, (*p), arr[i], and global inners.
Call inners still loud-reject upstream. Sibling of the #252/#257
array-field arms.
peel-ok/sizelint-ok/primsize-ok annotations lose their tools; sites
keep the WHY in plain words. Citations of retired carriers move to
their fixture or @test successors (949_errtype_compare -> r949_*,
900_stdlib -> library owners).
Discovers test/wcc/data, hard-pins corpus identity (1,224 fixtures,
314 error / 12 compile / 136 run / 762 run-exit, sha256 name hash),
schedules both frontend cells per fixture through os.exec with
per-cell capture dirs, and emits the strict wwfix TSV result stream.
integration.sh is the black-box owner of the CLI/protocol boundary.
Scratch acquisition is one bare mkdir that refuses a pre-existing
.sepwork; the path reaches cleanup only after acquisition succeeds.
ww build and ww test -o keep the tree for the caller; ww run and
no-output ww test build under an owned /tmp/ww_{run,test}_<pid> dir
and remove it on every result, escalating rc on cleanup failure.
The content-keyed package cache is deleted from both stages; -S stops
after per-package .s emission; --sep is gone and unknown flags are
rejected; directory ww test delegates to the wwtest coordinator; the
wwstage driver routes every subprocess through os.exec.
The wwstage Makefile recipes move off the persistent <tool>_ww.d
build-in-place shape to a per-invocation mktemp workspace: the old
recipes relied on the retired pre-wipe semantics and any rebuild
would trip the new refusal on their leftover main.sepwork.
The three 989_pkgcache_* carriers lose their subject with the cache
and go with it; the rest of the wave's carrier adaptation follows in
its own commits.
Single-directory package-test planner: discovers *.ww, groups white-box
and external <pkg>_test sources, composes the combined test package,
builds it through the sibling ww driver, and runs it with the runtime's
-package/-list/-timeout-ms contract. test/package holds its E2E corpus
and the manual runtime fixture set.
A module-global base has no frame slot; treating its local-lookup miss
as offset zero wrote .ptr/.len/.cap at the caller return address. LEAQ
the symbol like the struct-field global arms do. Both stages.
The a[i] = src copy loop enumerated ident/field/deref sources; an
N_INDEX rhs fell to the scalar tail and truncated the element. Route
it through the generic place-address funnel. Both stages.
The direct-field assignment arms enumerate CALL, STRUCTLIT, and local
IDENT producers; an addressable N_DOT/N_INDEX/deref rhs fell through to
the scalar tail, so a 16-byte struct field copied only its first word.
Resolve both places through the existing address funnels and use the
tail-aware aggregate copier. Both stages.
A str `def` has no name(SB) header; the streq push arms read a frame
slot that does not exist. Load the literal through cgexpr (AX=ptr,
BX=len) instead. Both stages.
Flip the soft-default to a hard "missing package clause" error symmetrically in
both stages (cmd/wcc/parse.c + lib/ww/syntax/parse.ww): the first real decl of a
primary section with empty pathmod/resetmod and no seen clause is now rejected.
Closes the documented soft-default divergence (the 63-wrapper carve-out).
The gate flip can't be split from the migration it breaks, so this is one atomic
commit: ~80 test/wcc wrappers gain `package main;` via a shared wwtestpkg.h
helper, 6 data fixtures plus 17 asm-grep assertions update for the bare->main.<leaf>
root-helper mangle shift, and rt/ declares `package rt;` with @symbol pinning the
bare rt_ensure/rt_malloc linker names.
Root mangling narrows: the executable entry `main` stays bare (existing
carve-out), but root helper symbols become main.X. The #84 cluster is rewritten
to assert main.run distinct from aa.run/test.run; its cgen fix and bare machinery
are retained — still load-bearing for package-less module-reset deps. New
table-driven test 782_strict_package.c (6 rows, both stages).
Retiring //ww:module-reset is deferred to #24b: it is load-bearing (clears the
.wwi pathmod so the body's package clause asserts), not a vestige; fusing its
removal here would be a silent mismatch.
All byte-id gates green; full make test reports "all 335 tests passed".
Completes the #14 close-by-construction begun by the helper extraction (e7fefa3): every <=24B aggregate register-store now routes through cg_agg_reg_store/cgaggregstore and handles all tail sizes. B/C/D/G fix a silent both-stage drop of a 3/5/6/7-byte tail (byte-id-blind: both stages dropped identically, so the gate could not see it). A/F enable a previously loud-rejected shape (a cgen backend gap, not a type rejection; harec accepts and lowers it). G (global g=f() array) routes symmetrically, dest_padded=false. The #11 arr[i].f scratch loops fold into the helper (dest_padded=true, byte-id zero-change), completing the grep-audit. Pins value-assert each eightbyte (the class is byte-id-blind) and redden under each stage's independent revert; site H's 3/5/6/7 let-receive stays a loud fatal (#22).
A stack array literal returned into a tagged-union slice success variant (fn mk() ([]i32|e) = { return [10,20,30]; }) slipped past reject_arrlit_borrow — it bailed when the dst was not TY_SLICE, but a union dst is TY_TAGGED — so cgen built an all-zeros slice header: a silent both-stage miscompile (and the .ptr would dangle anyway, no outliving backing). Extend the reject to chase a TY_TAGGED dst to its slice success variant, then apply the existing reject; this is the #25/#31 treatment seen through the union, and matches Hare (rule-9). Reached by all acceptance sites (return/assign/call-arg) so the class closes by construction. Both stages converge on an identical accept/reject decision (an array literal assignable to a union is assignable to a slice or array variant; neither stage can accept it). Full support — promoting the literal to an outliving backing — is the separate #33 arc. Compile-error fixture + a positive over-reach guard (a real slice into the union still compiles). No asm emitted by a reject, so byte-id is unchanged (no floor ratchet).
cg_structlit_fill/cgstructlitfill dispatched scalar field stores as {1->MOVB, 4->MOVL, else->MOVQ} with no fsz==2 case, so a 2-byte field was stored with an 8-byte MOVQ. Interior over-stores were harmlessly overwritten by the next field, but the LAST field at the frame edge corrupted the saved base pointer: an (S|e) union success variant places the struct payload after the 8B tag, landing the last field at -4(BP), so MOVQ AX,-4(BP) wrote into saved [BP] and POPQ BP restored garbage — a silent both-stage caller-frame clobber. Route the scalar store through the existing fldstoreop/fieldstoreop helper ({1->MOVB,2->MOVW,4->MOVL,else->MOVQ}), both stages; the #13 graduation comments already pre-documented this resolution. Pure width fix, no loud-stop (scalar widths are always {1,2,4,8} and narrowing is always correct). Value-asserting pin: an i64 sentinel live across the union-maker call (detects the clobber directly) + all members, with a non-union control.
A struct/array success variant in an (S|e)! / r? unwrap dropped eightbytes on BOTH stages (byte-id blind). Two layers: (L1) the unwrap success shuffle (cgtrytaggedshift) matched no arm for a struct/array success and fell to a bare MOVQ DX,AX, materializing only w0 — widen the existing nested-TAGGED shift's gate to admit TY_STRUCT/TY_ARRAY (the in-cap union packs the payload as raw GP words past the tag, so that shift is exact); (L2) the aggregate store arms gated on rhs.kind==N_CALL and stored one word for an unwrap rhs — relax to also admit N_TRYUNW/N_TRYPROP at the three silent store shapes (arr[i]=, single-dot field, indexed-field), reusing the materialise scratch path (now #10-correct). Rule-7 LOUD-STOP for a float-bearing success variant (an SSE eightbyte cannot ride the GP {AX,DX,CX} shift, #165). The four already-loud unwrap consumers (let-receive #7, call-arg #271, assign-existing #49, resolver-field #24) stay loud; global/chained single-dot field (#16) and the sub-8-tail-through-unwrap union-maker frame clobber (#15) are separate follow-ups. Value-asserting pin, reddens under each stage's independent revert.
The in-cap aggregate-receive materialise emitted a single narrow tail MOV that fell to MOVB for a 3/5/6/7-byte sub-8 tail, storing one byte while the scratch->dest copy read the full tail from uninitialised scratch — silently dropping members at the C2c whole-element arm (arr[i]=mk()) and loud-stopping at the #11 field arm. The scratch slot is ceil-8 padded (local_alloc/localadd round to 8) and the copy reads only tsz bytes, so flipping the tail default MOVB->MOVQ stores the full register harmlessly into the slot's own pad (in-bounds for in-cap <=24B); 1/2/4-byte tails stay byte-identical. Both stages symmetric. Removes the now-redundant #11 sub-8-tail loud-stop (keeps the float #165 and over-cap #234 loud-stops). The same narrow-tail materialise recurs at 6 other cstage sites (task #14).
Retires the obsolete idx_dot_aggret_subtail_loud //ww:error fixture (both stages now compile the case) and converts it to a positive cstage run-test; the struct-field shape is byte-id-divergent only via the pre-existing #9 frame-size bug, so the value pin uses array-field shapes. Value-asserting, reddens under each stage's independent revert.
The N_TRYUNW/N_TRYPROP success shuffle materialized {ptr,len,cap} into {AX,BX,CX} only when the success variant was a str; a slice success got only MOVQ DX,AX (ptr), leaving every slice-unwrap consumer (call-arg push, let-receive store, ident-source) reading junk .len/.cap — silent on BOTH stages (byte-id blind, cstage not the oracle). Widen the success gate to type_isstr||type_isslice (cstage) / typeisstr||typeisslice (wwstage) at all four shuffle sites; str and slice share the identical 24B header shuffle. Stays str||slice-specific — a struct success variant uses a different {AX,DX,CX} ABI (task #12). Value-asserting pin (len!=cap, poison-decoy) reddens under each stage's independent revert.
Follows #6 Mech A (Fix-R); order forced (C1 first or the slice call-arg push reddens byte-id).
The arr[i].f=src legacy assign block enumerated scalar field-type arms then fell to a 1-word scalar default, so a non-call aggregate source (ident/dot/index) cgexpr'd only its first word into AX and stored one eightbyte — silent on BOTH stages (byte-id blind). The non-indexed bases (local/deref/chained/global) reach the general assign resolver's canonical aggargsrcaddr+aggcopy; the indexed arm short-circuited before it. Route the indexed base through the block's own proven &arr[i] spine into the same aggargsrcaddr+aggcopy emitters (DRY — no third copy), dual-site symmetric. Unlike #11's in-cap arm, the source is a memory address so aggcopy is a pure memcpy: float bits and the sub-8 tail transport verbatim, no loud-stop needed. Did not fall through to the general resolver because its cgplaceaddr N_INDEX arm rejects a *[N]S (TY_PTR) base (latent resolver gap, filed separately).
Contained to the indexed base + non-call aggregate-field rhs; value-asserting pins redden under each stage's independent revert.
The arr[i].f=mk() assign arm had no aggregate-field sub-arm, so a by-value aggregate field receive fell to the scalar default (one MOVQ, dropping DX/CX) — silent on BOTH stages (byte-id blind). Add a dual-site symmetric in-cap N_CALL arm mirroring C2c (c83a340): scratch-first materialise AX/DX/CX, then word-copy to (fi.foff+k*8) within &arr[i], sizing from the natural field size fi.fsz (not slotsize). Rule-7 LOUD-STOP for the three cases the in-cap GP path cannot transport: over-cap sret (#11c/#234), a float-bearing field whose eightbyte classifies SSE (#11/#165), and a 3/5/6/7-byte sub-8 tail the single narrow tail MOV cannot express (the general cascade tail is the shared C2c/#11 follow-up, task #10). Value-asserting pins (poison-seeded, redden under each stage's independent revert) plus cfail pins for the three loud-stops.
Contained to the indexed base + in-cap call rhs; arr[i].f=src (#11b) and over-cap (#11c) are separate.
cstage rejected a deref-less function-pointer call `f(21)` (f: *fn...) with
"calling non-function" while wwstage accepted it and ran correctly -- a
cs!=ww divergence. Hare auto-dereferences a pointer callee to its fn type
before the call. Peel one pointer level after the named-type chase so
cstage accepts a `*fn` callee, matching wwstage and Hare.
One level only, deliberately: the #181-cgen indirect-call path lowers the
callee VALUE as the target (CALL AX), which is the fn address for a single
`*fn` but only the address-of the fn-ptr for `**fn`. A multi-level peel
would accept `**fn` past what cgen can lower -- a silent both-stage
miscompile (returns garbage). cstage stays loud on `**fn` (rule 7);
wwstage's loop-accept of all levels (check.ww:3763) is the over-permissive
side, filed for align-down plus the deeper cgen multi-level autoderef.
Surfaced by the codegen miscompile hunt (finding C6a). Pinned by
test/lang/fnptr_derefless_call_test.ww (deref-less + canonical (*f)(21),
multi-arg, alias-chain, tuple-return; reverting the peel reds the
deref-less rows at compile).
`alloc(Outer{ x = Inner{q=10} })` dropped the nested struct-literal field:
the alloc path had its own inline fill loop with only scalar/float/str
arms, so a field whose value is itself an N_STRUCTLIT fell to the scalar
tail and stored MOVQ $0 (cgexpr leaves a whole aggregate in no register)
over the inner slot. Both stages emitted the identical wrong fill, so the
byte-id gate was blind to it.
Route alloc's fill through the existing shared structlit-fill helper (the
one the BP-relative/global/local structlit sites already use -- it handles
nested-struct recursion, N_ARRLIT, str/slice and tagged) via a new 4th
destination mode DST_PTR_SP that reloads the heap base from (SP). This
deletes alloc's divergent inline loop, the lone site lacking the recursion.
As a side effect it also fixes a latent slice-field drop in the driver's
own alloc(sepgraph{...}) (pkg.len/.cap were dropped; the consumer reads
neither -- g.n is the count SSoT). Nested-array fields are closed in-class;
a nested tuple-LITERAL field now errors loudly and symmetrically (the #49
non-addressable gap, previously dropped silently at alloc only).
Surfaced by the codegen miscompile hunt (finding C7c). Pinned by
test/lang/alloc_nested_field_test.ww (nested struct depth 1+2, nested
array, adjacent multi-nested, sibling-no-clobber; reddens on revert).
Routing preservation proven: the whole test/lang corpus is byte-identical
HEAD vs fixed except the new pin; self-compile byte-id (990-996) green.
`arr[i] = mk()` where mk returns an in-cap (<=24B) struct/tuple/array
left the result in the #4 cgreturn registers (AX/DX/CX), but the
N_ASSIGN-into-N_INDEX path had no arm for an N_CALL rhs, so it fell to
the scalar store tail: only member 0 was written and the index scale
clobbered CX. Both stages emitted byte-identical wrong code (the
documented-but-silent #31-G gap), so the byte-id gate was blind to it.
Add an in-cap N_CALL-rhs arm: materialise the return into a frame scratch
first (keeping the CALL at the frame's natural 16B alignment), resolve
&arr[i], then word-copy the full eightbyte count + sub-8 tail -- mirroring
the #4 receive shape and the #270-1b copy. The eightbyte count derives
from the element size in the type table. Over-cap returns (#234),
non-call rhs (#270-1b) and tuple literals (#121) are unaffected; the
sibling field/deref shapes stay loud (#24).
Surfaced by the codegen miscompile hunt (finding C2c). Pinned by
test/lang/idx_aggret_recv_test.ww (10 value-asserting rows: 2/3-eightbyte
structs+tuples, array elem, sub-8 tail, const/runtime index, all four
base shapes; reddens on revert).
An untyped float literal defaults to f64, so in an f32 context it was
materialized as f64 then bit-truncated by a raw MOVSS (low-32 reinterpret)
rather than narrowed -- e.g. `let x: f32 = 2.0f32; x * 3.0` multiplied by
0.0f. Twelve byte-id-gate-blind both-wrong miscompiles, all this one cause
(compare, binop, call-arg, struct-field, array-elem against an untyped
literal).
Broaden coerce_floatlit to stamp the untyped fconst type_=f32 across the
f32-context sites (assign rhs, call-arg, struct-field, array-elem) and to
descend the implicit-cast shapes (peel unary +/-/cast, recurse binop
operands AND the binop node, recurse arrlit elems), mirroring harec's
lower_implicit_cast. The existing CVTSD2SS gate then fires; cgen is
unchanged. f64 contexts are untouched -- the stamp is gated on TY_F32.
Surfaced by the float codegen sub-hunt (= the deferred #120). Pinned by
test/lang/f32_untyped_narrow_test.ww (22 value-asserting rows incl. f64
controls; reddens on revert).
`let s: str = *h` (a str/slice loaded by value through *str / *[]T)
fell through the N_UN deref arm to the scalar load, emitting a single
MOVQ that read only the 8B .ptr and left .len/.cap from stale registers,
so len(*p) returned garbage. Both stages emitted byte-identical wrong
code, so the self-compile byte-id gate was blind to it. Add a str/slice
arm that loads the full {ptr,len,cap} via cgslicehdr when the chased
pointee is TY_STR/TY_SLICE.
Surfaced by the codegen miscompile hunt (finding C1b). Pinned by
test/lang/deref_hdr_test.ww, which interposes a different-sized decoy
header so the test reddens when the arm is reverted.
Reading gp.f through a module-global pointer miscompiled in BOTH stages,
differently: cstage classified gp as a local at boff 0 and derefed BP
(MOVQ (BP),BX), wwstage collapsed gp.f to an undefined global symbol f
(MOVQ f(SB)). Both now load the pointer value from the global's data slot
before the field offset, converging on MOVQ gp(SB),BX; MOVQ off(BX),AX.
cstage mirrors the #6 store decline; wwstage gains a global-ptr arm and
shares a cgptrfieldload helper with the local arm.
Fused, not split: the two stages must emit byte-identical asm, so a
one-stage commit would fail the byte-id gate. Sibling byte-divergences
filed: #16 (chained-spine gp.x.y), #17 (>32B tagged word-order).
Test: table-driven 689_globptr_field_read_run (24 rows, runtime + byte-id).
A module-global pointer's field store/compound (`let gp:*S=nil; gp.f += 5`,
`gp.in = Inner{...}`) SEGV'd in cstage: the enumerated N_DOT-lhs arms load
the base pointer with `MOVQ boff(BP),BX`, valid only for a LOCAL ptr slot,
but a module-global ptr has no local slot (localfind=0) so it dereferenced
the saved BP. wwstage was correct -- it routes these through its F6
cgplaceaddr resolver (its dedicated arm is scalar-`=`-only by design,
#60/#61). The byte-id gate was blind (no global-ptr compound in the
bootstrap corpus) and the deferral note was stale: this is a live cs!=ww
divergence with wwstage as the oracle.
cstage already has an equivalent assign-resolver (cgen.c ~7488) that emits
byte-identically to wwstage's F6 route, but the enumerated arms intercepted
the global case first. Fix (align cstage UP, cstage-only): two precondition
entry-guards decline a module-global `*struct` base for the compound +
non-scalar-field cases so they fall through to the resolver. Plain-scalar
`=` stays in the enumerated arm (its #47 fix already matches wwstage). The
decline and resolver accept-sets exactly partition the global-base
N_DOT-lhs space (no gap, no overlap); tagged/float field stores now both
loud-stop symmetrically (were SEGV'ing). The discriminant keys on
localfind-presence + let_islet, so a param at offset 0 stays local.
New both-stage + byte-id test 689_globptr_field_store_run covers offset-0/8,
compound, struct/str field, chained gp.x.y, indexed gp.a[i].f, with local +
offset-0-param controls. The field-READ path is independently broken in
both stages (filed #15). make clean && make test: all 403 passed, byte-id
990-996 green.
wwi_emit took the .wwi `package` leaf from the first primary decl's module tag;
a fully empty primary module body (zero decls) had none, so the leaf stayed the
literal default "main" and the importer rejected it ("package main does not
match import path <leaf>"). The module identity is only available at parse time
(curmod is overwritten by imported //ww:module sections before emit), so stamp
the primary path onto the N_FILE node (TK_MODULE and TK_MODRESET rp!=NULL sites,
only-if-empty so a bare-reset `package main` root stays "main") and, when the
decl-scan finds no leaf, fall back to that stamped path. Symmetric cstage+
selfhost; both detect scan-miss via the same found-flag so the emitted .wwi
stays byte-identical.
Regression: test/wcc/989_wwileaf_run.c, table-driven over {empty body,
comment-only, nested a.b.c} decl-less shapes, non-vacuity proven.
A torn producer write (e.g. disk-full mid-copy) could leave a 0-byte P.wwi or
P.o in out/.pkgcache under a self-consistent key; cache_lookup checked only
existence, so every later build HIT and served the empty artifact forever
(silent serve-wrong). Reject size==0 on both sides, symmetric across stages:
store refuses to commit a 0-byte temp before the key write, lookup treats a
0-byte cached artifact as a MISS so existing poison self-heals on re-derive.
A valid .wwi/.o is never 0 bytes, so the guard cannot misfire.
Regression: test/wcc/989_pkgcache_poison_run.c, table-driven over
{poison P.wwi | P.o | both}, non-vacuity proven by guard-neuter.
BUG-A #9: when an empty/export-less inlined module body produces `//ww:module <p>` immediately followed by `//ww:module-reset` in one skipws run, lexnext drained TK_MODRESET first then emitted the STALE TK_MODPATH past the reset boundary, re-binding pathmod=<p> so the root `package main` was validated against <p> -> wrong reject. Fix clears the pending modpath at the reset-RECOGNITION site in skipws (NOT lexnext drain): cstage l->modpath=NULL after both l->modreset=1 (cmd/wcc/lex.c); selfhost l.modpathset=0 after all three l.modreset=1 (lib/ww/syntax/lex.ww). Symmetric (rule-10): identical token streams, bootstrap byte-id holds. The normal RESET-then-PATH boundary is unaffected (clear is a no-op when no path is pending). Pin: test/wcc/989_modresetadj_run.c feeds the raw composed adjacency to w6c+w6c_ww (accept + byte-id), red-path-proven (reverting a clear reddens it). A separate wwi-producer defect surfaced (decl-less .wwi defaults to 'package main') filed as #11; not touched here.
A bodied fn with a bare C-style `...` was silently accepted by cstage
and SEGFAULTED wwstage (resolvefnbody walked a typeless `...` param).
Gate it: bare C-`...` is allowed only on bodiless decls (extern /
@symbol prototypes), the real FFI path; Hare-style `T...` is unaffected.
ww restricts C-`...` to bodiless decls pending vastart/vaarg/vaend
builtins (#16); harec permits bodied C-variadic fns (check.c:3656) -- a
documented divergence, reopened when #16 lands.
Test 852 runs both stages; its reject rows require the gate's diagnostic
(not merely a nonzero exit), so a crash can't pass them vacuously.
C99 6.5.2.2p6 requires float->double promotion for floats in the
variadic region of a call; w6c emitted MOVSS (4B), so a C variadic's
va_arg(double) read 8B of garbage. Promote at push (CVTSS2SD) so the
8B slot holds a real double, covering the XMM-reg and >8-float spill
cases uniformly; fi/AL XMM-count is unchanged.
cstage only; the wwstage twin rides C2 (#10), which adds wwstage's
C-variadic-call codegen.
SysV §3.5.7 requires a variadic call to set AL = number of vector (XMM) regs used for the variable float args; the C callee gates its xmm-save-area stores on `test %al,%al`, so the old hardcoded XORQ AX,AX (AL=0) made va_arg(double) read garbage for any float-bearing C variadic call. Emit MOVQ $fi,AX (fi = the in-scope XMM cursor, ≤8); w6a has no MOVL-immediate encoding so MOVQ is the assemblable form and sets AL=fi identically. fi==0 keeps XORQ → byte-identical to pre-fix for no-float variadic calls. Runtime test 989_ffivariadic links a cc-compiled va_arg(double) fixture (zero relocs/undefined, w6l-linkable) and sweeps N=3/5/8 floats (N=2 is vacuous via stale-stack aliasing). C1 of the C-FFI-variadic align-up (USER ruling); C2 wwstage + C3 bodiless gate follow. ref/qbe/amd64/sysv.c:384. 454 green.
The depth->=2 chained `t.m.l = s` struct-ident field copy selected its tail with (tail==4)?MOVL:(tail==1?MOVB:MOVQ), so every tail in {2,3,5,6,7} fell to an 8-byte MOVQ that over-wrote past the destination field — clobbering a @packed neighbour (t.m.l=s wrote s's slop over t.m.z: cstage exit 0 vs wwstage's correct 222) and diverging cs!=ww gate-blind. Replace with a descending 4/2/1 MOVL/MOVW/MOVB ladder comparing to ssz (the field's natural type-table size), aligning cstage UP to wwstage's sized ladder + cg_aggcopy. New table-driven gate 949_chained_dot_struct_copy_run (7 sizes x runtime-exit oracle on both drivers + cs==ww byte-id; negative-control proven).
A struct/array sized {3,5,6,7} returned by value fell past every N_LET call-receive arm (the sized-tail arm handles {1,2,4}, the memcpy arm gates on sz>8), so cstage emitted nothing and silently DROPPED the CALL — garbage exit plus a gate-blind cs!=ww divergence (wwstage emitted CALL+MOVQ via its generic let fallback). Align cstage UP: emit the CALL + one MOVQ AX, the SysV single-eightbyte receive (ref/qbe/amd64/sysv.c retr). New table-driven gate 949 (runtime exit oracle on both drivers + cs==ww byte-id; negative-control proven).
ww run / ww test created /tmp/ww_{run,test}_<pid>.sepwork/ but removed only
the built binary, leaking the scratch DIR every invocation — the tmpfs filler
(98,811 entries blocked the gate twice). Add a keepscratch param to
build_one_sep: a thin wrapper rm -rfs the impl scratch at the single
choke-point when keepscratch==0 AND the path ends ".sepwork" (covers every
return, success+error; fires post-link, pre-run). do_build keeps it (the
byte-id gates read <stem>.sepwork from ww build -o); do_run/do_test clean;
do_test no-o redirects scratch into /tmp. Both stages symmetric; reuses the
existing shell rm -rf idiom (lib/os.removeall = #109). Test 989_sepscratch_run
is self-scoped by child pid (non-flaky) with a KEEP control + revert-verified
non-vacuity. Full gate: 448 pass, zero new run/test leaks. (ww_d_* relic +
historical bulk = one-time sweep + agent-probe discipline, not code.)
The E3 flip (#87) made sep the sole compile path and deleted the
combined.ww writer, leaving the six committed *.combined.ww files dead.
Remove them and the last references to the retired amalgamator.
- rm the 6 tracked *.combined.ww (selfhost/cmd/{w6a,w6c,w6l,ww,wwdump}/
main.combined.ww + selfhost/test/smoke.combined.ww). Verified no live
build path or gate still feeds one as compiler INPUT.
- 901_asserttyped_gap: its 5 combined.ww gap fixtures were the last
combined.ww INPUT consumers (4 already missing/vacuous post-flip, only
smoke.combined.ww still fed). Retarget all 5 to sep-feed via a
resolveunit helper (whole-package-dir copy -> `ww build --sep` ->
<stem>.sepwork/__root.unit.ww), mirroring 990's #89 pattern; the 3
import-free test fixtures stay raw-fed. All 8 counts hold at 0 (A-D
coverage, vacuous since the flip, is live again).
- INV-2 (the driver's unresolvable-import-is-fatal guard) is KEPT; only
its "Mirrors the deleted expand" lineage tail is swept. The #110
combined_ww_fresh freshness gate was already removed in #89 (5f85852).
- Sweep dangling amalgamator lineage comments (build_one/buildone/expand/
peek_package/peekpackage + stale combined.ww/combined intermediates)
in cmd/ww/main.c + selfhost/cmd/ww/main.ww, symmetrically (rule-10),
and the stale Makefile combined.ww test-comments (enumcap bigmod.unit.ww
+ 784/787/792/794/848 sep .s cmp + make-smoke sep self-compile).
Closes M4 and epic #22. all 445 pass; 990/993/994/995 byte-id HOLD;
sizelint clean.
The out/.pkgcache content-keyed store copied each artifact IN-PLACE
(cp -f / copyfile) to the fixed paths P.wwi/P.o/P.key. Key-last gave
crash-consistency but NOT concurrent-read safety: two same-stage builds
of a shared lib pkg (rt/time/os) target one out/.pkgcache/<pkg>/P.{wwi,o};
once an early finisher writes P.key, a later build's cache_lookup copies
P.wwi/P.o while a mid-finisher is still mid-write -> torn read -> corrupt
link / cs!=ww. The key is content-only, so it is purely the non-atomic
write.
Fix (Go-build-cache pattern, both stages in lock-step, rule 10): write
each artifact to a per-pid same-dir temp (P.wwi.tmp.<pid> etc.) then
rename() into place. Same dir => rename is atomic (cross-fs is not);
per-pid temp => concurrent writers don't clobber each other mid-copy;
content-keyed => last-writer-wins is byte-identical. Key renamed LAST so
a reader that sees the new key always finds complete artifacts. On any
mid-store error the per-pid temps are unlinked so a failed store leaves
no litter (cstage goto cleanup; wwstage cachermtmp helper).
cstage cmd/ww/main.c cache_store: libc rename(2) + getpid().
wwstage selfhost/cmd/ww/main.ww cachestore: new os.rename + cachetmp.
lib/os/os.ww: add rename(2) (RENAME=82), ref/hare/os/os.ha:17 -- returns
raw i32 errno like sibling remove/mkdir/rmdir (ww's os is the flat
syscall floor, no fs:: layer); a second pathbuf2 slot holds newpath
since kpath's single pathbuf can't carry both paths.
cache_lookup is unchanged: it reads cache->private scratch, and an atomic
source is never torn.
The torn-read race is closed BY CONSTRUCTION; a deterministic behavioral
regression-guard isn't feasible through the product build path (content-
keying => concurrent COLD builds all MISS+STORE, never HIT-read a mid-store
entry; a warm cache is never re-stored). The deferred white-box guard is
TASK #105. A WHY-comment at both fix sites records this.
Tests: 989_sepbuild_run KEEPS its private per-pid WW_PKGCACHE -- the
comment is corrected: the pin is NOT a torn-read mask (closed by
construction) but cold-compile isolation for the test's INTERMEDIATE
(.s/.unit.ww) byte-id compare, which a cache HIT legitimately skips
producing. The former 989_pkgcache_atomic_run is renamed to
989_pkgcache_concurrent_run and HONESTLY relabeled: it is a concurrent
shared-cache build-correctness smoke (N concurrent --sep builds sharing
one cache -> every binary byte-identical to an isolated reference + correct
run, both stages), NOT a torn-read/atomicity proof (a review revert-
experiment proved the original claim vacuous). Shrunk to 4 concurrent
builds x 1 batch x both stages. COLD/dev-only, off every byte-id/bootstrap
gate.
selfhost/cmd/ww/main.combined.ww remains stale (its writer was deleted at
the M4 E3-C1 flip; #90 deletes the file) -- not regenerated.
make test: all 445 passed; make sizelint clean; 990-997 byte-id hold.