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.
wwstage cgen resolved struct field LAYOUT (offset + field type) by bare-leaf
name (structlookupchain / structlookup / letvarstructinfo) at nine field
READ / addr-of / let-copy receiver arms whose base is a *struct pointer, a
value struct, or a module global. Under a cross-module same-leaf collision
(two modules each exporting a `pair`, 16B vs 24B) the bare-leaf lookup
first-matches the wrong-size struct -> the field is read / addressed / copied
at the wrong offset with the wrong width. cstage type-keys off the stamped
node.type_ (type_chase_named(base->type)->fields) and is correct; this aligns
wwstage UP to it (ww-only change).
Route the nine arms off the stamped receiver tinfo: R1/R2 *struct reads via a
new shared choke-point cgptrfieldloadtf (the tinfo twin of cgptrfieldload);
A1/A2/A3 addr-of and W3 scalar global-store via tichase(recv.type_)->fields;
C1/W4a copy/size via structabisizetn(tichase(.type_)). Mirrors #21 (5ae6e34);
the *struct arms peel the pointee with tichase(.type_).sub, the faithful twin
of cstage type_chase_named(bu->sub).
The global value-struct arms (R2/R3/A3/W3/W4a) are converted-for-construction:
a global struct's type is always explicitly qualified, so the bare leaf already
resolved correctly and they cannot be reddened -- byte-id (cs.s==ww.s) is their
net, not a value pin. Labelled so the absent reddening pin is explained, not
silent.
Commit 1 of a 2-commit arc (RULING R2 / Opt-2, .ai/ken-31-spec.md): closes the
field-LAYOUT read/copy/addr surface. Commit 2 converts the W1/W2/W5/W4b store
loops to a tinfo-native fill (cgstructlitfilltn + sretretsizetn) and closes the
in-loop nested sub-arms by construction. Part of the #224 name-keyed-cgen
cluster retirement.
Pin: test/wcc/797 value-asserts R1 ptr-read / C1 let-copy / A1/A2 addr, each
reddening under independent per-arm revert (a ratchet proves nothing for this
silent-capable class).
wwstage cgen resolved struct LAYOUT by bare-leaf name (structlookup /
structparamsize) at three caller-side sites — the by-value arg push
(cgenutil), the let-receive copy width (cgenstmt), and the field-read
offset (cgenexpr). Under a cross-module same-leaf collision (two modules
each exporting a `pair`, 16B vs 24B) the name lookup first-matches the
WRONG type, so the push dropped the 2nd eightbyte, the receive over-copied,
and the field read the wrong offset. cstage type-keys off the stamped
tinfo and is correct; this aligns wwstage UP to it (ww-only change).
Route all three sites through the stamped node.type_ via a new
structabisizetn(*tinfo) accessor (push + receive) and tichase(type_).fields
(field-read, structlookupchain removed). One commit (rule-11 carve-out):
the collision drives all three at once and no per-site fixture isolates, so
it cannot bisect-split. A scoped slice of the #209/#211 name-keyed-cgen
cluster retirement; the cgdot *struct-ptr/global and let-copy siblings stay
name-keyed and are filed (#31).
New table-driven test 793_xmod_struct_argpush_collide_run (4 scenarios:
push/recv/field over 16B and 12B tails) reddens under a revert of the three
cgen files. Full make test green (336 passed); make sizelint clean.
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".
fold-2 chunk C5 (drew's Fam8-13 plan), the highest-risk chunk: 21 match/tagged
value-row C drivers re-homed. 20 -> test/lang/*_test.ww @test row-tables + 12
runww //ww:error carriers (both stages reject). The global-tag cluster
(globtag*/globstructwiden/taggedderefstore/...), which sits on the #15/#17
global-ptr fix, was empirically probed byte-id CLEAN -- the predicted hotspot
surfaced ZERO fresh cs!=ww. Carves: variant_chain_b95 #81 -> _runonly (genuinely
diverges at HEAD); callret_bound277 #277 -> slim C pin (cs-runs/ww-rejects),
mutation-gated. 929_tagged_memarg kept whole (SSE-ABI asm conformance). 19
drivers deleted, 944_variant_chain slimmed to the #277 pin.
The match-on-tagged-struct-field divergence (former #26) probed RESOLVED for all
its cited shapes (938 voidstr_field/recursion_torture, tagnorm dedup_match all
byte-id cs==ww + value-correct) -- closed no-reproducer-at-HEAD, attribution to
#15/#17 INFERRED. Those rows migrate as normal byte-id @test and serve as the
REGRESSION SENTINEL for the inferred close (a resurgence trips the gate).
LANGBYTEID floor 93->113; test count 374->355 (19 deleted; 929 + 944_variant_chain
kept). do-not-auto-batch (926_tagscr/940_global_sret/940_str_forrange) untouched.
fold-2 chunk C2 (drew's Fam8-13 plan): 14 tuple value-row C drivers migrate to
15 test/lang/*_test.ww @test row-tables (the +1 is 954_tuprecv, slimmed not
deleted -- its value rows split out while the asserttyped-stamp dimension stays
as a carrier-split C pin, mutation-proven non-vacuous). Reject rows move to 32
test/wcc/data/*/case.ww //ww:error carriers (runww asserts the substring in
BOTH stages). The test-lang byte-id (LANGBYTEID) gate gives cs==ww automatically
and is strictly more sensitive than re-running the wwstage leg; floor 59->74.
Tuple surfaced zero cs!=ww as the plan predicted -- no value-only carve. The 945
trio folds in here; 940_global_sret / 940_str_forrange / 926_tagscr untouched
(routed to drew per-file). Test count 402->388 = the 14 retired drivers.
fold-2 chunk C1 (drew's Fam8-13 plan): 960_opaque_decl_run.c and
962_opaque_assign_cast_run.c were value-row C drivers. Migrate their 3+3
cases to test/lang/opaque_decl_test.ww and opaque_assign_cast_test.ww as
@test row-tables. The test-lang byte-id (LANGBYTEID) gate gives cs==ww
automatically -- 960 was cstage-only-run before, so this strengthens it.
Both byte-id clean, no cs!=ww carve. Retire the 2 C drivers (LANGBYTEID floor
57->59) and repoint a dead comment ref in lib/sort/sort.ww. 961_opaque_guards
(reject/guards) stays in C -- fold-3 territory, not in the Fam12 fold-2
worklist.
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.
The 9 Makefile recipes that build selfhost/cmd/<tool>/main.ww (5 primary
$(BIN)/*_ww + 4 nocc bootstrap) passed no -o, so the compiler's scratch
followed the SOURCE stem and left selfhost/cmd/<tool>/main.sepwork in the
tracked tree (the binary was mv'd out; the .sepwork dir stayed behind).
Each recipe already cd's into a gitignored out-of-tree dir before building,
so a uniform `-o main` makes the scratch follow the OUTPUT stem (cwd) out
of the source tree; the binary still lands at cwd `main` and the existing
`mv` lines are unchanged. Same intermediates-follow-output principle as the
T3 -o redirect (rule 14).
Carved from #8 by unanimous advisor ruling as its own bisect-clean commit.
make clean && make test: all 402 passed, zero in-repo .sepwork, zero /tmp
scratch.
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.
#152 link-the-let-before-its-init miscompile (miscompiled IDENTICALLY on both stages, so byte-id 990-997 was blind; only a runtime value check catches it). Lifted the run fixture into test/lang/letshadow_test.ww (primitive @test value pin; byteid-eligible — symmetric, cs==ww). byteid floor 56->57. Closes the fold-3 #7 umbrella (last C test twin retired).
#154 cstage-only str== global-header miscompile (str== fast-path read the global str header off BP+0 instead of name(SB); byte-id 990-997 blind to a runtime-value miscompile). Lifted the run fixture into test/lang/strglobeq_test.ww (primitive @test value pin; byteid-eligible — post-fix cs==ww). byteid floor 55->56.
949_dotfield_compound + 949_idxfield_compound are one bug class (#133-lineage compound-assign load-op-store on field lvalues; #34/#33, #263 carve-out) sharing the combine + hard-error path, so the two C carriers fuse into one commit: 26 value rows -> test/lang @test row-tables (primitive-only asserts), 8 reject rows -> runww //ww:error dual-stage carriers. byteid floor 50->52.
#133 indexed-scalar compound-assign. The 21 rows split by observability:
18 value rows -> test/lang/idx_compound_test.ww (one @test fn each,
primitive-only asserts, slot-poison + neighbor-unchanged read-back);
3 reject rows -> test/wcc/data/idx_compound_{float_indexed,str_indexed,
chained_ptr_float}/case.ww as //ww:error, asserting BOTH stages reject on
the shared diagnostic body (cstage's leading prefix excluded). The C twin
is retired; its coverage is a strict superset of the original rows.
First reject-bearing fold-3 migration -- validates the runww dual-stage
home end to end. LANGBYTEID_EXPECTED_MIN 49 -> 50.
test-run drives runww.ww over the test/wcc/data/*/case.ww behavior and
//ww:error cases, but was not a prerequisite of any aggregate, so the
reject surface was ungated. With runww's arm now dual-stage, gate it.
Added to the pre-push `test` target only -- not test-unit, not the
test-commit content-key tier. runww execve's its children with nil envp,
so a WW_PKGCACHE override cannot reach them; they fall back to the default
out/.pkgcache that $(TESTS) and the byte-id gates also use. A normal
prereq would let make -j overlap test-run with those writers, so test-run
carries an order-only prereq (| $(TESTS) test-lang test-lang-byteid) and
is scheduled strictly after every default-cache writer. No cache
isolation is added: it would be inert under nil envp, and the ordering
prevents the race.
True per-run cache isolation stays blocked on an os envp() forwarder.
Continue fold-2: migrate the struct-by-value / sret round-trip family from
bespoke build+run C twins to test/lang @test, retiring each twin in the same
commit. Runtime coverage MOVES from $(TESTS) to test-lang (T1 runs+asserts via
`ww test`) + test-lang-byteid (T2 keeps cs==ww .s byte-id); the $(TESTS)
headline drops 6. Every assert is a primitive int/u8/bool comparison (no
fmt/strconv in the assert path); each returned struct/tuple FIELD is asserted
individually so a dropped/mis-offset/over-wide word FAILS. 46 @test cases, a
strict superset of the 46 C rows (799=4, 925=10, 930=6, 949odd=7, 949chained=9,
949aggret=10).
799_tuple_sret_receive_run.c -> tuple_sret_receive_test.ww (#10 Fold B: over-4-GP tuple `([]u8,[]u8)` sret RECEIVE — destructure/single-var/return-forward/reassign; len() only on destructured bindings, never len(t.N))
925_sret_struct_return_run.c -> sret_struct_return_test.ww (#23: >24B sret round-trip; 32B/40B/nested/slice-payload, reassign-receive, struct16-by-value-arg #11 collision, N_IDENT return rhs, forward #9 simple/multi-arg/slice)
930_sret_narrow_field_run.c -> sret_narrow_field_test.ww (#33: sret narrow trailing-field copy — bool/u8/i16/i32 + mixed bool+i32+i64 after the 24B slice)
949_oddstruct_byval_ret_run.c -> oddstruct_byval_ret_test.ww (#107: by-value return of odd sub-8 size {3,5,6,7} single-eightbyte + 8/12/24 boundaries)
949_chained_dot_struct_copy_run.c -> chained_dot_struct_copy_test.ww (#107 sibling: chained-DOT `t.m.l = s` natural-size tail copy {0..7} + via-CX global dest; neighbour z is the oracle)
949_aggret_source_run.c -> aggret_source_test.ww (#272: aggregate return from every addressable source — arrlit/N_DOT/N_INDEX/deref/ident + >24B sret arm + global-receive caller-half)
This family is sret / struct-by-value-return (the #107/#38/#271/#272 ABI area):
all 6 new files are byte-id cs==ww (no fold-5 divergence surfaced). Bump
LANGBYTEID_EXPECTED_MIN 43->49 to ratchet the new corpus floor.
Continue fold-2: migrate the pointer-deref / narrow-load / pointer-array-stride
family from bespoke build+run C twins to test/lang @test, retiring each twin in
the same commit. Runtime coverage MOVES from $(TESTS) to test-lang (T1 runs +
asserts via `ww test`) + test-lang-byteid (T2 keeps cs==ww .s byte-id); the
$(TESTS) headline drops 5. Every assert is a primitive int/bool comparison with
a width-preserving (`==`) sink so a stale high half or wrong stride FAILS; the
narrow-signed rows route through an `: i32` cast against a 64B-widened literal
to force sign-extension onto the load. Each .c row maps to one inline @test fn
(50 cases total, strict superset of the C rows):
947_deref_narrow_run.c -> deref_narrow_test.ww (#116, 10 rows: *p reads pointee width not 8B MOVQ; i32/u32/u8/i8/i16/u16 + !i32-alias + enum-i8 + bool/i64 controls)
949_ptrarr_index_run.c -> ptrarr_index_test.ww (#61, 23 rows: p[i]/(&p[i])/(*p)[i] stride by size(T); {1,2,4,8}B, const+var idx, param/local/cast bases, neighbor guards, nested *[2][3], *[3]str header, siphash round())
949_dotbase_arr_run.c -> dotbase_arr_test.ww (#135, 3 rows: (*struct).arrayfield[i] read/write/compound addresses the field)
944_def_amp_idx_run.c -> def_amp_idx_test.ww (#94, 6 rows: &D[i] over a def-array; +plain/read/2D controls)
944_alias_amp_idx_run.c -> alias_amp_idx_test.ww (#5, 8 rows: &a[i] over an alias-typed base classifies off the chased type; local/global, narrow, fwd-ref, plain+str controls)
The two sibling .c (949_dotbase_addr_slice_run, 944_alias_def_addr_run) stay in
$(TESTS): the first is run-only byteid=0 (#254 cs!=ww rows), the second carries
a LOUD rule-7 reject row — both routed to fold-3 (task #7). Bump
LANGBYTEID_EXPECTED_MIN 38->43.
Continue fold-2: migrate the global-value read/store/addr-of family (non-tagged)
from bespoke build+run C twins to test/lang @test, retiring each twin in the
same commit. Runtime coverage MOVES from $(TESTS) to test-lang (T1 runs+asserts
via `ww test`) + test-lang-byteid (T2 keeps cs==ww .s byte-id); the $(TESTS)
headline drops 3 (421->418). All asserts are primitive int comparisons; fresh
globals are zeroed, so a store that misses the symbol reads back 0 and FAILS.
989_globptrfield_run.c -> glob_ptr_field_test.ww (scalar field store through a module-global *struct pointer loads the pointer from gp(SB), not saved BP; runtime gp=&backing form, static-init is #48-blocked)
989_globstructret_run.c -> glob_struct_ret_test.ww (returning a module-global struct ident by value copies g's bytes; rsz 16 + rsz 24, each field asserted)
989_dotbasehijack_run.c -> dotbase_hijack_test.ww (indexing an [N]T field of a module-global struct addresses the struct field, not an unrelated global sharing the field name; +typed-inner control +colliding-global-intact pin)
glob_ptr_field keeps only the .c's offset-0 row: an added non-zero-offset row
(gp.g=9) surfaced a cs!=ww divergence (cstage folds the offset into the store
displacement `MOVQ AX,8(BX)`; wwstage emits `ADDQ $8,BX; MOVQ AX,(BX)`) — a
latent global-*struct-pointer field-store divergence beyond the .c's coverage,
reported for the backlog, not carried here. Bump LANGBYTEID_EXPECTED_MIN 35->38.
Continue fold-2: migrate the #73 whole-struct field-copy ragged-tail test from a
bespoke build+run C twin to test/lang @test, retiring the twin in the same
commit. Runtime coverage MOVES from $(TESTS) to test-lang (T1 runs+asserts via
`ww test`) + test-lang-byteid (T2 keeps cs==ww .s byte-id); coverage is
preserved, the $(TESTS) headline drops 1 (422->421). All four rows are pure
value-rows (no reject rows): each poisons `mark` at the inner struct's natural
offset, copies, then asserts every field back with primitive int comparisons, so
an 8-byte MOVQ that over-writes the ragged-tail successor FAILS.
989_structcopytail_run.c -> structcopytail_test.ww (tail2/tail6/tail7 ragged-tail copy preserves mark; ctl8 8-aligned control)
This is the only Family-3 file with no build-must-fail rows; the three
compound-OP= twins (948_idx_compound, 949_dotfield_compound, 949_idxfield_compound)
carry loud //ww:error reject rows a runtime @test cannot replicate and are
DEFERRED to a fold-3 value-split + reject-carrier pass (they stay fully in
$(TESTS), no coverage lost).
Bump LANGBYTEID_EXPECTED_MIN 34->35 to ratchet the new corpus floor.
Continue fold-2: migrate drew's Family 2 (def-dimensioned array len/cap/slice
resolution) from bespoke build+run C twins to test/lang @test, retiring each
twin in the same commit. Runtime coverage MOVES from $(TESTS) to test-lang (T1
runs+asserts via `ww test`) + test-lang-byteid (T2 keeps cs==ww .s byte-id);
coverage is preserved, the $(TESTS) headline drops 3 (425->422). All asserts are
primitive int comparisons (no fmt/strconv in the assert path); the defcap rows
poison cap != len and assert both words so a dropped cap word FAILS.
989_defdim_field_run.c -> defdim_field_test.ww (`.len` field-read on a def-dim [MAX]T resolves from the type table: local/let-global/def cgdot arms + sum)
989_defdim_slice_run.c -> defdim_slice_test.ww (slicing a def-dim [MAX]T resolves len AND cap from the type table: default-hi + cgbasecap, local+global)
989_defdim_argslice_run.c -> defdim_argslice_test.ww (def-dim slice passed as a call arg resolves default-hi len in the N_SLICE arg-push arms; litctrl pins the N_INTLIT path)
Bump LANGBYTEID_EXPECTED_MIN 31->34 to ratchet the new corpus floor.