cgen.c's two ≤24B / tagged-widen return scratch allocations called
mklabel(c, "retscr"), bumping labelseq once per function with a
struct or tagged return. Wwstage's mirror uses the fixed `@retscr`
name through `c.retscroff` SSoT (post-#14 b401cce) and never
touches labelseq for the scratch alloc. Result: cstage's labelseq
runs 1 ahead of wwstage in every fn with a struct/tagged return,
so every subsequent ct_N / ce_N / end_N branch label diverged by
the same offset.
Class A byte-id drift, previously latent. Filed STATUS-3 #15 —
promoted to bootstrap-blocking once lib/strings's time.add-chain
and nested-if shapes compounded the cumulative skew past the
993/995 byte-id threshold. The label name was never emitted (it's
a hidden local-table key); only the labelseq side-effect mattered.
Polarity catalog: cstage OVER — extra mklabel per fn. Convergence
cstage → wwstage's fixed-name SSoT per rule 10 (wwstage's pattern
is the cleanup target; STATUS-3 #14 already enforced single-slot
@retscr on both stages, this commit aligns the *name source* too).
Site 2 (struct/tagged @retscr in cgreturn) is the actively-tripping
site that reproduces in the in-tree corpus (lib/strings's time.add
chain). Site 1 is preventive symmetry per rule 10 — its sentinel
flip is masked by pre-existing pre-existing #20/#21 struct-widen
offset latents, documented inline.
Filed follow-up (NOT in scope here): #26 graduate the other
hidden-name mklabel sites (tagscr, tagbase, argscr, idxscr) to
@-prefix SSoT. Same family but per-site allocations, structurally
bigger; belongs with STATUS-4 task #1 variant-widen consolidation
refactor.
Tests:
- 725_nested_if_labels pins cstage vs wwstage cmp -s byte-id on
a canonical struct-return row mirroring lib/time.add shape
(multi-arm if + struct {sec,nsec: i64} return). This row
actually exercises mklabel site 2; the prior worker draft used
a scalar-widen path that bypassed both mklabel sites.
97/97 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
Wwstage's cglet skipped MOVQ $0 for sz=8 slots that cstage
zero-inits unconditionally — !void error types (utf8.invalid)
and void-alias variant tags (utf8.done / utf8.more) drifted
byte-id post-utf8 + lib/strings; promotes STATUS-3 #22 from
latent to bootstrap-blocking. Cstage emits MOVQ $0, -K(BP) in
the prologue for any sz=8 let-decl slot via the natural
type-fallthrough; wwstage's `typeis8byteprimitive` helper
returned false on N_TBANG and on N_TNAME pointing to an alias
that resolves to void, so the gate never fired and the slot
stayed uninitialised.
Polarity catalog: wwstage UNDER — `typeis8byteprimitive`
classifier too narrow at N_TBANG and void-alias N_TNAME.
Convergence wwstage → cstage's natural sz=8 fallthrough (rule
10). N_TBANG arm recurses on inner type (cmd/wcc/check.c:290
resolve_type copies T's kind, only sets iserror — so !T is
8B iff T is 8B); void-alias N_TNAME resolves through alias-
recursion the same way.
Tests:
- 724_letdecl_zeroinit pins MOVQ $0, -K(BP) presence between
function prologue and body on canonical !void and void-
alias rows, plus cmp -s byte-id between stages per row.
Filed follow-up (NOT in scope here): #25 wwstage 8B struct
without rhs still under-emits (structlookup != nil short-
circuits the classifier). Same family as STATUS-4 #36 primsize
composite-aware sizing. No in-tree consumer.
96/96 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
Wwstage call-arg-emit recognized slice args only when source was
IDENT/SLICE/CAST/DOT. For N_CALL returning []T the natural-push
fallthrough emitted one PUSHQ AX (lost .len/.cap) and cgcall's
pop-count under-drained by 2 words — corrupting R8/R9 and every
subsequent arg. Class A runtime miscompile with stack misalignment
and 3-POPs-of-garbage at the receiving call. Sister to #21
(tagged-CALL arg) but for plain []u8 slice, not tagged-variant —
wwstage's pushargsrev grew the tagged-CALL arm at #21 and never
grew the plain-composite arm.
Surfaced by lib/strings landing's `bytes.X(toutf8(in), p)` call
sites: 995_self_rebuild's wwstage rebuild tripped on byte-id
divergence at w6c_ww + wwdump_ww emit. 967_bytes_run was green
because `ww run` exercises the cstage path. Corpus-coverage-blind
on the wwstage side until lib/strings pulled the chain through
wwstage compilation.
Fix is minimal: `nodeisslice` (selfhost/cmd/wcc/cgenutil.ww) gains
an N_CALL arm structurally identical to the existing N_CALL arm in
`nodeisstr` (only swap: isslicetype for isstrtype). The downstream
natural-push slice path (PUSHQ CX/BX/AX, extra=2 pop-count) was
already correct — it just needed the N_CALL-of-slice-return shape
to be recognized as a slice. pushargsrev and cgcall untouched.
Polarity catalog: wwstage UNDER — missing N_CALL arm in slice
shape recognition. Convergence wwstage → cstage per rule 10
(cstage reads typed-AST `type_isslice` natively).
Tests:
- 723_composite_call_arg pins the 3-PUSH order (CX, BX, AX)
between `CALL view` and next CALL on canonical `f(g())` shape,
plus cstage vs wwstage cmp -s byte-id.
- 927_composite_call_arg_run runtime-pins 7 rows × 2 stages =
14 fixtures: canonical, slice-CALL + let-slice (hasprefix
shape), two composite-CALL args (arg-shift collision),
middle-argpos, nested composite-in-composite, slice + scalar
pop-count mix, tagged-CALL regression alongside (confirms
#21 still holds).
95/95 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
Class A wwstage cgen miscompile, silent until wwstage path engaged.
Pre-fix wwstage's name-keyed flatvariantidx returned -1 for `[]T`
variants (pat.str empty on N_TSLICE), so cgmatch and
cgtagvariantidx collapsed every `(scalar | []T)` arm to tag 0.
Internally consistent within wwstage; cstage's structural
`type_eq` (cmd/w6c/cgen.c:466 cg_tag_for_variant) matched
correctly. Bootstrap stayed green because no selfhost-corpus path
exercises `(scalar | []T)` until lib/bytes / lib/strings landing
pulls bytes.index through wwstage compilation — 967_bytes_run
uses `ww run` (cstage only), so the wwstage path was never
exercised.
Polarity catalog entry: wwstage UNDER (missing N_TSLICE dispatch
arm in variantindex lookup), not REVERSE — worker's deeper read
corrected rob's initial diagnosis. cstage's structural type-eq is
the leaner-correct side; wwstage converges to it per rule 10.
Fix: new `flatslicevariantidx` helper in cgenutil.ww keyed on
N_TSLICE shape walking pat.lhs against vt.lhs alongside the existing
name-keyed flatvariantidx; extend `taggedvariantindex` shape-fallback
with a `wantslice == ivisslice` axis alongside the existing str
axis; route N_TSLICE in cgenexpr.ww's cgtagvariantidx (is/as)
and cgmatch (case) through the helper. No edits to cgenmatch's
dispatch codegen (CMPQ/JNE/spill) — that's symptom, the bug is
in the variantindex lookup.
Surfaced the 7th corpus-coverage-blind unmask of session 5 (sister
shape to STATUS-4 #11 / #14 / #21 wwstage UNDER family). Latent
within lib/bytes (a6abac2) since landing today; 967_bytes_run's
cstage-only `ww run` driver kept it dormant.
Tests:
- 722_match_slice_variant pins cmp -s byte-id between stages
for the canonical (u8|[]u8), reverse-order ([]u8|u8), and
three-arm (u8|[]u8|str) shapes.
- 926_match_slice_variant_run runtime-pins 7 rows × 2 stages
(cstage + wwstage drivers): canonical, reverse-order, and
other scalar-vs-slice-of-same-primitive matrices (i8|[]i8,
i32|[]i32, u64|[]u64, rune|[]rune), three-arm with str.
Verifies both arms reachable and payload survives.
Filed follow-ups (latent, NOT in this commit's scope):
- flatslicevariantidx falls back to first slice slot when no
element-name matches; `([]u8 | []i32)` would mis-route. No
in-tree consumer.
- 926 missing nested ((u8|[]u8) | i32) row per rob's spec.
- 3-arm 32B tagged sequential-push payload corruption (both
stages, asm byte-id passes, only 9xx runtime catches).
- Chained inline pick() over 32B 3-arm slot (both stages,
bind-to-let workaround documented at 926 row).
93/93 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
Hoehrmann DFA from ref/hare/encoding/utf8/decodetable.ha flattened
to 1D [2048]i8 (task #20: 2D-array jagged cgen still pending);
encoderune takes a caller buffer matching lib/encoding/hex.encode;
done/more/invalid all spelled as plain void aliases per lib/io's
eof precedent. Surface ports decoder + decode + next + encoderune
+ runesz + utf8sz + validate from ref/hare/encoding/utf8/{types,
decode,encode,rune}.ha. next() polarity rewritten from Hare's
`(state-1):uint >> 31` to an explicit `if state == 0` branch
because ww's uint is 64-bit (cmd/wcc/type.c:58); same effect, no
hidden 32-bit assumption.
Deferred (no in-tree callers): prev, slice, position, remaining,
appendrune, strencode, strdecode. String iteration (chars/
newchars/nextchar in the session-4 draft) dropped per Hare
discipline — belongs in lib/strings::iterator, not encoding/utf8.
Tests:
- 968_utf8_run drives lib/encoding/utf8/utf8test.ww via ww run.
21 @test fns: boundaries (ASCII, 2-byte, 3-byte, 4-byte
encode/decode), surrogate/overlong/out-of-range/bad-continuation
reject, max-in-range (U+10FFFF) accept, truncated→more, done@EOI,
validate empty/mixed/malformed, encode/decode roundtrip. Two
rows ported from ref/hare/encoding/utf8/decode.ha @test that
were missing in the session-4 draft: bad-continuation
[0xC2,0xFF]→invalid and max-in-range [0xF4,0x8F,0xBF,0xBF]→
U+10FFFF.
- 9xx stdlib runtime slot range extended from 970-989 to 960-989
to accommodate utf8 at 968 (970-989 block was full).
90/90 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
Class B shared miscompile pre-fix: cstage skipped the CALL emit at the
receive site (frame collapsed, exit 11); wwstage emitted CALL but
truncated 32B return to AX only (slice payload garbage, segfault on
g.b[0]). Both stages now lower plain TY_STRUCT > 24B through the SysV
sret discipline: caller pre-allocates dest, passes &dest in RDI as a
hidden first-arg (user args shift to SI/DX/CX/R8/R9/+stack), callee
saves RDI to @sretarg at the prologue and writes through it, returns
RDI in RAX. Surfaced by lib/encoding/utf8 pre-flight when the
Hoehrmann decoder (32B) hit 698_cgreturn_struct.c's OUT-OF-SCOPE
marker.
Scope: plain TY_STRUCT > 24B only — tagged unions, tuples, str, slice
keep their existing register-return ABIs. `return f()` forwarding
from a sret callee is fail-loud-not-wired (compile-time error in
both stages, follow-up filed); the workaround `let r = f(); return
r;` is wired and byte-identical. Discard-context calls (`f();` of an
sret-returning function) share a per-fn single-slot @sretscr;
consecutive discards reuse the same slot.
698_cgreturn_struct.c's OUT-OF-SCOPE marker retired in the same
commit; three positive rows (32B quad, 32B decoder, 40B five) now
assert the sret discipline across both stages via byte-id diff.
Tests:
- 721_sret_struct_return pins three asm-presence sentinels per
row: (a) LEAQ -K(BP), DI immediately before CALL at the receive
site, (b) MOVQ -K(BP), AX before RET in the callee (sret return-
the-pointer), (c) negative-assert no MOVQ AX, -K(BP) capture for
return type >8B. Three rows × both stages × cmp -s byte-id.
- 925_sret_struct_return_run runtime-pins 7 rows × 2 stages
including the collision row (25B+ struct BOTH returned AND passed
by-value as arg — catches arg-shift, sister site to #11), nested
struct payload, slice payload, reassign-receive, N_IDENT return
rhs.
89/89 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
Wwstage call-arg-emit recognized tagged args only when the source
was an IDENT (already-materialized var). For N_CALL returning a
tagged-union, the natural-push path mis-routed: AX (tag) pushed
twice, AX clobbered with widentag(=0) between pushes, DX (payload)
dropped entirely. After POP, DI ← 0, SI ← tag — both reversed and
the payload word lost. Class A runtime miscompile, masked by zero
in-tree call sites of the shape until lib/encoding/utf8's iterator
API surfaced it via pre-flight A probe.
Fix aligns wwstage DOWN to cstage (rule 10). cgenutil.ww:pushargsrev
aistagged guard now fires for N_CALL whose callee returns a tagged
whose slot matches the param's tagged slot (mirrors cmd/w6c/cgen.c:
4216-4221's type_eq guard), and the natural-push fallthrough adds a
tagged-CALL arm pushing R8/CX/DX/AX high→low by slot size (mirrors
cmd/w6c/cgen.c:4373-4387). cgenexpr.ww:cgcall's per-arg pop-count
picks up the same taggedcallslot helper so the next arg's POPQ
doesn't land on residual tag/payload words.
Sister-family to #11/#14 in the variant-widen ABI chain — call-site/
caller-side surface, distinct from callee-side #11 (param decompose)
and scratch-side #14 (return slot). Fifth corpus-coverage-blind
unmask this session (catalog: i64 div/mod CQO #16; wwstage IDENT-
local /= no-op #16-B2; cstage signed-DATA module-scope #19; wwstage
silent-zero arrays #19 mirror; #21 call-arg DX drop).
Test: 720_tagged_call_arg asm-presence row (PUSHQ DX appears
between CALL and next CALL, before PUSHQ AX) + 924_tagged_call_arg_
run 9xx semantic row (5 rows: 4-variant CALL-source, 4-variant
IDENT-source regression guard, 2-variant ptr/err, multi-arg tagged
+ scalar). Bootstrap byte-id (ww2 == ww3 == ww4) holds.
emit_lets / emitletdataw's scalar-8B and array arms only matched bare
N_INTLIT / N_RUNELIT / N_TRUE / N_FALSE / N_NIL on the let rhs. `let
x: i8 = -1i8;` arrives as N_UN(TK_MINUS, N_INTLIT(1)) — none of those
— so cstage's scalar arm hit `else continue;` and dropped the DATAW
row entirely; the array arm bailed at the first non-foldable element
and the skip-array-with-non-NIL-rhs fall-through dropped the whole
row. Wwstage's mirror arms silently emitted zero bytes for negative
literals in both shapes.
Severity split — cstage symptom is no DATA emit, the linker fails
loudly at build time. Wwstage symptom is silent-zero element
substitution for negative array values: compiles, runs, returns
wrong answers. Corpus-coverage-blind on the wwstage side, only
surfaces when a consumer reads the wrong value. Single N_UN-fold
helper application retires both symptoms across both stages. Fourth
corpus-coverage-blind unmask this session (catalog: i64 div/mod CQO
#16, IDENT-local /= no-op, #21 call-arg DX drop, now #19 wwstage
silent-zero).
Route all four sites through fold_int_literal / foldintliteral, the
same helper #24 used on the def-emit side (which already covered
N_UN over the leaf set). The wwstage array arm also picks up an
N_CAST peel and drops a dead non-`...` N_FIELD branch (the parser
never emits non-`...` N_FIELD inside an N_ARRLIT — only as the `...`
repeat marker). Same-path sibling cleanup; rule-11 justified.
Tests:
- 719_signed_data_emit asserts DATAW <sym>(SB),"<bytes>" lines are
present in both stages' .s for the {i8, i16, i32, i64} × {scalar,
1D array} matrix, plus cmp -s byte-id between stages per row.
Corpus-coverage-blind sentinel per rob's STATUS-3 note.
- 923_signed_data_emit_run runtime-pins the same matrix plus a
TK_TILDE row through cstage and wwstage drivers.
995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
Removes the local os.timespec (sec, nsec) struct in favour of
time.instant from lib/time. lib/os now `use time;`. filestat's
atime/mtime/ctime change type with byte-identical layout
(i64+i64=16B both sides), so .sec / .nsec accessors at all caller
sites work unchanged.
Rule 12: simple data + mirror Hare. Two same-layout types — one
Hare-canonical, one not — is exactly the structural divergence the
rule forbids. Single-source-of-truth; no transitional alias.
Citations: ref/hare/fs/types.ha:141 (Hare's fs::filestat carries
time::instant), ref/hare/time/instant.ha:9 (canonical layout).
Caller impact (sole reader): lib/os/stattest.ww (.sec / .nsec
unchanged; one comment line refreshed). examples/cmatrix migrated
already in 7e9bede. No selfhost/cmd/* reads mtime/atime/ctime.
Test wiring: lib/os/os.ww removed from 900_stdlib.c's standalone-
w6c-codegen list (cross-module type ref now needs the driver's
module concatenation, same reason lib/bufio and lib/fmt graduated
off earlier). Coverage stays at 976_stat_run via stattest.ww.
Makefile dep edges for the five wwstage targets gain
lib/time/time.ww so changes to it trigger wwstage rebuild.
lib/time is now in the toolchain transitive chain via lib/os. No
selfhost cmd calls time.add/time.diff today; bootstrap is safe.
Latent risk: any future selfhost edit adding time.add/time.diff
would surface task #15 (nested-if label-counter skew in lib/time/
add) as a bootstrap regression. File a fix-#15 before such an edit.
Bootstrap byte-id: ww2 == ww3 == ww4 for all five wwstage tools.
Replaces the lib/time placeholder (a monotonic(*timespec) shim that
predated lib/os's syscall surface). Ships Hare's time module first
cut per ref/hare/time/{duration,instant,arithm,+linux/functions}.ha:
- duration (i64 ns); nanosecond / microsecond / millisecond / second
constants.
- instant (sec, nsec) — Hare's layout, NOT POSIX's nsec:u32. Matches
Linux struct timespec on 64-bit, so &instant lands directly in
clock_gettime.
- clock enum: realtime + monotonic only. The rest of Hare's set
(process_cpu / thread_cpu / boot / realtime_alarm / boot_alarm / tai)
graduates when a caller actually needs it (rule 9).
- now(c: clock) instant — aborts on EINVAL/EFAULT (mirrors Hare's
abort-on-impossible-errno). Deliberately NOT (instant | oserror) to
sidestep task #9's 1-word-payload tagged-return trap.
- add / diff / compare on instants per ref/hare/time/arithm.ha
verbatim.
Deferred to follow-up commits when a caller surfaces: sleep, format /
strftime, time.chrono / time.date calendar, timezone, time.error
sum-return shape, time.unix helpers, time.mult, conversion helpers.
Tests at lib/time/timetest.ww (15 rows, table-pattern, semantic per
Class B doctrine). Driver test/wcc/977_time_run.c slotted between
976_stat_run and 978_intdiv_signed in the 9xx _run band.
examples/cmatrix migrates to the new API in the same commit (sole
pre-existing caller — bisect-clean per rule 11).
Class B bug #16 (sign-extend before IDIVQ) was surfaced by the
negative-duration rows during this work; landed 63332fe..4fa4bcf
before this commit. Rows 8-9 (addneg_noborrow / addneg_borrow) are
the load-bearing Class B exercisers; would have stayed silently wrong
pre-#16.
lib/time's own .s output has a cstage/wwstage label-counter skew in
add (cstage emits add_ct_7/_ce_8/_end_6 vs wwstage _6/_7/_5). Filed
as task #15; non-bootstrap-blocking since lib/time is outside the
selfhost toolchain transitive chain.
Shared miscompile in both stages — not a divergence. Bootstrap byte-id
passed throughout because both stages emitted the same wrong asm. Both
the C cgen (cmd/w6c/cgen.c TK_SLASH/TK_PERCENT) and the ww cgen
(selfhost/cmd/wcc/cgenexpr.ww) prepped IDIVQ with `MOVQ $0, DX`, which
is the unsigned 128-bit dividend shape. For a negative RAX, the CPU
then divides 2^64 + (-RAX) by the divisor — unsigned wraparound, not
signed division. Surfaced via lib/time/add() needing the verbatim Hare
signed-%-normalisation in ref/hare/time/arithm.ha.
Fix: emit CQO (sign-extend RAX into RDX:RAX, REX.W 99) on the signed
arm; keep MOVQ $0, DX on the unsigned arm where the DIVQ-vs-IDIVQ
dispatch was already correct. Since both stages always emit 64-bit
IDIVQ regardless of source width, a single CQO suffices for
i64/i32/i16/i8 — the dividend already lives in RAX sign-extended. No
CDQ/CWTL/CBTW needed.
Symmetric stages (rule 10): both stages were broken identically; both
get the same surgical fix. Adds A_CQO to each assembler's opcode set:
cstage in cmd/w6c/6.out.h + cmd/w6c/txt.c + cmd/w6a/{parse,asm}.c;
wwstage in selfhost/cmd/w6a/{types,parse,asm}.ww.
Class B (shared miscompile) — new in the session's polarity catalog.
Bootstrap byte-id is useless for catching it; semantic 9xx runtime
tests are the right shape. test/wcc/978_intdiv_signed.c covers 27 rows
× 2 drivers = 54 fixtures across {i8,i16,i32,i64,u8,u16,u32,u64} ×
{/, %} with width-boundary minima (INT8_MIN, INT16_MIN, INT32_MIN,
INT64_MIN/2) and high-bit-set unsigned anchors. INT64_MIN is spelled
(-INT64_MAX) - 1 per task #17 (wwstage NEGQ-over-imm drops digits on
-9223372036854775808i64); that literal-cgen bug is unrelated to this
fix.
Two known compound-assign workarounds at cmd/w6c/cgen.c:3765
(TK_SLASHEQ IDENT-local) and :3549 (TK_SLASHEQ/TK_PERCENTEQ
deref-compound) remain in tree; both depend on the assembler having
CQO, so they revert in a follow-up commit citing this one.
wwstage's $64 frame was 24B below required — the second struct-return's
@retscr write at -88(BP) landed below SP. Silent miscompile masked by
bootstrap-window luck. The fix retires the stomp by enforcing single-slot
@retscr at emit-time.
cstage was per-site-fresh (wasteful but safe, frame $96); aligned UP to
single-slot for ABI consistency with wwstage's @-prefix convention, not
for correctness. Both stages now produce $64 frame; second return reuses
the first's -64..-48(BP) slot.
Generalizes #38's c.tagscrsz SSoT pattern to c.retscroff (wwstage) and
cg_retscr (cstage). Returns are terminal — only one fires per call, so
the two slots' lifetimes never overlap; single-slot is structurally
correct. wwstage's emit-side dedup was incomplete post-#27 (cgblock
save/restore unwinds the @-prefix stub); the @retscr fast path in
localadd bypasses the c.locals walk.
Test 718: 4 rows × {cstage runtime, wwstage runtime, byte-id, stomp
sentinel}. Stomp sentinel scans .s for any -N(BP) where N>64 and fails
the row if found — catches below-SP writes that bootstrap byte-id would
miss in a lucky window. Row 2 (3-return) byte-id disabled per task #15
(pre-existing label-counter skew, unrelated to #14).
Polarity catalog this session:
- #9 wwstage OVER (tagged-return slot)
- #11 wwstage UNDER (struct-by-value param decompose)
- #14 wwstage UNDER (struct multi-return @retscr — silent stomp)
Initializing a tagged-union variant slot with a runtime f64 source
(let, cast, fn call, unary, struct field, etc.) stored the i64 bit
pattern in the payload, not the float bit pattern. cgexpr leaves f64
in X0; the existing scalar-fallback MOVQ-from-AX wrote whatever was
last in AX (typically pre-conversion integer or stale residue).
Worker-fmtfloat surfaced this during #17 pre-flight (probe at
.ai/probe_f64_union_widen.ww). Blocks #17 fmt.float dispatch arm.
TK_FLOAT literals were coincidentally correct because the lowering
loads bits into AX before passing through X0 — the literal_1_0 test
row pins that as the principled MOVSD path now.
cstage cg_widen_tagged_store: add fld_isfloat arm between the slice
and scalar fallbacks. Emit MOVSD (f64) / MOVSS (f32) from X0 to the
payload offset, then the tag MOVQ. Mirrors existing str/slice/
structlit field-flow dispatchers.
Wwstage cgwidentaggedstorebp: mirror via exprfloatkind. Resolves a
secondary gap by looking up the variant tag directly via
flatvariantidx(c, dt, "f64"/"f32") — rhstargetname has no N_FLOATLIT
/ N_CALL / N_DOT branch and would fall through to str-fallback
returning tag 0.
No in-tree consumer triggered this pre-fix (no f64 in any tagged
union yet) — hence latent silence. arr[i]= and append() have the
same class gap but no in-tree exerciser today; same shape if/when
[N]f64 / []f64 land.
Test 715 (tagged_widen_f64): 7 rows × 2 stages = 14 fixtures with
bit-pinning via *u8 punning. literal_1_0 (regression lock-in),
cast_1_f64, call_makeone, unary_neg_f64, ident_f64, field_f64
(rob's extra row), i64_rhs_still_integer (negative control).
Diagnosable 0/1/2 return codes distinguish pass / wrong-tag /
wrong-payload.
ww2 == ww3 == ww4 byte-identical post-fix.
Closes STATUS latent #1: @tagscr shared 24B reservation across the
four tagged-scratch sites (cgreturn, pushargsrev, cgindex
tagged-elem, pointer-rooted struct-field tagged write). Any fn that
needed >24B (e.g. slice-in-tagged-field 32B) silently overflowed
into the neighbor frame slot. Surfaced concretely as getopttest's
errortable wwstage exit 16 after #37 fixed the upstream gaps.
c.tagscrsz: i32 on the cgen struct is the single source of truth.
tagscrbump(c, need) in scanlocals raises the max across all 4
reservation sites and returns the frame delta. All emit sites
(cgreturn / pushargsrev / cgindex / cgwidentaggedstore pointer-
rooted) read c.tagscrsz instead of hardcoded 24. Mirrors the existing
cgwidentaggedstore precedent; @tagbase keeps its 8B scanseenmark
dedup (always 8B, correct).
Unmasked latent bug (now fixed): scanlocals's pointer-rooted struct-
field tagged-write detection uses localfindnode(c, base.str) to
resolve the *struct base. For `fn fill(h: *holder)`, h's scan-time
stub from scanseenmark had tnode=nil, so the @tagscr reservation
never fired. Pre-#38 the hardcoded 24B masked this; #38's correctly-
sized slot exposed it. cgfn's param scan loop now sets
c.locals.tnode = scanp.lhs after scanseenmark so localfindnode
resolves param types at scan time.
Test 714 (tagged_return_scratch): 4 rows × 2 stages = 8 fixtures.
Direct adjacency repro; match-arm field-by-field read; **mixed-
sizes-one-fn** (16B pushargsrev widen + 32B cgreturn widen in the
same body — pins the lockstep invariant that a sibling site can't
undersize the shared slot); call-site struct-payload widen. Row 3
specifically would regress if a future refactor ever forgets to
route an emit site through c.tagscrsz.
982 getopt_run green through both stages (was the original surface);
995 self_rebuild byte-id holds.
The original #37 symptom (worker-34's `..findflag` mangle) cannot
reproduce on master — was a runtime miscompile misattributed to a
link-time issue. Investigation surfaced three real wwstage cgen
gaps in the N_INDEX-through-struct-field family, sister bugs to
#34 (N_INDEX N_IDENT-base) and #36 (primsize-default-to-8).
1. `indexbaseesz` slice-element stride defaulted to 8 for named-
struct elements. `&opts.ptr[i]` for `opts: *[]option` computed
MOVQ $8 instead of $24. Fix: route slice case through
`elemsizeofc(c, innert)`; ptr-to-named-struct via structlookup.
2. `cgun TK_AMP N_INDEX` ignored N_DOT base. `&p.ptr[i]` left
esz=8 because only N_IDENT base was handled. Mirror cgindex's
existing N_DOT arm.
3. `nodeisstr` N_INDEX arm only walked N_IDENT bases. `cmd.argsptr[i]`
for `argsptr: *str` returned false; pushargsrev dropped the
.len half at call sites. Add N_DOT-base arm that walks the
struct field's pointee.
Test 713 (struct_field_index): 3 rows × 2 stages = 6 fixtures, one
per fix shape. Runtime-only; bootstrap byte-id (995_self_rebuild)
covers cross-stage drift.
Residual: getopttest's errortable still fails through wwstage with
a slice-of-str-via-&arr[expr] miscompile. Filed as task #38.
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.
[N]str array literals wrote only the .ptr half of each element.
cstage used esz=16 from `lu->sub->size` and a single per-element
MOVQ → .len trailed uninitialized stack residue. Wwstage was worse:
primsize("str")=0 fell through to esz=8, so element i+1's ptr-MOVQ
clobbered element i's .len slot, scrambling everything.
Worker-18 sidestepped during #18 by rewriting array primer rows to
[N]i64.
cstage cgen.c N_ARRLIT TY_STR branch: emit AX → base+i*16 then
BX → base+i*16+8. Repeat-`...` path mirrored. type_isstr handles
TY_UNTYPED_STR + TY_NAMED-aliased-str.
Wwstage cgenstmt.ww: isstrel flag conditionally drives the two-MOVQ
store in both the per-element walk and the repeat fill. The dispatch
loop was refactored to unify FIELD/ellipsis branches via isellip,
cleaning up the duplicated arms.
Wwstage cgenutil.ww slotsize/letslotsize: TNAME-"str" element gets
esz=16, replacing the primsize=0 → 8B fallback. Without this the
frame collapsed to 24B for [3]str.
Slice (24B), struct, tuple, tagged element arrays have the same root
cause but distinct width/layout concerns — deferred to #35 per rob.
Test 711 (arrlit_str_full): 7 rows × 2 stages = 14 fixtures —
str_lens_3el, str_ptrs_3el, str_repeat_5el (TK_ELLIPSIS), bool_3el,
rune_3el, i32_3el, i64_3el. Rune relies on the pre-existing esz==4
→ MOVL path (incidental correctness); sibling slot types pinned as
regression nets.
Followups filed: #34 (wwstage cgindex truncate on [N]str bare-let
read side, surfaced by this fix), #35 (composite element types),
#36 (primsize-returns-0-default-to-8 cleanup).
cstage's N_CAST narrow-clamp emitted `MOVL AX, AX` on u32 → enum-u32
casts. wwstage's cgcast walker steps through N_TBANG / N_TNAME alias
links only; an enum's aliaslookup returns the N_TENUM body, which
breaks the loop and skips the clamp. The asymmetry surfaced during
#10 (lib/os/stat) when kstat.mode typed as u32 tripped the cross-
stage diff; worker-stat sidestepped by typing kstat.mode as `mode`.
Single-site gate: skip the narrow-clamp when the destination type is
TY_ENUM. Mirrors wwstage's N_TENUM lacuna exactly. Predicate
recursion (type_isint, type_isunsigned) over TY_ENUM stays intact —
this is emit-side only.
Wwstage unchanged.
Test 710 (cast_enum_movl): 5 rows × {exit-code per driver, asm
byte-id when w6c_ww built} = 10+ assertions. u32→enum-u32 headline,
enum-u32→u32 reverse (pins direction-of-asymmetry), u32→enum-u8
different-width, i64→enum-i32 signed-narrow, struct-field rt mirror
of `out.mode = k.mode` (the #10 trip-wire).
A principled identity-width identity-sign skip across both stages is
filed as #33. The lib/os.ww kstat.mode workaround revert is a
sibling cleanup, not in #25 scope.
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.
When `use fmt;` is in scope and a local/param named `fmt` shadows it,
`fmt.X` in the body silently resolved to the str-typed value sym and
emitted `CALL AX` through str.ptr → runtime crash. Surfaced during
#15 (lib/log's printfln family); worked around by renaming the param
`fmt`→`format`.
Per rob + user, option (C): "value names and module names are
disjoint." Refuse the shadow at the decl site. Single rule, no
non-local reasoning, no silent footgun if a future lib/X exports a
new leaf.
cstage: src_imports walks file->list for N_USE entries (skipping
self-imports where u->module == u->str — same-module fixtures like
lib/fmt/fmttest.ww carry these); check_module_shadow runs before
each SK_PARAM / SK_VAR scope_define (param, clet, mlet, forrange
single + tuple, mcase). Wwstage mirror in check.ww; wwdump-only
diagnostic today, full enforcement waits on #11 checkfile pass.
Bootstrap byte-id holds — no codegen change. One source patch in
selfhost/cmd/w6a/main.ww renames an outer `let asm: asm_;` to `s` to
sidestep task #27 (cstage localoff scope-blind dedup); unrelated to
#19 but the new rule's first run flagged it as a self-shadow.
Test 708 (param_shadow_mod): 4 rows — neg_param (param shadow errs
at fn decl line), neg_let (let shadow errs at let decl), pos_rename
(rename compiles + runs), pos_selfimp (in-module use is skipped).
4 wired sites without dedicated rows deferred to task #28.
Follow-up: lib/log can revert format→fmt now that the silent
crash is impossible.
Top-level `def NEG: i32 = -100;` skipped DATA emission in both stages
— cstage's emit_defs and wwstage's emitdefconstants each carried a
literal-leaf whitelist that excluded N_UN nodes. Same gap in
check.c's eval_enum_value cstage-side. Surfaced during #10 (lib/os
forced an `at` enum for AT_FDCWD=-100 etc. as workaround).
Factor a single fold_int_literal helper (cstage check.c; wwstage
cgen.ww). Handles N_INTLIT / N_RUNELIT / N_TRUE / N_FALSE / N_NIL
plus N_UN with TK_MINUS / TK_TILDE / TK_PLUS recursively. Consume
from eval_enum_value, emit_defs, emitdefconstants, enumevalmember —
single source of truth for "is this a literal-leaf foldable".
Side effect: cstage's def-emit set widens from {INTLIT, RUNELIT,
TRUE} to match wwstage's pre-existing 5-shape set plus the new
unary peel. Bootstrap byte-id holds (995_self_rebuild green).
Test 631 (def_neg_global): 6 rows × cstage/wwstage run + asm
byte-identity diff. Covers all three unary arms (-, ~, +), positive
regression-pin, i32 + i64 + u32 slots.
Follows up #26: revert lib/os.ww `at` enum to three top-level defs.
Hare-shaped filestat introspection. New types: filestat (80B,
mirrors fs::filestat ref/hare/fs/types.ha:141), mode (31-member
enum mirroring fs::mode ref/hare/fs/types.ha:63), stat_mask (7 bits
mirroring fs::stat_mask ref/hare/fs/types.ha:129), timespec (i64+i64,
layout-compatible with future lib/time::instant).
APIs: stat / lstat / fstat (*filestat, *u8|i32) (void|oserror) over
SYS_newfstatat (nr=262). The out-param shape sidesteps the cgreturn
24B ABI cap; commented inline. exists(*u8) bool goes through the
syscall directly rather than wrapping stat()? — dodges task #22's
80B-scrutinee match-slot disagreement until that lands.
Three latent cgen workarounds in tree, all pointer'd to filed tasks:
#22: os.exists sidesteps the (void|oserror) match shape
#24: `at` enum bundles AT_FDCWD/SYMLINK_NOFOLLOW/EMPTY_PATH instead
of three top-level `def`s (negative-literal def DATA omit)
#25: kstat.mode typed as `mode` (enum) rather than u32 to skip the
redundant u32→enum cast emit
Tests: 976_stat_run, 9 rows — stat/lstat/fstat × regfile/dir/symlink
plus exists × {regfile,dir,noent}. Row 1 also pins perm-bit and
atime/mtime/ctime!=0 to catch silent kstat→filestat offset miscompiles
(kstat fields at 72/88/104).
Graduation to lib/fs when it ships is noted inline; signatures stay
rename-compatible.
cgreturn's variant-widen arm only filled the registers each variant's
payload needed: scalar variants left CX and R8 stale; str variant
left R8 stale. The receiver (cg_widen_tagged_store non-N_IDENT
branch) writes all four ABI words to the dst slot unconditionally,
so caller-side residue in CX/R8 (the array-index IMULQ being the
canonical primer) landed at slot+16 and slot+24.
Worker-fmtparser surfaced this through fprintf's loop body where
array indexing primed CX and a 24B-return helper failed to clear
it; bug isn't loop-specific — straight-line repro at /tmp/wcrs_repro/
repro8.ww confirms.
Patch: emit `MOVQ $0, CX` after the variant's register shuffle when
the slot exceeds 16B and the variant doesn't fill CX; same for R8
when the slot exceeds 24B. Symmetric across cstage cgen.c and
wwstage cgenstmt.ww. Order: zero-MOVQs precede `MOVQ $tag, AX` so
AX-as-staging stays safe. Inline comment at cg_widen_tagged_store
non-N_IDENT branch documents the producer-zero contract.
Test 707 (cgreturn_variant_zero): 6 rows × both stages = 12 fixtures.
Covers scalar/bool/str returns after array-index priming in
straight-line / single-loop body / nested-loop body / mixed-variant
loop. Asm byte-identity check intentionally omitted; wwstage
taggedvariantindex divergence on str/bool N_IDENT is filed as task
#20. 995_self_rebuild covers the broader cross-stage drift surface.
ww2 == ww3 == ww4 byte-identical post-fix. 67/67 green.
Deferred (followups filed): #20 wwstage taggedvariantindex,
#21 [N]str/[N]bool array-literal non-pointer-half writes, #22
consolidate variant-widen into uniform scratch-slot path.
Hare-shaped {} / {0} / {n:mods} parser + printf wrappers. APIs:
fprintf, fprintfln, fdprintf, fdprintfln, printf, printfln, errorfln,
fatalf, bsprintf. Parser handles indexed/positional placeholders,
alignment (- / default / =), pad-width, zero-pad (_05), radix (x X o
b), precision (.N for int pad / str trunc), sign markers (+, space),
and {{ / }} escape.
Internals: scandigits + scanmods drive a field-by-field dispatch into
formatfield, which inlines the field→formattable widen per-arm to
sidestep task #18 (24B return-by-value miscompile in for-loop
context). Render through formatraw + formatone over io.stream sinks.
formatone tail-pad uses a separate counter rather than mirroring
Hare's `?`-propagating loop: ww's memio.fixed returns partial-write
0 instead of errors::overflow, so the Hare shape would spin forever
on a full fixed buffer.
Deferred per drew's vet: asprintf/errorf (needs os.alloc, #16),
parametric width/precision dispatch (#16-family), float arm (#17),
log.printfln family wiring (#15).
Tests: 26 scenarios covering every placeholder shape, both arms of
fprintf's variadic dispatch (incl. bool/rune to pin #18 regression),
bsprintf overflow + width-against-full-buffer, closed-stream.
Both stages emitted fn TEXT labels by leaf only; lib/os and lib/io
exporting the same leaves (read, write, close) collided at link.
lib/fmt + lib/log worked around with @symbol("rt_syscall") stubs.
Drop d->export from the fn skip rule in mod_collect (both stages) so
exported fns mangle as <module>.<name>. Let/def/type keep current
behavior. Skip retained for {@symbol, main, empty-module}.
Add cur_mod thread through cgfn + mod_lookup_for_fn(name, hint) at
all 4 label-emit sites (TEXT def, LEAQ N_IDENT, CALL N_IDENT, CALL
N_DOT). Wwstage mirror: emitfnname + modlookupforfn + curmod.
Invariant comment pinned in both stages.
ww2 == ww3 == ww4 byte-identical at the new label format.
706_fnlabel_mangle covers same-leaf cross-module CALL + private-leaf
cur_mod disambiguation through a fn-pointer rvalue.
Wwstage LEAQ-of-fn N_DOT (`let p = mod.fn` rvalue) is a pre-existing
gap; deferred to a follow-up. fmt/log rt_syscall stubs untouched
here; cleanup follows.
Sister bug to #17 / #18. The structlit-fill helper handled nested
N_STRUCTLIT field values but a struct-typed field whose VALUE is an
N_CALL (call returning a struct, #4 cgreturn ABI) fell through to the
cgexpr-then-AX-store path — landing AX=first qword and silently
dropping DX/CX. For 16B/24B inner returns the trailing 8B/16B stayed
zero (whatever was in the destination slot beforehand).
Fix: a new N_CALL+struct branch in cg_structlit_fill / cgstructlitfill,
placed between the nested-N_STRUCTLIT recursion and the scalar
cgexpr fallthrough. Emits cgexpr -> BX reload (non-BP modes only) ->
MOVQ AX/DX/CX x full + sized tail (MOVL/MOVW/MOVB) per #4's receive
shape.
INVARIANT (commented inline both stages): between cgexpr(N_CALL) and
the AX/DX/CX stores below, no instruction may touch AX/DX/CX. Only
the BX reload (MOVQ srcoff(BP),BX or LEAQ name(SB),BX) is safe.
Sized-tail dispatch is {1->MOVB, 2->MOVW, 4->MOVL, else MOVQ}. Unlike
the scalar fallthrough — which still uses the {1/4/else MOVQ} shape
to stay byte-identical with cstage pending #13 — the new branch is
correctness-by-construction: MOVW for tail==2 only fires on call-rhs
shapes that didn't compile before, and both stages emit it
symmetrically (705's 10B inner row pins this).
Guard `fsz <= 24 && fsz%8 in {0,1,2,4}` mirrors #4's cgreturn ABI:
>24B falls through (sret deferred), and fsz%8 in {3,5,6,7} would need
shift-store — also unsupported by #4. Filed as task #21 (covers both
cgreturn and call-rhs's identical gap).
Two #15 sidesteps, both documented inline:
1. wwstage's fi.fsz for an inner-struct field is slot-padded
(8-rounded), not natural — using it would emit 2x MOVQ where
cstage emits MOVQ+MOVL for a 12B inner. The new wwstage branch
uses structnaturalsize(csi) to recover the natural size, matching
cstage's fl->type->size (check.c hands the helper natural sizes).
This sidesteps #15 without touching its scope.
2. The outer struct's totsize diverges across stages when
maxalign<8 (wwstage rounds to 8 universally; cstage to maxalign).
The 705 test rows pin `x: i64` on the outer to force outer
maxalign=8, keeping BP offsets stable across stages. Test-side
sidestep only; also #15 territory.
Files:
- cmd/w6c/cgen.c cg_structlit_fill extended
- selfhost/cmd/wcc/cgenutil.ww cgstructlitfill mirror
- selfhost/cmd/{w6c,wwdump}/main.combined.ww auto-regen
- test/wcc/705_nested_call_rhs.c 8 rows, table-driven; pins cstage
exit + wwstage exit + .s byte-identity. Tail widths 0/4/2/1, dst
modes DST_BP + DST_PTR_LOCAL, shallow + 3-deep.
- Makefile 705 wiring
Test: 65/65 PASS. 994_w6c_ww + 995_self_rebuild PASS (byte-identity
holds — load-bearing).
Sister fix to #17. The BP-rel helper from #17 covered N_LET /
N_ASSIGN N_IDENT-lhs / N_RETURN; the four N_ASSIGN N_DOT-lhs
structlit walks still went through the inline `cgexpr(field.lhs);
store-AX-sized` shape and silently dropped trailing bytes when a
struct-typed field's value was itself an N_STRUCTLIT. Affected dot
flavors: single-dot via_ptr / global / BP-rel and the chained-dot
walker (depth >= 2, all three root flavors).
Extend `cg_structlit_fill_bp` / `cgstructlitfillbp` into
`cg_structlit_fill` / `cgstructlitfill` taking a destination mode
(DST_BP / DST_PTR_LOCAL / DST_GLOBAL = 0/1/2), srcoff (PTR_LOCAL),
srcname (GLOBAL), and disp accumulator. `disp` grows by foff on
descent; srcoff/srcname stay constant across the call tree. The
pre-#17 wrappers are preserved byte-identically by delegating with
mode=DST_BP — 995_self_rebuild byte-identity holds for the no-
nested-STRUCTLIT case that selfhost source actually uses.
The non-BP modes reload BX before the ELLIPSIS zero-fill loop AND
before every field store (tagged, scalar, and the cgexpr leaf).
This is correctness-by-construction — cgexpr clobbers BX between
fields, and the redundant reload only fires on shapes that didn't
compile before. The four dot-flavor sites in each stage now compute
their dst mode + disp and call the shared helper (reducing each
from ~80-130 inline lines to ~5-12 lines of dispatch).
Stage signature asymmetry: cstage threads Local** for cgexpr; ww-
stage takes explicit totsize because #15 (split totsize into
naturalsize + slotsize) is still pending and the dot sites need
structnaturalsize while the BP-rel sites need si.totsize. Both
asymmetries are documented in the helper docstrings.
704 covers 8 rows (24 checks: 8 cstage exits, 8 wwstage exits, 8
cstage-vs-wwstage .s byte-identity diffs): 6 dst-flavors (single-
dot local/ptr/global, chained-dot local/ptr/global) plus single-
local 3-deep and single-ptr 3-deep to pin disp threading through
the helper's recursion and through DST_PTR_LOCAL BX reloads.
The nested struct-typed CALL rhs in field-walks has the same shape
as the STRUCTLIT bug fixed here but the helper only handles
STRUCTLIT — tracked as task #20.
Pre-existing landmine surfaced by #5. For a struct literal whose
field value is itself an N_STRUCTLIT of a struct-typed field, the
inline field-walk did `cgexpr(field.lhs); store-AX-sized`. cgexpr
has no whole-struct-in-register convention, so the nested literal
landed AX = first qword and the trailing bytes silently stayed zero
(or stack garbage). Three BP-relative sites in each stage hit it:
N_LET, N_ASSIGN N_IDENT-lhs, and N_RETURN N_STRUCTLIT.
Fix: shared cg_structlit_fill_bp (cstage) / cgstructlitfillbp
(wwstage) helper handles TK_ELLIPSIS autofill, tagged-field
widening, float vs scalar store dispatch, AND recurses on
struct-typed N_STRUCTLIT field values at bp_off + field_off. All 3
sites in each stage now call the helper instead of the inline walk.
Scalar store dispatch is the explicit {1->MOVB, 4->MOVL, else MOVQ}
shape (not fieldstoreop, which would emit MOVW for fsz==2) to stay
byte-identical with cstage pending task #13. Sister N_ASSIGN N_DOT
structlit walks (via_ptr / global / BP-relative-through-N_DOT) keep
their inline walk and still drop nested-STRUCTLIT silently — tracked
as task #18.
703 covers 6 rows: let_nested_i64, let_nested_3deep, let_nested_i32,
let_nested_middle (i64; switch to i32 once #15 lands),
assign_ident_nested, return_nested. 995_self_rebuild byte-identity
preserved.
Pre-existing landmine surfaced by #5 (whole-STRUCT N_ASSIGN). Both
stages' N_DOT dispatch gated on `lhs->lhs->kind == N_IDENT`; the
parser produces N_UN(STAR, IDENT(p)) for `(*p).f`, so both sides fell
off:
- Write side (cgassign N_DOT base): emitted nothing, store dropped.
- Read side (case N_DOT pointer-auto-deref): cgexpr derefed the
pointer as a scalar, AX = first qword of struct, field offset
dropped.
Fix: retarget base / dot_lhs to the inner IDENT when shape is
N_UN(STAR, IDENT). The existing via_ptr branch fires identically to
`p.f`. v1 scope is bare-IDENT inner only; `(*expr).f` (non-IDENT
pointer expression) is tracked separately as task #19.
702 covers 7 rows: write_i64/i32/str, read_i64/i32/str_len, roundtrip
Both wwstage targets transitively pull lib/strings via lib/strconv
(strconv.ww has `use strings;`), but neither rule listed it as a
prereq. A touch on lib/strings/strings.ww would not re-stamp the
binaries, masking real changes in selfhost smoke tests.
Verified: after the fix, touching lib/strings/strings.ww re-stamps
exactly wwdump_ww + w6c_ww; w6a_ww / w6l_ww / ww_ww (no strconv
use) stay put. 61/61 tests pass, 995_self_rebuild PASS.
Capture envp from the kernel-supplied stack into a DATAW slot during
_start's prologue (before CALL main), and expose it via a `rt_envp`
TEXT getter. lib/os.getenv binds the getter as `@symbol("rt_envp")
fn rtenvp() **u8` — the getter-fn pattern works around @symbol-on-let
not being supported by the compiler yet (silent miscompile otherwise).
`os.getenv(name: str) (str | void)` matches Hare's os::getenv surface:
walks the NUL-terminated envp table, "name=" prefix-matches with an
explicit `=` boundary check so prefixes don't false-match longer
names, returns the value as a borrowed str view. Empty value (env
"FOO=") returns len=0 str, not void — void is reserved for "name
not present at all".
Cohort coverage in lib/os/ostest.ww + test/wcc/974_getenv_run.c:
set / empty / unset / prefix-no-match (4 @test fns).
Receive side of #4's cgreturn ABI (aee8149) for TY_STRUCT lvalues of
size <=24B. Producer materialises rhs into AX=bytes[0..7], DX=[8..15],
CX=[16..23], zero-padded to 24B; receive sites here read the regs and
write only `declared sz` bytes — MOVQ for full 8B chunks plus a sized
tail (MOVL/MOVW/MOVB) by the *declared* struct size. ASYMMETRY: do NOT
mirror the sender's three uniform MOVQs, else trailing 1..7B chunks
overrun the next local slot. Tail chunks in {3,5,6,7} are unreachable
under WW struct align rules (size%align==0) and fall through.
Five sites wired in each stage (cstage cgen.c, wwstage cgenexpr.ww +
cgenstmt.ww), call-result + structlit rhs at each:
- N_LET `let s: T = bar()` / `= T{...}` cgenstmt cglet
- N_ASSIGN N_IDENT-lhs `s = bar()` / `= T{...}` cgenexpr cgassign
- N_ASSIGN single-DOT local-base `o.f = ...`
- N_ASSIGN single-DOT ptr-base auto-deref `p.f = ...`
- N_ASSIGN single-DOT global-base `g.f = ...`
- N_ASSIGN chained-DOT depth>=2 `o.m.in = ...`
(The four dot-flavors share one shape pattern, hence "5 sites".) Where
the dst addr needs scratch (ptr-base/global-base/via_cx), it is loaded
into BX after the call so CX stays as the third value word; for
structlit field-walks BX is reloaded before each store since cgexpr
clobbers AX/BX between fields.
wwstage needed a new `structnaturalsize(si)` helper (cgenutil.ww):
si.totsize is mis-named — it's slot-padded to 8 by registerstruct for
stack-slot use, while the receive ABI wants the type's natural size
(max(foff+fsz)). Splitting si.totsize into naturalsize + slotsize is
tracked as the wwstage struct sizing follow-up (task #15); until that
lands, the helper recovers the natural size at receive sites.
Test 701_cgassign_struct.c (18 rows, 3 checks each — cstage value,
wwstage value, asm byte-identity), wired in Makefile after 698. The
headline ASYMMETRY case is the 20B `{i32×5}` row: sender pads to 24B
via three MOVQs, receiver writes MOVQ AX +0, MOVQ DX +8, MOVL CX +16.
A regression to a MOVQ tail there overruns 4B past the slot and
flips the exit-code check.
smoke.combined.ww is the auto-regen ride-along of strings.freeall
landing in 714d089 (worker-shlex).
Pre-existing gaps surfaced and tracked separately (not fixed here,
out of scope):
- task #16: silent drop of `(*p).f = ...` explicit-deref dot lhs.
- task #17: silent zero of nested STRUCTLIT field in N_LET / N_ASSIGN
initializer — the field_chain and field_global test rows use
explicit field writes (`o.m.t = 10i64;`) rather than nested
literals as a fixture-level workaround.
- task #9: module-name-mangle for fn labels avoided in the
field_global_call fixture by `let g: outer;` (no init).
make test: 59/59. 994_w6c_ww + 995_self_rebuild PASS — bootstrap
byte-identity is the load-bearing proof for this commit's scope.
The 699 test source landed with the SK_USE→SK_X promotion fix in
7f60ebb but Makefile wiring was deferred so it wouldn't collide with
parallel fnmatch + cgreturn WIP. Tree is clear; wiring it now.
make test: 57/57.
Whole-struct return ABI for sizes <=24B. Both stages materialise rhs
into a zero-padded 24B @retscr scratch slot, then load AX=bytes[0..7],
DX=bytes[8..15], CX=bytes[16..23] unconditionally — three MOVQs
regardless of declared struct size, so the receive side (landing in
task #5) can read all three words and mask by the declared size. R8
stays reserved for the tagged-return 4th word; the uniform-MOVQ shape
is cheap over a size-conditional partial-load and keeps the producer
diff vs the existing tagged-return AX/DX/CX/R8 path minimal.
Two rhs shapes wired this pass: N_IDENT (word-copy from rhs local slot,
MOVQ pairs + MOVL/MOVB tail bounded by declared struct size) and
N_STRUCTLIT (field-walk; tagged fields delegate to the existing tagged
widening helper, float fields go through X0, int fields use MOVQ/MOVL/
MOVB by field size). Sizes >24B fall through to the existing scalar
path (only AX gets the first qword), pending sret in a future task.
N_CALL chain-return (`return otherfn()`) is deferred to task #5's
receive side — until that lands the call-result lives in caller regs.
The wwstage mirror in cgenstmt.ww matches cgen.c byte-for-byte on the
new branch; cgendecl.ww's scanlocals pre-reserves 24B for @retscr under
the same predicate (N_RETURN, fnret is N_TNAME, structlookup hit,
totsize<=24, rhs is N_IDENT|N_STRUCTLIT) since wwstage writes its
prologue SUBQ from the upfront frame total — cstage patches SUBQ at fn
end so it can allocate inline.
Latent fsz==2 MOVW divergence between stages (cstage structlit int-
branch only special-cases fsz 1/4, wwstage's fieldstoreop also returns
MOVW for fsz==2) tracked as task #13; not exercised by the new fixtures
or by any current selfhost <=24B struct return.
main.combined.ww files also pick up worker-checkfix's wwstage
architectural comment from 7f60ebb (auto-regen ran after that commit).
Port of ref/hare/fnmatch/fnmatch.ha. Public surface mirrors Hare:
`flag` enum (NONE/PATHNAME/NOESCAPE/PERIOD) and `fnmatch(pattern,
string, flags) bool`.
Algorithm is the three-phase sea-of-stars (also used in musl):
exact-match the prefix before the first `*`, exact-match the tail
after the last `*`, then greedily match each star-delimited middle
segment with backtrack on inner failure. No exponential corner —
each star anchors a "match found" at strictly increasing positions.
Bracket expressions: Hare-strict — `!` for negation, `^` rejected
as invalid; `]` as first member legal, trailing `-` literal, all
12 POSIX classes ([:alnum:] … [:xdigit:]) via direct streq + the
ascii.is* predicates.
Divergences from Hare (documented in fnmatch.ww docblock):
- byte-indexed cursors in place of strings::iterator (no UTF-8
rune iter yet); ASCII-only meaningful, multibyte matches
byte-identically. Graduates "in one go" per lib/CLAUDE.md
when the language stack grows rune iteration.
- invalid pattern collapses to `false` at the public boundary
(Hare's `b is bool && b: bool;`); a try-shaped diagnostic
entry can be added later without churning the surface.
- tail-match uses a forward cursor at `string.len - cnt`
instead of riter/prev — same byte sequence either way.
Test fixture follows the project's helper-per-row table-driven
shape (precedent: lib/encoding/base32/base32_test.ww). 8 @test
fns clustered by feature (basic / brackets / ctype / period /
noescape / musl_basic / pathname / combined), ~95 rows total
adapted from Hare's +test.ha plus musl-derived edge cases.
Wired as 972_fnmatch_run alongside 970_fmt_run / 971_log_run in
the stdlib-runtime band.
Unblocked by 7f60ebb (cstage+wwstage SK_USE→SK_X promotion
missing use_alias), which is what let the module name `fnmatch`
coexist with an exported leaf fn `fnmatch`.
Hare-shaped lib/log v1: logger vtable carrying one println slot,
stdlogger forwarding to a *io.stream, plus *logger globals (silent
/ default / global), setlogger, lprintln / println, lfatal / fatal.
Default sink is stderr through a private rt_syscall write callback;
graduates with #17 (cgen mod-mangle for fn labels), at which point
the rt_syscall stub disappears the same way fmt's will.
Module-scope struct/pointer-literal init isn't constexpr in cstage
emit_lets, so silent/default/global are wired lazily by ensureinit
on the first exported-fn entry (lib/temp rnginit pattern). Callers
that read the globals directly must call some lib/log fn first.
Skipped this round: printfln / lprintfln / fatalf / lfatalf and the
matching printfln vtable slot — they need a fmt {n}-placeholder
parser that isn't shipped yet. fatal / lfatal's exit(255) arm has
no test fixture (needs fork+wait for WEXITSTATUS); left as TODO.
971_log_run wraps the @test fixture under `ww run`, mirroring
970_fmt_run. make test: 54/54; bootstrap fixed-point (990-997)
holds.
Tags structinfo/enumtype with originating module; exact-match first,
then split pkg.X and filter by smod/emod. Without this, two modules
with same-leaf-name struct/enum types collapsed to whichever entry
appeared first in the chain.
Wired into 696_modtype_leaf_collision via a wwstage run_pos using
ww_ww (negative case omitted: w6c_ww has no checkfile pass). Updated
the test's Makefile deps to include the wwstage binaries.
Audited the rest of the lookup family — fnretlookup, fnparamslookup,
deflookup don't need the same treatment: the parser emits N_DOT.str
(call/field name) as the leaf only, and fnparamslookup is only
invoked with N_IDENT.str. Dotted module-qualified function calls go
through the module-mangling path instead.
Match-arm bind size in wwstage hardcoded str=16, []T=24, else=8 in
both cgmatch (emit) and scanlocals (frame pre-scan). A TY_STRUCT
variant fell into the 8B fallback: only the first quadword reached
the bind, and the prologue SUBQ underbooked the frame so the
emit-time localalloc(bsz=24+) wrote past SP.
Replace the hand-rolled table with slotsize(c, pat) at both sites.
slotsize already covers N_TNAME named structs (returns si.totsize),
str (16), []T (24), tuples, aliases, and primitives (8). Mirrors
cstage cgen.c cgmatch which falls through to bu->size for TY_STRUCT.
Cstage is correct; no mirror needed.
Test 695 covers seven shapes: the 3xi64 headline repro, a 4xi64
struct via let-init scrut (exercises >24B bind), a mixed-quadword
struct (i32+i32+i64+i64), str/slice/i32 negative controls, and a
direct let-init scrutinee variant. The wider 4xi64 row uses the
let-init shape because the N_DOT spill path in cgmatch tops out at
AX/DX/CX/R8 — a separate, unrelated gap from the bind size.
Typed-int literal assigned into a tagged-union slot (`h.e = 42i64;` where
e: (i32 | i64)) wrote tag = 0 (the i32 slot) instead of tag = 1 (the i64
slot). Cstage was correct: parse.c parseprimary copies tok.tsuffix onto
N_INTLIT, check.c stamps node.type = ty_i64, and cg_widen_tagged_store →
cg_tag_for_variant walks variants matching by structural type_eq —
ty_i64 lands at index 1. Wwstage had two gaps:
1. The parser (lib/ww/parse/expr.ww parseprimary) read p.curuval and
p.curtext from the current token but never the tsuffix field. Token-
side capture has been in place since the lexer's `i8/i16/.../u64/f32/
f64` glue suffix landed (lib/ww/lex/lex.ww sets out.tsuffix); the
parser side was missed. So an N_INTLIT for `42i64` carried tsuffix=""
into cgen. Mirror of cmd/wcc/parse.c parseprimary's `n->tsuffix =
t.tsuffix` line. Same plumb for N_FLOATLIT.
2. Wwstage has no checker stage to stamp N_UN's type from its inner
expression's type. `-42i64` parses as N_UN(MINUS, N_INTLIT(42,
tsuffix="i64")) and rhstargetname stopped at N_UN, returning "" and
falling through to taggedvariantindex's "first non-str variant"
fallback — which picked tag 0 (i32) for any numeric rhs in an
(i32|i64) union. Cstage's cunop returns the inner type for
TK_MINUS / TK_PLUS / TK_TILDE so the N_UN gets ty_i64 stamped
naturally; wwstage gets the equivalent via an explicit peel in
rhstargetname, recursing into rhs.lhs for these three ops. The
recursion also covers nested unary (`- -42i64`), which parseunary
builds as N_UN over N_UN over N_INTLIT.
The lib/ww/parse change is mirrored in selfhost/cmd/{w6c,wwdump}/
main.combined.ww so the bootstrap snapshot stays consistent with the
working frontend source. parser.curtsuffix is a new str field; refill
copies t.tsuffix into it; parseprimary TK_INT / TK_FLOAT copy it onto
the new node before advance.
Cstage handled both `42i64` and `-42i64` correctly already; no cstage
mirror needed.
Test 694_tagged_store_intlit — eleven rows running on both stages: i64
lit in (i32|i64); i32 lit (existing-working pin); i64 lit in
(i32|i64|str) with the str fallback at tail; u8 lit at head of
(u8|i32|i64); i64 lit at tail of (u8|i32|i64) with a +100 marker so
mis-binding into u8 can't masquerade as success; negative-i64 lit
(N_UN MINUS peel + sign extension through match-arm bind);
unary-plus i64 lit (N_UN PLUS peel); bitwise-not i64 lit (N_UN TILDE
peel; `~0i64 == -1i64`); nested unary `- -42i64` (recursion through
two N_UN levels); direct `let x: ev = 42i64;` (cglet's tagged-init
code path, separate write site from cgassign's field-write);
negative-control str field (pins the existing str-fallback path
through rhstargetname).
Pre-fix run on wwstage: 8/11 rows fail (every typed-i64 case including
all three unary operators, nested unary, and the direct let-init);
cstage 11/11 pass. Post-fix: 22/22 across both stages. make test
41/41. Bootstrap ww2 == ww3 == ww4 byte-identical.
cgdot of a tagged-union struct field previously dropped the AX/DX/CX/R8
payload-register convention used by tagged-union returns: cstage's
direct-struct branch stopped at CX (size > 16) and never loaded R8
(slice-payload variants, slot 32B); the via_ptr branch had no TY_TAGGED
handler at all, falling through to fldloadop and yielding only the tag
in AX. The N_DOT scrutinee fallback in N_MATCH similarly stored only AX
into the spill slot. Wwstage cgdot had no TY_TAGGED branch in any of
the direct, *struct, or top-level-global field-load paths, and cgmatch's
non-ident scrutinee branch didn't recognise N_DOT — dispatch always
computed want = 0 and the spill scratch was hardcoded 24B. The combined
effect: any code reading `s.taggedfield` and consuming more than one
quadword of the payload saw garbage in the upper halves.
Cstage: extended the direct-struct TY_TAGGED branch with an R8 load for
size > 24 (CX still loaded last so global LEAQ-into-CX rooting
survives), added a parallel TY_TAGGED handler to the via_ptr (TY_PTR
inner TY_STRUCT) field branch, and extended the N_DOT scrutinee spill
fallback in N_MATCH to write DX/CX/R8 alongside AX.
Wwstage: new cgloadtaggedfield helper emits the four-register load with
CX-last ordering, and dotfieldtnode resolves a field's declared type
node for a local-ident or *struct base. cgdot grew three TY_TAGGED
branches (direct local, *struct deref staging in BX, top-level global
through CX). cgmatch's non-ident-scrutinee branch grew an N_DOT type-
extraction path mirroring the N_CALL / N_INDEX shapes and now sizes the
@match_spill slot from slotsize(scrutt) so slice-payload variants don't
overflow the historical 24B alloc. rhstaggedabicall accepts N_DOT so
`let copy: ev = h.e;` and tagged-arg call sites pass through the
tagged-source spill branch of cgwidentaggedstore.
Out of scope for #28 and left as separate latents: wwstage's match-arm
bind for a TY_STRUCT-typed variant copies only 8B (cstage falls back
to bu->size; wwstage's bsz=8 default), and the variant-index lookup
for an i64 literal in (i32 | i64) picks the wrong tag on the write
side. Both surface in struct-payload tagged unions and merit their
own tasks; the new test rows steer clear so #28's fix verifies
end-to-end on scalar / str / slice payloads.
Test 693_dot_tagged_source — three variant shapes (16B i64, 24B str,
32B slice) read from direct local, *struct param, top-level global,
and let-init round-trip. The 32B-slice rows verify v.cap (R8 / +24)
so dropping the upper-word load isn't masked by len-only checks; the
top-level-global row routes the write through *p because the direct
global-LHS tagged store is a separate wwstage gap (followup). Three
negative controls (untagged i32 / str / slice fields) keep the new
TY_TAGGED guard from shadowing the existing field-load paths. Wired
into make test; 37 tests total. Bootstrap ww2 == ww3 == ww4
byte-identical.