The wcc test drivers ran `ww build <bare-/tmp src>` with no -o, so the
compiler's <stem>.sepwork scratch landed beside the source and was never
cleaned: unbounded /tmp growth (2195 stale dirs observed) that fills tmpfs
and fabricates phantom test failures + silent harness aborts, and for
in-repo fixture builds leaked .sepwork into the tracked tree.
Each leaking build now writes its source + output inside a per-invocation
tmpdir, passes -o <tmpdir>/<stem> so the .sepwork lands inside it, and
rm -rf's the tmpdir on every exit path -- including fopen-fail and the
expected-fail reject builds (scratch is mkdir'd before the build can fail).
`ww run` and explicit-`-o`/byte-id helpers are left as-is; the 990/993
byte-id comparison logic is byte-for-byte unchanged.
Two items filed separately (this commit holds the no-Makefile / no-main.c
rail):
- #13: a stale <src>.s byte-id readback (749) silently no-ops since
separate-compile emits .s to <ostem>.sepwork/__root.s; documented inline.
- #14: build-system Makefile recipes build selfhost/cmd/*/main.ww with no
-o and leak main.sepwork in-tree (bounded, gitignored; own commit).
One concern -- sepwork leak hygiene -- across 228 drivers; uniform
transform applied per-file and two-round reviewed. make test: all 402
passed, zero net-new /tmp scratch, zero test-driven in-repo .sepwork.
cmd/wcc/check.c silently accepted `let a; let a;` in the same block
and similar redecls. Pre-#27 the localoff dedup masked it; post-#27
last-write-wins via head-first localfind. Surfaced by worker-27
during the #27 review.
Cstage: 5 guard sites (check_scope_define-NULL → err) covering
N_LET block-bind, N_MLET tuple binders (incl. same-tuple
`let (a,a)`), N_FORRANGE tuple binders, top-level let, fn param.
Voice: "<kind> '<name>' redeclared in same scope" for inner;
"duplicate let %s" for top-let, matching the existing
"duplicate <kind>" idiom at 1812/1851/1872.
Wwstage: TODO(#11) comments at the 4 mirror sites (installdecl,
N_FORRANGE, N_LET, installparams). Full enforcement waits on the
checkfile pass per rob.
**Unmasked by #32 (worth flagging):** selfhost/cmd/wcc/cgenexpr.ww
cgcall had `let callee: *node = n.lhs;` twice at fn-body scope
(copy-paste, identical value). Pre-fix silent-redecl absorbed it;
post-fix the new guard rejects. Removed the second decl — outer
`callee` stays visible across the intermediate block.
Test 712 (redecl): 10 rows (6 neg + 4 pos), cstage-only per rob.
Negative rows cover all 5 guard sites + same-tuple-dup. Positive
rows pin the legal counter-shapes (cross-block, name-only bucket,
forrange body, mcase-per-arm).
Test 300 row 34 ("shadowing in inner scope; same scope flagged")
was incorrectly asserting the bug; flipped to expect "redeclared"
and added a sibling row pinning cross-block shadow stays ok. Test
709's `same_block_redecl_pin` canary (explicitly documented as
flipping under #32) removed; pointer to 712 left in its place.
localoff (cstage) / localadd (wwstage) deduped stack slots by name
alone, ignoring scope. Outer `let a: [128]u8` and an inner-block
`let a: *u8` shared one 8B slot; prologue truncated to inner size
and outer-scope writes past saved RIP corrupted the frame. Worker-19
hit it during #19 (selfhost/cmd/w6a/main.ww carries a defensive
asm→s rename pointing at this task).
Drop the name-dedup. Each let allocates fresh. Then preserve
outer-scope visibility across inner blocks: cgstmt's N_BLOCK case
saves `*locals` head, walks body, restores. cgfn iterates fn->body
->list directly (bypassing the outermost N_BLOCK) so defers and the
implicit-return epilogue still see fn-body locals after the loop.
Wwstage symmetric: localadd keeps dedup only for `@`-prefixed
synthetic scratches (`@tagscr` / `@retscr` / `@tagbase`) which need
single-slot semantics; user names get fresh stubs. scanlocals always
counts + always appends a fresh stub for N_LET / N_MLET / N_FORRANGE
so prologue SUBQ stays in sync with emit-time offsets. cgblock and
cgfn mirror cstage.
ww2 == ww3 == ww4 byte-identical post-fix.
Test 709 (localoff_scope): 8 rows × 2 drivers = 16 fixtures —
inner_first_outer_bigger, outer_first_inner_writes, nested_3_deep,
same_name_diff_type, same_block_redecl_pin, defer_shadow,
forrange_body_shadow, if_body_shadow. defer_shadow pins the cgfn
body-bypass; if_body_shadow pins the save/restore independently.
Asm byte-id not diffed in 709 — 995_self_rebuild covers cross-stage
drift more broadly.
Follow-ups (filed): #32 (check: refuse same-block let-redecl), w6a
`s`→`asm` revert sibling commit.