Last per-caller migration before fold-eFinal (#50). Adds the 10 _v
variants of OLD log.ww's surface (new_v / lprintln_v / println_v /
lprintfln_v / printfln_v / lfatal_v / fatal_v / lfatalf_v / fatalf_v /
setlogger_v) alongside a vlogger vtable + vstdlogger over io.vstream.
Default sink is a module-static stderrsink_ctx_g with vt FIRST field for
the intrusive vstream cast and fd=2 — only scalars/ptrs beyond vt per
ken's mandate, no nested aggregates that would bite #18, no f32 per
#165b. Zero-init at link time per #129 A.2/A.3 SSoT; ensureinit_v wires
vt.reader / vt.writer / fd lazily on first dispatch (mirror of OLD
ensureinit at log.ww:122 + lib/temp's rnginit pattern).
OLD lib/log/log.ww UNCHANGED. fold-eFinal (#50) atomically retires the
OLD logger / stdlogger / globals + the pre-vtable stderrsink and drops
the `_v` suffix wholesale to match Hare's bare names.
Two `export` bumps on lib/fmt/vstream.ww (vfprint, vfprintf) so log's
stdprintln_v / stdprintfln_v dispatch through the existing vstream-side
formatters; additive exposure, eFinal collapses fprint over the unified
surface.
Bootstrap-embed check: log is NOT in any selfhost/cmd/*/main.combined.ww
(grep `package log\|import log` returns empty pre-impl). The fmt
vstream.ww changes are also non-embedded. 990-997 byte-id gates stay
green by virtue of log being test-only and the touched fmt symbols not
being embedded.
Probe wcc/779_log_vstream_run pins the additive surface across 4 rows
(println_v_default_stderr / printfln_v_default_stderr /
lprintln_v_custom_sink / branched_lprintln_v) cstage-only per #209
(wwstage formattable match-arm bail; bites OLD log.println identically).
Byte-id graduates when #209 lands.
Cite refs: ref/hare/log/{logger,funcs,global,silent}.ha; drew acks on
module-static stderrsink_ctx + intrusive vt + 10-fn _v parity; ken
mandates on bootstrap byte-id mechanical + simple-ctx + #129 static-init.
Sibling tasks parked (filed, NOT fixed): eFinal #50; #206 (2 cast sites
at ensureinit_v); #173 (stderrwrite_v constructs nomem + widens to
io.error); #209 (cstage-only).
Adds bufio_vstream + isbuffered_v alongside the pre-vtable
bufio.init / bufio.isbuffered surface, mirroring fold-e2's
lib/memio and fold-e3's lib/fmt parallel-API shape. The OLD
bufio.ww surface stays untouched; fold-eFinal (#50) atomically
flips the package shape, drops the `_v` suffix, and retires the
legacy callbacks.
bufio_ctx wraps an underlying *io.stream (OLD API) — bufio_vstream
src parameter type stays *io.stream until io fold-2 lands
`handle = (file | int)` (drew-deferred). vt is the first field
for the intrusive vstream→*bufio_ctx cast, same shape as
memio/fmt vstream wrappers.
Sibling task filed:
#210 struct-lit slice-typed field silently drops under
alloc(T{slice = val})?. Parallel to #207 for slice fields;
scalar/ptr fields in the same alloc-struct-lit populate
correctly. Workaround: post-alloc field-assign
c.slicefield = val. Documented inline; drops out on close.
Other deferrals retained inline: #206 cast wrappers (3 vtable
wire-up + 2 isbuffered_v comparand), #173 nomem-widen for the
io.closed → io.error boundary, #207 alloc-zero-chain for vt.
bufio is not embedded in any selfhost combined.ww (test-only);
no Makefile regen needed (#110-blind safe).
test/wcc/778_bufio_vstream_run pins the 5-row scenario set:
write+flush, read+refill, isbuffered_v discriminator, OLD/NEW
boundary check, and a branched-callee #105 row. cs+ww+byte-id
green on all 5 rows.
make test: 211 passed (was 210).
Adds lib/fmt/vstream.ww with four new wrappers — fdprint_v /
fdprintln_v / fdprintf_v / fdprintfln_v — that take a raw fd, stack-
allocate an fd_ctx whose first field is `vt: io.vtable`, and
dispatch through io.st_write on a vstream pointing at &c.vt
(intrusive offset-0 cast — same shape as lib/memio/vstream.ww's
fixed_ctx / dynamic_ctx in fold-e2). Coexists with the pre-vtable
fdprint / fdprintln / fdprintf / fdprintfln in fmt.ww.
ken's escape-risk discipline: every wrapper owns the fd_ctx slot
for its frame only; the &c.vt vstream pointer is consumed inside
the same function (passed through internal vfprint / vfprintf
helpers) and never returned. Single tagged field (vt) lets the
local default-zero plus chained `c.vt.X = …` assigns sidestep the
multi-tagged-field copy drop (sibling #207) — no struct-lit init
needed and c is a stack value, not an aliased pointer, so #195's
chained-store carve-out doesn't bite either.
Drew defer cited at the file head: io.handle (= file | int) is
out-of-scope for this fold per fold-d/fold-e3 precedent; Hare's
ref/hare/fmt/wrappers.ha:9-25 routes through the handle sum, and
fdNNN_v collapses into bare fNNN_v once io fold-2 lands the port
(filed inline as "io fold-2 handle port" backlog).
Internal vfprint / vfprintf duplicate the per-arg and {n}-
placeholder loops from fmt.ww (fprint:177, fprintf:676) because
the OLD versions take *io.stream (the legacy struct) and fmt.ww
stays UNCHANGED this fold. Shared bits — i64dec, modsinit,
scandigits, scanmods, formattable, field, mods, fmtabort — are
reused directly from fmt.ww. vformatfield mirrors the inline-per-
arm dispatch shape OLD formatfield (fmt.ww:648) uses to dodge #18
silent miscompile of 24B return-by-value in for-loop context.
Cast workaround per #206 at each vtable-fn-ptr-slot init (2 sites
per wrapper, 8 total): bare `&fn_name` does not type-check as
`(*<alias> | void)`. Same `(&fn): *io.<role>` cast shape that
lib/memio/vstream.ww uses. Drops out wholesale when #206 closes.
#173 workaround at fdsinkwrite_v: constructs nomem and widens to
io.error explicitly rather than `os.trywrite(...)?` — same shape
memio.vstream.ww line 71-74 note adopted.
OLD fmt surface is UNCHANGED. fold-eFinal (task #50) atomically
flips the package shape: deletes OLD wrappers + callbacks, renames
_v suffix off, and migrates the few callers (with io fold-2's
handle sum landing in the same flip).
Probe test/wcc/777_fmt_vstream_run.c: 4 rows
(fdprintf_v_int / fdprintln_v_multi / fdprint_v_raw /
branched_fdprintf_v) open per-row /tmp output files, dispatch one
V wrapper per row, reopen the file, read the bytes back, and
assert both the exact byte content and a unique row-tagged exit
constant. 4/4 fixtures total, all green via cstage.
Cstage-only per row (no STAGE_WW, no byte_id) — pre-existing
wwstage match-arm bug (sibling of #190, filed inline as #209): the
wwstage checker bails `case: not a variant of scrutinee (X)` /
`match: variant not handled (formattable)` on the OLD
fmt.fdprint's match arms whenever any probe `import fmt;`s the
package. The bug bites the OLD surface identically — even
`fmt.errorln("hi")` from a probe trips the same trace. 970
fmttest dodges via cstage-only ww run; 995 self-rebuild dodges
because no selfhost cmd transitively pulls fmt (err.ww imports
fmt but no main.ww in cmd/{ww,w6c,w6a,w6l,wwdump} pulls err.ww in).
Byte-id graduates when #209 closes — out-of-scope for the additive
fold-e3.
Combined.ww regen NO-OP: none of the five tracked combined.ww
files (cmd/{ww,w6c,w6a,w6l,wwdump}/main.combined.ww) embed
`package fmt;` — fmt is not in the dep graph of any selfhost
binary. combined_ww_fresh stays green untouched.
210/210 tests passing (was 209; +1 for 777_fmt_vstream_run).
Adds lib/memio/vstream.ww with three new constructors —
fixed_vstream / dynamic_vstream / dynamicfrom_vstream — that return
io.vstream (= *io.vtable, from lib/io/stream.ww) alongside the
pre-vtable memio.fixed / dynamic / dynamicfrom shape in memio.ww.
Hare's memio::fixed/dynamic return a `stream` whose first field IS
io::stream (= *vtable); ww mirrors that intrusively with fixed_ctx
+ dynamic_ctx structs whose first field is `vt: io.vtable`. A
heap-alloc'd *fixed_ctx is castable to vstream via `&c.vt`, and
callbacks recover the outer ctx via `s: *fixed_ctx` (same pattern
as lib/bufio.stream over io.stream and lib/log.stdlogger over
logger). ptr/len/cap kept flat (memio.ww:39 SOP) to dodge the
chained-dot-through-pointer-into-slice-subfield miscompile family.
Constructor flow: alloc with vt zero-initialised via a local, then
chained `c.vt.X = …` field-assigns through the *ctx pointer.
Struct-lit init via `vt = local_vt` (with local pre-set) silently
drops tagged-union slots past the first — sibling task filed,
workaround is the alloc-then-assign route (proven byte-id between
both stages). *ctx is a plain pointer-to-struct, not an aliased
pointer, so the chained-store path doesn't hit #195.
Cast workaround per #206 at each vtable-fn-ptr-slot init (8 sites
across the 3 constructors): bare `&fn_name` does not type-check as
`(*<alias> | void)`. Same `(&fn): *io.<role>` shape that
test/wcc/775_io_vtable_run.c uses. Drops out when #206 closes.
OLD memio surface is UNCHANGED. fold-eFinal (task #50) atomically
flips the package shape: deletes OLD constructors + callbacks and
renames `_vstream` suffix off.
Probe test/wcc/776_memio_vstream_run.c: 4 rows
(fixed_read_5 / dynamic_write_grow / dynamicfrom_alt_rw /
branched_fixed) exercise both vtable flavours through the
io.st_read / st_write / st_close dispatchers. Each row drives
cs runtime + ww runtime + cs.s == ww.s byte-id — 12 fixtures
total, all green.
Pre-existing wwstage gap surfaced + documented inline: ww_ww's
combined.ww concat order trips the wwstage checker on os.tryread
/ trywrite / tryopen's bare `return r;` over `(int | oserror)`
when os is checked after rt/io. Each probe row places `import os;`
FIRST to match the ordering selfhost uses (time → os → rt → …)
where the checker resolves cleanly. Sibling task; resolves the
ordering-sensitivity in the wwstage checker drops the workaround.
Additive: keeps lib/io/io.ww's pre-vtable `stream` struct +
`read`/`write`/`close` wrappers (fold-e2-eN migrates the legacy
surface to vtable-backed implementations and retires it).
lib/io/stream.ww — vtable struct (reader/writer/closer slots,
spelled `(*T | void)` per #192 — ww parser rejects `nullable *T`),
vstream = *vtable, and the st_read/st_write/st_close dispatchers
per ref/hare/io/stream.ha:33-68. Void-arm `return e;` chains two
direct widens: concrete `errors.unsupported` → `error` (#199 α)
then `error` → (size|eof|error) (#205 NAMED-variant nominal at
tagged→tagged subset). Hare's `?`-propagating st_close collapses
to a direct `return (*c)(s);` because #173 is still open; the
surface stays Hare-shaped.
lib/io/types.ww — retarget reader/writer/closer fn-aliases from
*stream to vstream. Extend `error` union to include
`errors.unsupported` explicitly (no spread — per ken's #204-block
the wrapper-vs-flatten layout asymmetry would mis-widen; the
deferred fix is filed as #199b layout-extension).
test/wcc/775_io_vtable_run.c — 7-row sentinel: reader/writer/
closer × {set, void} happy paths + branched-callee runtime.
Rows verify the call runs + the constant exit; the void-arm
rows do NOT inspect the resulting variant tag (deferred #199b
wrapped-slot tag-remap mis-routes to dst tag 0). Cstage and
wwstage emit byte-identical asm on every row.
test/wcc/768_io_types_run.c — track the alias retarget; rows
now build a vtable, pass `&vt` (= vstream), and call through
the fn-VALUE param shape.
Combined.ww regen for w6c + wwdump per #110: lib/errors lands
transitively via the new `import errors;` in types.ww.
Sibling filed inline (NOT fixed): the checker rejects bare
`&fn_name` / `let p: *alias = &fn` assignment to a
`(*alias | void)` field — the structural `*fn(...)` value isn't
accepted as the `*alias` NAMED variant. Both stages reject.
Probes route around via explicit `(&fn): *io.reader` cast at
each vtable-field assignment.
The tagged→tagged subset arm walked src's leaves against dst's flat
variant list, so `let r: (size | eof | wrapper) = e` with e: wrapper
REJECTED at cstage's checker — wrapper's leaves (unsupported, underread,
nomem) aren't direct variants of dst. Wwstage's permissive tail
accepted silently but cgen then miscompiled the tag (#199b layout-
extension family, deferred).
Mirror the concrete→tagged fix from #199 (α) at type.c:316: when src is
a NAMED-tagged wrapper and dst has a direct NAMED-tagged variant equal
to src, accept by nominal identity BEFORE the subset loop. Wwstage's
isassignable mirrors the structural insertion before the existing
`*confident = false; return true;` tail (deferred-tightening per #202).
SSoT with `is`/`as` non-recursive variant lookup (#198 family).
Cgen's tag-remap for the wrapper-as-whole case still maps src variants
to dst tag 0 — the wrapped-slot layout for `dst.tag = variant_idx,
dst.payload = src` is #199b future-work. Probe verifies checker-accept
+ runtime exit-clean only; does NOT inspect the resulting variant tag.
Probe 774_tagged_widen_named_variant.c covers 5 rows: bug-repro,
nested-wrapper, pure-leaf subset (regression), concrete-unrelated
rejection (gate), branched callee. Two sibling cgen/checker bugs
surfaced (wwstage cgwidentaggedstorebp ssz<slot_sz pad gap; wwstage
isassignable !void-alias collapse) and documented inline at the
probe-row comment, kept in #202 family.
checkisas walked the unflattened AST u.list via casevariantin (typeeqast
streq), so any variant introduced via a `...inner` spread was invisible
and rejected as "is/as: not a variant of operand". Repro:
type rsh = (size | io.eof | ...io.error);
let r: rsh = 42: size;
if (r is io.underread) ... -- pre-fix wwstage REJECTS
io.underread is in io.error.params, which tinfofornode splices into the
parent at L1827-1836, but the AST u.list still holds the single
`...io.error` entry that streq("io.underread", "io.error") rejects.
Route through flatvariantidxt — the same Phase-N helper #179 cgmatch
and #66 cgtagvariantidx already key off. Mirrors cstage cmd/wcc/check.c
:1662-1675 u->params + type_eq. Falls back to casevariantin AST walk
when tinfo isn't available (defensive — non-#198 path stays as-is).
project_tinfo_lossy_nominal: name-keying was the pre-Phase-N workaround
for tinfo lossy on nominal identity; typeeq inside flatvariantidxt now
handles NAMED ptr-id (#64), so the checker pair aligns with cgen on the
flattened-variant axis.
Closes the cgen-drain mini-cluster (#201 -> #199 -> #200 -> #198).
773_isas_spread_variant: 5 rows (spread_is_inline_variant,
direct_cross_mod_tagged, cross_mod_named_void, same_module_variant,
spread_as_inline_payload). Rows 2-4 byte-id; rows 1/5 skip byte-id due
to layout-asymmetry on `...wrapper` (cstage flattens at resolve_type,
wwstage computes maxsz off vt.size of the un-spliced alias) — sibling
not blocking the checker correctness fix.
cgtypeassert kept scrutoff=0 when the scrutinee wasn't an N_IDENT
(direct call result, arr[i], p.field, ?, paren-wrap of any of those),
so the tag-load fell on (BP) — the saved-BP word — and the payload-
load on +8(BP) — the return address. The wwstage repro returned 220
(garbage from RIP) where cstage returned 42 (impl-e1-resume sibling
of #199/#201).
Mirror cstage cmd/w6c/cgen.c:6300-6316 N_TYPEASSERT non-IDENT arm.
Add an `else` branch after the existing N_IDENT path that resolves
the tagged type via matchscrutt, alloc an @asrt_spill slot via
matchspillsz/localalloc, cgexpr the LHS, then spill the AX/DX/CX
tagged-return-ABI words: AX→+0 (tag), DX→+8 (word0), CX→+16
(word1, guarded on spill > 16). Subsequent tag-check + payload load
indexes off the spill like the IDENT path. Helpers reused from
cgmatch (cgenexpr.ww:1422-1460).
cstage's cgtypeassert omits the cgmatch 4-word R8→+24 spill (rule-10
stage symmetry: rather than diverge into a 32B-payload case the test
suite doesn't exercise, mirror cstage exactly and file the cstage
omission inline). Filed inline: cstage cgtypeassert needs the same
R8→+24 path cgmatch already has (drew's design rationale, blocked
by the rule-10 floor today).
772_typeassert_nonident: 7 rows (call_as_size — the repro, call_as_str
— CX→+16 spill + BX post-load, call_as_namedvoid — void-variant
tag-check fires, payload load is a 0-byte no-op, call_as_fnptr —
fn-ptr variant 8B word0, call_as_u8 / call_as_i16 — narrow scalar
round-trip via MOVQ + MOVQ confirms no truncation, branched_call_as
— runtime-chosen tag). Each row gated on cstage runtime + wwstage
runtime + cs.s == ww.s byte-identity.
cgen has no wrapped-slot layout — the tagged-union slot is universally
[tag:8B][payload:up_to_24B], single level. The recursive walk admitted
let r: (size|io.eof|io.error) = u for u: io.underread (transitively
in io.error.params); cg_tag_for_variant + taggedvariantindext don't
recurse, returned -1, defaulted to tag=0, and the slot read back as
variant 0 = size at runtime.
Restores SSoT inside the checker pair: is / as / match variant
lookup is already non-recursive (#198 sibling), and the LET-init /
return / assign arms now agree. Aligns DOWN to the leaner side
(rule-10 stage symmetry). ww-stricter than Hare; harec keeps the
drill at ref/harec/src/types.c:702-739 (#199b is the deferred
wrapped-slot layout port).
Pre-flight audit (drew mandate): zero transitive-widen sites in
lib/ + selfhost/ + cmd/ + examples/. No wrapper-tagged variant
(io.error, strconv.error, fmt.field) is used as a variant of a
wider union anywhere in bootstrap. Mechanical fix.
Escape hatch for callers: spread (...wrapper) inlines the wrapper's
flat variants into the parent set at parse time. Wwstage's gate
additionally preserves the recursive drill on op == TK_ELLIPSIS
because wwstage stays AST-keyed (cstage flattens at resolve_type).
771_widen_transitive: 5 rows (reject_transitive_widen,
spread_alt_widen, direct_flat_variant, branched_callee_widen,
wrapper_typed_widen). Row 2 is CS-only — wwstage's is / match on
spread-expanded variants is open-bug #190/#198.
cgreturn's forwardtagged detection was keyed on the CALLEE NAME
(N_IDENT/N_DOT only via fnretlookupmod), so any other callee shape
fell through to the variant-tag synthesis path — clobbering the
just-returned AX/DX/CX/R8 tagged-ABI words. The deref-call case
`(*r)(...)` (impl-e1-resume STOP, 994 w6c_ww byte-id red) was the
proximate trigger.
Replace with a TYPE-BASED predicate over the checker-stamped tinfos
(rhs.type_ vs c.fnret.type_), mirroring cstage cgen.c:8007 passthrough.
Peel TY_NAMED on both sides then identity-check the underlying
TY_TAGGED — sufficient for the NAMED case because tinfocache memoizes
per typedecl (#191 lineage). Variant-pointer fallback walks the
params chain when identity fails so anonymous unions like the
cross-module (i32 | void) shared between strings.byteindex and
bytes.index still forward correctly; full recursive tinfo
structural-eq is gated by #178 (typeeqast's TY_TAGGED arm
conservatively returns false today).
Probe 770_return_tagged_forward covers 6 rows — IDENT forward, widen
non-matching, deref-call (the bug), scalar (sanity), nested call,
cross-module forward — each gated on cstage runtime + wwstage runtime
+ cs.s == ww.s byte-identity.
`type vs = *vt; fn(s: vs) s.field` linked-failed in wwstage with
`undefined reference to field' — cgdot read lc.tnode.kind without first
walking N_TNAME aliases, so lkind stayed N_TNAME (not the underlying
N_TPTR), structlookupchain missed (`vs` isn't a struct alias), and the
lookup fell through to the SB-global fallback that emits `MOVQ
<field>(SB), AX`. Mirror cstage type_chase_named (cmd/w6c/cgen.c:144-155)
via an aliaslookup loop, stopping at struct aliases so the existing
direct-struct N_TNAME arm stays byte-id with pre-fix #22 callers. LOOP
(not single-peel) — Phase-N builds N_TNAME chains
(project_tinfo_lossy_nominal), depth-2+ aliases require iteration. Inner
peel on the pointee is unnecessary: the existing structlookupchain
already walks N_TNAME chains via aliaslookup (cgenutil.ww:1266-1276); row
4 of probe 769 proves the inner-chain depth-3 path stays green without
an explicit inner peel.
Probe test/wcc/769_dot_aliased_ptr.c covers 4 rows (fn-param read,
let-binding read, double-alias receiver, pointee-alias chain), per-row
runtime + byte-id gates. Files inline two sibling bugs surfaced during
impl (cgassign write-side silent-drop, chained-N_DOT spine link-fail)
plus a cstage checker assignability gap on chain-depth-2 aliases — all
out-of-scope per rule 11 split.
New lib/io/types.ww mirrors ref/hare/io/types.ha — the surrounding
port that lives alongside the existing lib/io/io.ww (pre-vtable
stream + eof + underread). Hare splits the same way (stream.ha +
types.ha share `module io`); ww does the equivalent via dir-enum.
Each type cites Hare per CLAUDE.md rule 9:
- mode (enum u8) — ref/hare/io/types.ha:29-34. RDWR=3 (not
Hare's `READ | WRITE`) because ww enum-value
positions don't fold expressions; bitfield
value SSoT preserved, divergence inline.
- whence (enum i32) — ref/hare/io/types.ha:37-41. Hare leaves the
underlying implicit; ww requires one. i32
matches the `off` type fold-e wires in.
- error — ref/hare/io/types.ha:11. Hare spreads
`errors::error`; lib/errors not ported, so
the union carries the two tags observable
in this fold: underread (from io.ww) and
the predeclared `nomem` (#29, type.c:72 /
check.ww:78). NOT redefined here.
- reader/writer/closer — ref/hare/io/types.ha:46/51/55. EOF=eof
(not Hare's `done` singleton) per #93
and the io.ww:8 rationale. `*stream`
forward-refs the existing pre-vtable
struct in io.ww; same cross-file pattern
Hare uses.
Drew signoff (this fold only): seeker, copier, strerror, and the
EOF=done singleton DEFERRED to fold-e — they need the `handle` sum
and #93's done landing. Hare's `_unsafe` carve-out unaffected.
eof / underread / stream re-used from io.ww (NOT redefined); io.ww
keeps the pre-vtable struct unchanged, ditto its WHY-comments.
Combined.ww regen (#110): selfhost/cmd/{w6c,wwdump}/main.combined.ww
auto-pulled the new types.ww via dir-enum (+52 lines each, same
package io). Makefile dep lines for wwdump_ww + w6c_ww add the new
source so editing it triggers rebuild.
Probe: test/wcc/768_io_types_run.c — 5 rows × 2 stages = 10
invocations. Pins enum value/underlying + the three fn-type aliases
at the param slot. Both siblings filed inline in the probe header:
- #189 wwstage `let r: io.reader = fn_name;` bails "let: not
assignable". cstage accepts. Param + struct-field paths work
in both stages, so io vtable port is unblocked. Probe uses
the alias only at the param slot.
- #190 wwstage match-arm on cross-module variant tag bails
"case: not a variant of scrutinee (io.eof | io.error)". Likely
same family as #178. cstage accepts. Probe uses `is` instead
of `match` for the variant gate.
make test: 201/201 (was 200; +1 from 768). 990-997 byte-id +
combined_ww_fresh + sizelint all green.
cgmatch's non-nullable variant-tag synthesis gated flatvariantidx on
pat.kind == N_TNAME (with N_TSLICE else-branch for #19's untyped-elem
fallback). N_TPTR / N_TFN / N_TPTR(N_TFN) case-patterns fell through
both, leaving r=-1 → want=0 so every variant past 0 silently
collapsed to tag 0 — runtime-passes only when the value happens to
sit on variant 0 (zero-coincidence miscompile).
Cstage cg_tag_for_variant works on resolved Type and is kind-
agnostic; harec stores `_case->type = ctype` (ref/harec check.c:2527).
#66 Phase-N already flipped match dispatch to typeeq; #179 is the
last site still keyed on AST kind. Memory: project_tinfo_lossy_nominal
+ feedback "Hare = resolved-type-only match dispatch".
Route through flatvariantidxt(scrutt.type_, pat.type_) directly,
guarded by istaggedtype + typeisslice(pattype) for the slice axis.
No new helper — existing flatvariantidxt / flatslicevariantidx wire
up unchanged.
767 probe locks the fix across 5 rows: (1) nullable *fn branched
store (ken's verify gate — proves the nullable arm at line 1484
isn't perturbed); (2) *i32|*i64 storing &i64 — pre-fix wwstage
emitted CMPQ $0 for *i64 arm, post-fix CMPQ $1; (3) *fn(i32)|*fn(i64)
via intermediate local; (4) branched runtime variant choice defeats
const-fold; (5) aliased ptr variants (typeeq through TY_NAMED).
Per row: cs runtime, ww runtime, cs.s == ww.s byte-id.
Sibling filed inline: widening `&fn` INLINE into a fn-ptr-only
tagged union picks tag 0 in wwstage's cgwidentaggedstorebp
(out-of-scope; row 3 dodges via intermediate ident store).
Discovered while verifying #181's checker fix end-to-end: wwstage's
cgcall pre-computes `isfnptrcall` only for N_IDENT (local) and
N_DOT (struct fn-ptr field) callees. For a non-named callee — the
deref-call `(*f)(...)` shape (N_UN TK_STAR) most prominently — the
flag stayed false, the IDENT/DOT name-emit branches both missed,
and the emit produced `CALL (SB)` with an empty symbol.
Cstage handles this naturally via its default-fallthrough at
cmd/w6c/cgen.c:5918-5921 — `else { cgexpr(c, n->lhs, locals);
ins1(c, A_CALL, areg(D_AX)); }` catches every callee shape that
isn't bare-IDENT module-fn or N_DOT module-qualified call. The
fix here mirrors that fallthrough: any callee whose kind is
neither N_IDENT nor N_DOT sets isfnptrcall = true, routing
through the existing cgexpr-into-AX + CALL AX path.
Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main
.combined.ww per #110 freshness gate.
Lands as a follow-up to the #181 checker bail-lift: the checker
now stamps the deref-call N_CALL (so cgen runs), and with this
fix the wwstage cgen lowers it correctly. test/wcc/766's wwstage
+byte-id rows go green; runtime symmetry with cstage holds.
Pre-fix the wwstage checker bailed asserttyped on the N_CALL whose
callee was N_UN TK_STAR over a *fn — selfhost/cmd/wcc/check.ww
exprtype's N_CALL arm only resolved IDENT/DOT-named callees and
early-returned nil for any other shape, leaving e.type_ unstamped
so the post-checker invariant fired. cstage worked because cexpr
recurses on the callee — TK_STAR's unop arm returns t->sub which
IS the TY_FN, no name path needed.
Fix: replace the `if (nm.len == 0) return nil` early-bail with
`if (nm.len > 0) { name-lookup }`, so non-named callees fall
through to the existing fn-VALUE fallback below (peel TPTR /
dealias to TFN / stamp the result type). Mirrors harec
check_autodereference at ref/harec/src/check.c:1566. cgen post
-#180+#185 already lowers the deref-call correctly, so lifting
the asserttyped bail is silent-SIGSEGV-safe per drew + ken.
Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main
.combined.ww per #110 freshness gate.
Probe: test/wcc/766_star_fn_deref_call.c, 5 rows table-driven —
minimal / branched-callee / alias-chain / fn-with-args / fn
-tuple-return. Gate flip from 765: every row now gates BOTH
stages — cstage runtime, wwstage runtime, AND cs.s == ww.s byte
-id. This is the runtime coverage 765 deferred plus the symmetry
gate that proves both stages emit identical asm for the deref
-call shape. Closes the full c-cluster (#180 + #185 + #181 all 3
commits working together end-to-end).
Pre-fix the N_UN TK_STAR arm applied the generic pointer-load
`MOVQ (AX), AX` to a *fn operand. cgexpr on the operand already
left AX = fn-addr (post-#180 LEAQ); the spurious second load
read the first instruction word, and the subsequent CALL AX
jumped through that junk address and segfaulted.
Cstage: cmd/w6c/cgen.c N_UN TK_STAR opens with a TY_NAMED-peel
+ TY_FN early-break — leave AX as the fn-addr cgexpr produced.
Wwstage twin in selfhost/cmd/wcc/cgenexpr.ww cgun TK_STAR walks
the n.type_ tinfo chain the same way (TY_NAMED peel then TY_FN
check) and returns before the generic load. Mirrors
ref/harec/src/check.c expr_call's STORAGE_POINTER→STORAGE_FUNCTION
path (harec skips the deref since the pointer IS the address).
Both stages must land together per rule-10 (cstage-only would
break 990-997 byte-id gates — same lesson as #180).
Probe: test/wcc/765_star_fn_deref.c, 5 rows table-driven —
minimal / branched-callee / alias-chain / fn-with-args /
fn-tuple-return. Every row is cstage-only via stage_mask
because wwstage's checker bails asserttyped on `(*f)(...)`
(filed as #181 — N_CALL type_ stamp gap on deref-call); #181's
own probe will lock the wwstage runtime once the bail lifts.
Gate-blind risk (ken's note): byte-id alone cannot catch this
class because both stages drop the SAME instruction
symmetrically, so cs.s == ww.s holds either way. Runtime
exit-code is the only correctness net here.
Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main.
combined.ww per #110 freshness gate.
Pre-fix the N_UN TK_AMP arm fell through silently when the operand
was an N_IDENT naming a top-level function — the let/def cascade
had no TY_FN branch, so the store at the assign site picked up
whatever AX held from prior code (commonly a stale arg register).
A subsequent (*f)(...) jumped through that junk and segfaulted.
Cstage: cmd/w6c/cgen.c N_UN TK_AMP IDENT adds a TY_FN arm before
the let/def cascade, mirror of the read-arm at line 2330 — same
mafn(opnd->str, c->cur_mod) shape. Wwstage twin in selfhost/cmd/
wcc/cgenexpr.ww cgun TK_AMP IDENT uses the analogous predicate
fnretlookup(c, nm) != nil + emitfnname(c, nm, c.curmod), matching
the cstage emit on byte-id. Both stages must land together per
rule-10 (cstage-only breaks 990-997 byte-id gates).
Combined.ww regenerated for selfhost/cmd/{w6c,wwdump}/main.combined
.ww per #110 freshness gate.
Probe: test/wcc/764_amp_fn_ident.c, 6 rows table-driven —
minimal / branched-callee / alias-chain / fn-with-args / fn-tuple
-return / cross-module. Rows 1-5 gate both stages (run + .s LEAQ
check + cs.s == ww.s byte-id); row 6 cross-module is cstage-only
because wwstage bails asserttyped on `&mod.fn` (sibling project
#184, filed). Per drew option (b) the probe exercises the address
-of without (*f)(7) — deref-call runtime coverage stays with
project #181's probe once the wwstage asserttyped bail on
N_CALL(*f) is lifted.
Phase 1 cross-mod verdict = FINE for cstage (LEAQ emits via the
already-present N_DOT TK_AMP branch at cgen.c:2477-2493); WWSTAGE
fails asserttyped on the same shape → project #184.
Round-1 reviewer follow-up to 7031e0d. The brief was explicit
that the test header had to NAME the sibling cgen bug (project
#179, cgmatch flatvariantidx N_TNAME-only gate) — not just
describe its shape. Pre-fix the gap paragraph mentioned the
gate site (selfhost/cmd/wcc/cgenexpr.ww:1512) and the runtime-
masking mechanism, but lacked the literal "#179" handle a
future bisect would grep for. Also folds in the explicit
zero-init-tag-0 masking note (the brief asked for it).
Comment-only — no Makefile, no harness, no row delta.
Pre-fix master left N_TFN under typeeqast's conservative
"anything else fails" tail (selfhost/cmd/wcc/check.ww:748-751).
case-patterns spelled with a raw `*fn(...)` head — the io vtable
use case — tripped casevariantin / casecovers on every variant
compare, so a well-typed `match (v: tagged-of-fn-ptr) { case
*fn(...) => ... }` would not compile under wwstage.
Adds a TY_FN arm that mirrors harec STORAGE_FUNCTION
(ref/harec/src/types.c:589-615): recurse on the result type
(.lhs), iterate the param chain (.list of N_PARAM, descend each
.lhs), require the variadic flag (.op == TK_ELLIPSIS) to match
position-by-position, and require both chains to terminate
together. Param NAMES do not participate (harec analog), and the
C-variadic terminal sentinel (N_PARAM with .str == "...") is
handled defensively even though wwstage's parseparams doesn't
currently produce it. Attributes + default-param values are NOT
checked (drew-pre-approved, harec doesn't either).
cstage type.c:239's type_eq walks the same shape on the resolved
Type. typeeqast lives one layer below — a documented divergence
filed as project #178 for the harmonization fold; the in-source
comment cites #178.
Probe 763_typeeq_fn_ast.c locks 7 rows covering identical /
diff-return / diff-arity / diff-param-type / variadic / param-
name-only / io-vtable shapes across cstage + wwstage (14
fixtures). Pre-fix wwstage red-errors every row at the checker
("case: not a variant of scrutinee" + "match: variant not
handled"); post-fix all 14 compile and the tag-0 arm fires
(exit 7). Per-row .s byte-id is intentionally NOT gated — see
the probe header for the cgmatch N_TPTR-not-routed-to-
flatvariantidx sibling bug that drives the divergence on rows
b/c/d/e/g; orthogonal to this AST-layer typeeqast fold and not
swept per the brief's "do not sweep" instruction.
Row 5 in 098b58d was byte-identical to row 2 ({i32,i32,i64} fields
{{4,4},{4,4},{8,8}}): same struct, same field table, only label
differed. The row's own comment acknowledged "off from 8→8 (already
aligned)" — i.e., it did not exercise mid-record alignment at all
despite the label "i32_i32_i64_mid_align" and the commit-body
listing "mid-record align step explicit".
Replace with {i16, i64}: after f1 off=2, f2 must align off 2→8
before placing the i64. A missing `off = align(off, fa)` on either
walker (check.c:760 / check.ww:958 / structabisize) would land f2
at off=2 and size to 10 — clearly red. This is the shape the row
was meant to be.
Six distinct shapes after this fix; gate polarity unchanged (GREEN).
Project #78 was ken-flagged: cstage folded size(struct{i32,i32,i32}) to
12 (cmd/wcc/check.c:760, `(off + maxalign - 1) & ~(maxalign - 1)`) while
wwstage walked a slot-padded fsz ladder and yielded 16. The two
checkers ran different layout formulas. d4e500f (#169) closed the
cgen ABI half by introducing structabisize and walking it via per-
field tinfo.align; 39f9267 + 66a91c8 converged the remaining cgen
sites that had picked the slot-padded number. Verified at the size(T)
fold and the .s byte-id on every layout-edge shape.
Add 762_struct_abi_size.c (6 table rows) to lock the closure. Each
row carries a (sz, aln) field table; the expected size is computed in
C via the Hare layout formula (rule 13: no hardcoded size literals —
a formula bump lands in compute_expected and every row tracks). Both
gates run per row: a runtime check that `return size(T): i32` matches
the computed expected on both stages (catches a fold drift), and a
w6c vs w6c_ww .s byte-id (catches a cgen-ABI walker drift even when
the fold still matches — the two walkers are independent SSoTs).
Rows target maxalign edges:
i32_x3_maxalign4 #78 canonical, natural==ABI==12
i32_i32_i64_lead_narrow maxalign 8, narrow lead, ABI 16
i64_i32_i32_sub8_tail #169 sub-8 tail, ABI 16 (the round-up)
i16_x3_maxalign2 maxalign 2, natural==ABI==6
i32_i32_i64_mid_align mid-record align step explicit
i64_x3_natural_24 all 8-wide, natural==ABI==24
GATE POLARITY: this file must stay GREEN. A red here means either
d4e500f reverted, or one of the two layout walkers (check.c:760 /
check.ww:958 / cgen.c struct_arg_size / cgenutil.ww structabisize)
drifted from the formula.
Project #16 (inferred-let struct-typed local pushed as call-arg: cstage
2-word vs wwstage 1-word) does not reproduce on master. A 16-shape impl
sweep confirmed byte-identity for every reasonable trigger; the
mechanism that closed it is incidental, distributed across four
commits:
ea1579a exprtype resolves SK_USE module-qual N_DOT call results
7198937 asserttyped bail armed (any nil-typed VALUE node fatal)
39f9267 DOT-recv, structlit-fill, bare-let zero-init via structabisize
66a91c8 let-IDENT memcpy, IDENT-assign recv, nested struct
call-recv via structabisize
Together they guarantee `checkletassign` writes a resolvable type-AST
to `n.lhs` for every inferred let, `cglet` carries it onto the local's
`lc.tnode`, and `pushargsrev`'s N_IDENT struct arm (cgenutil.ww:459)
reaches `structparamsize > 0` so the 2-word push fires — byte-id with
cstage's `args[i]->type` -> struct_arg_size path (cgen.c:427, :5452).
This commit adds 761_inferred_struct_arg_push.c (6 table rows) to lock
that symmetric behavior in. Rows cover:
i64_i64_infer_direct canonical 16B
u32_i64_decf_infer decf32-shape (ken-flagged ftos.ww:411)
i64_i32_narrow_tail_infer #169 maxalign-8 tail-padded ABI
i32_x3_maxalign4_infer maxalign 4, ABI 12B
ident_rhs_chain_infer `let p = q;` two-step inference chain
big_sret_infer_then_ptr_arg >24B sret receive via callsretsize
Each row drives both stages, asserts cs==ww asm byte-id, and runs the
binary asserting the exact exit code. A future regression of any one
closing commit reds this gate at the matching shape; the citation map
in the comment header points the bisector at the four commits to walk.
Separate divergences surfaced during the sweep (filed independently,
not bundled here): #173 tagged-return-try, #174 cast-inferred call,
#175 arr[0] inferred, #176 nested-field let inferred, #177 aliased-
struct-return cstage 1-word.
make test: all 194 tests passed (exit 0).
Three more wwstage cgen sites still used unrounded structnaturalsize where
cstage rounds via lu->size — pre-existing gate-blind cs!=ww latents the #169b
reviewer surfaced: cgenstmt N_LET struct-IDENT memcpy (let p2: T = p1; twin
cgen.c:7869), cgenexpr N_ASSIGN N_IDENT-lhs register RECV (s = mk(); twin
cgen.c:4700-4737), and cgenutil's nested struct N_CALL recv inside
cgstructlitfill (twin cgen.c:2121).
Converge all three onto structabisize, completing the same-class closure
started by #169 and continued by #169b. Also corrected the inline comment at
cgenutil.ww:3273-3286 that wrongly claimed fl->type->size was natural
(check.c:760 sets ABI). sretretsize at cgenutil.ww:1301 is gate-equivalent
natural and is left alone.
Probe 698 +3 rows (one per converged site) with cs==ww .s byte-cmp and a
pre-fix-rebuild discriminator. 990-997 byte-id hold.
Three wwstage cgen sites still used the unrounded structnaturalsize where cstage
rounds via lu->size (check.c:760), pre-existing gate-blind cs!=ww latents
flagged in #169's reviewer notes: cgenexpr DOT register-RECV for obj.f = mk()
(~5175/5393/5628/6164); cgstructlitfill's TK_ELLIPSIS zero-fill branch
(cgenutil); and cglet bare 'let z: T;' zero-init of a maxalign<8 struct
(cgenstmt). Each produced MOVQ-vs-MOVL or wider-write divergence vs cstage on
the trailing word of a sub-eightbyte tail.
Converge all three onto the maxalign-rounded structabisize the #169 work
established at the register-ABI sites (cite cstage cgen.c:7720 RECV twin +
cgen.c:2085 cg_structlit_fill). cgstructlitfill's signature drops the external
totsize parameter in favor of one internal source; the field-walk path is
untouched, only the ELLIPSIS zero-fill uses the ABI size. cglet's slot
allocation stays on the frame size; only the zero-fill extent uses ABI.
Gate-blind (the bootstrap exercises none of these shapes); covered by 5 new
rows in probe 698 with cs==ww .s byte-cmp and a pre-fix-rebuild proving the
exact MOVQ-vs-MOVL discrimination. 990-997 byte-id hold.
exprfloatkind was wwstage cgen's structural float-classifier — a workaround for
the checker stamp being untrustworthy. With the previous commit arming the
asserttyped bail, every checked value-node is now stamped (or cited-exempt),
so its job collapses to a 2-liner reading n.type_ — the same path cstage cgen
has always taken. Retire it: inline the stamp-read at its eight sites (cgcast,
cgun, cgbin lhs+rhs, cgcall pop, pushargsrev, cgwidentaggedstorebp, cgreturn
x2 collapsed), delete the wrapper, and delete the two residual
sibling-evidence loud-aborts (cgbin float-arith, cgwidentaggedstorebp
float-arm) — their operands are real source value-exprs the armed bail now
stamps, so the guards can never fire.
One synth-post-checker value-node remained outside the bail's reach: the
variadic-slice descriptor pushed in pushargsrev/cgcall (cgenexpr.ww). Stamp
it at synthesis with the variadic param's []T slice tinfo so the inlined
reads see a stamped node, no nil special-case. Byte-id-neutral by design
(slice tinfo and nil both read non-float); 990-997 confirm.
Closes the bail-rearm arc — wwstage now reads the same float-class SSoT
cstage does, the gate-blind float-classification family is closed, and the
build+test corpus is asserttyped-clean by construction.
The wwstage asserttyped pass only WARNED on a checked value-node with no
result type_, a check-bail-discipline regression that let gate-blind nil-stamp
miscompiles ship green (the whole #6 arc: tuple/struct/fn-ptr/enum/binding
nil-stamps were all invisible to the byte-id gates). With every nil-gap class
now stamped (module-qual calls, fn-ptr-field calls, computed enum value-exprs,
for-range/massign binds) the bail can finally arm: warn -> os.exit(1).
Exempt exactly the two legitimately-no-type value classes, each a positive
cited assertion (never a residual warn): the EXPR_ASSERT family (abort/assert,
guarded against a user shadow; harec check.c:877,893) and seeded pseudo-builtin
callees (len/append/free/alloc/size — a structural nil-decl-SK_FN predicate,
not a name-list). The pre-existing module-ref and dot-lhs filters stay: they
identify access-path components that aren't value exprs (harec EXPR_ACCESS),
not exemptions.
Verified clean over the broadest net — the armed checker over all five
self-build combined units (the full selfhost source) plus the 901 gap corpus —
zero out-of-class bail; fails-loud confirmed (undeclared call, abort's args, a
nil dot-base all bail). Checker-only: 990-997 byte-id hold.
The 990 standalone probes fed raw single files to the ww-stage checker, but
ww build always pre-concatenates a module + its transitive imports into a
resolved <stem>.combined.ww (expand()), and w6c/wwdump only ever consume that
single resolved unit — mirroring harec, which resolves the module graph before
check and never checks a partial unit. A raw partial file is thus an
unsupported input the asserttyped invariant must never see. Before arming the
bail, fix the PROBE (not a production exemption): a resolveunit() helper ww
builds each fixture to its .combined.ww and feeds that, detecting the artifact
(import-only modules link-fail but expand() writes the unit first). err/tok/
smoke unresolved-import-nil warns 20/30/24 -> 0.
Probe-only (990_selfhost.c); compiler untouched, 990-997 byte-id unchanged. err
is dropped from the cgen byte-match probe (its unit imports fmt, whose
match-exhaustiveness the ww-stage checker rejects while cstage accepts — a
separate cs!=ww divergence, filed); err's asserttyped coverage is retained in
the resolve probe.
For-range tuple-destructure binders (for (let (k,v) .. s)) and the tuple
massign discard _ were left nil-typed: the for-range binders are N_IDENT
use-sites and the _ slot, though unbound, has a real element type. Add a
shared stamptuplebinds helper — one lockstep walk distributing an N_TTUPLE's
per-element types onto a binder chain — refactoring the existing N_MLET
destructure loop into it (behavior identical) and adding N_FORRANGE and
N_MASSIGN call-sites. _ is STAMPED with its slot's element type (unbound is
not untyped), not exempted. Mirrors harec create_unpack_bindings
(ref/harec/src/check.c:1354-1419), the routine harec shares between let-unpack
and the for-each header (:2308-2317).
A prerequisite for arming the wwstage asserttyped bail. byte-id holds (cgen
derives binder/elem widths structurally, never off type_; 990-997 green).
Extends the 901 gap-corpus with 901_forrange_tuple.ww + 901_massign_blank.ww.
A computed enum member — B = A + 4, RW = R | W, sibling/chained backref —
left its value-expr node nil-typed: enumvalfold folds the constant but never
stamps the expr, and since enum members are not installed as scope idents the
sibling backref resolves to nothing, so BOTH the N_BIN/N_UN wrapper and the
backref N_IDENT go nil (literal members are fine). Stamp the value-expr subtree
(only-nil) to the enum's underlying storage type via a new
stampenumvals/stampnilexpr pass on the N_TENUM branch. Mirrors harec checking
each member value-expr at the underlying type (ref/harec/src/check.c:4419).
A prerequisite for arming the wwstage asserttyped bail. Checker-only — the
value folds to a constant at every use site and in cgen, so the node's type_
is never read by codegen; 990-997 byte-id hold. Extends the 901 gap-corpus
with 901_enum_corpus.ww.
A call whose callee is a fn-VALUE (a fn-pointer struct field like w.emit(...),
or a local/param) had no free SK_FN entry, so exprtype's name lookup missed and
the N_CALL went nil-stamped (the fn-ptr-field class of the asserttyped gap
audit — 3 warns at smoke). When the name lookup misses, fall back to harec's
check_expr_call shape: read the result off the checked callee node's own type —
autodereference + dealias to the TY_FN, take its result (ref/harec/src/check.c
:1566-1581). Name lookup stays primary: a fn-NAME callee node carries its
return type, not its fn-type, so an N_TFN check first would mis-yield void for
a fn-returning-fn; only genuine fn-value callees reach the fallback.
Drives the 901 gap-corpus B count to 0 — with A/D already closed, only the
legitimate abort exemption (C) remains before the bail can arm. Byte-id holds
(cgen's fn-ptr detection is structural, independent of the stamp; the lib/io
return-forwarding site is cs==ww on both bootstrap combined.ww).
The wwstage checker resolved a module-qualified call/access mod.x by the
same-module preference in scopelookupprefer, so when the importing package's
name collides with a type/fn of the same leaf (package fnmatch with fn fnmatch;
package random with type random), the dot-lhs mod resolved to the same-leaf
SK_TYPE/SK_FN instead of the coexisting SK_USE import — the N_DOT module-qual
arm never fired and the call went nil-stamped (the D class of the asserttyped
gap audit: fnmatch 2, random 16). cstage resolves this via Sym.use_alias; this
ports the equivalent to wwstage.
Add scopelookupuselocal (a single-scope SK_USE lookup, twin of scopelookuptype)
and prefer SK_USE for a dot-lhs in exprtype's N_CALL and N_DOT arms, keyed on
the scope where scopelookupprefer landed so a local binding sharing a module's
leaf keeps value semantics. Scope-layer only — no type-identity touch (cstage
use_alias never reaches type_eq).
Drives the 901 gap-corpus D count to 0 (random_test now byte-id cs==ww).
Compiler binary unchanged (no such collision in its own source); 990-997 hold.
The separate fnmatch bare-enum-member cgen cs!=ww is unrelated (filed).
The wwstage checker's asserttyped pass currently only WARNS on nil-typed nodes
(a check-bail-discipline regression). Before re-arming it to a bail, this probe
pins the warn set so the re-arm is verifiable — the live ww-driver suite is
blind to it (the stdlib _run tests use the cstage ww driver, no asserttyped;
990 feeds only -t/-a).
901 runs the ww-stage checker (wwdump_ww -c) over the gap-bearing combined.ww
fixtures and asserts the asserttyped warn count per fixture against a manifest:
checked 0 (closed-root sentinel), smoke 3 (fn-ptr field call), utf8 8 (abort
intrinsic), fnmatch 2 + random 16 (module-leaf==type/fn collision). Each
subsequent stamping fold drives a count to 0 and edits its manifest line; the
bail is safe to arm when all reach 0. A new nil-gap or a regressed class fails
loud (mutation-tested both directions). Test-infra only — no compiler change.
Re-arming the wwstage asserttyped bail surfaced 94 nil-stamp warns in the
checked corpus: let (a,b) = mod.fn() left its destructure bindings (and every
use) unstamped because exprtype's N_CALL arm resolved an N_DOT callee by bare
leaf — the gap its own comment flagged (#16/#17). Fix at the root: when an
N_DOT callee's lhs resolves to SK_USE, resolve the result via
scopelookupinmodule (mirror cstage cexpr check.c:1035 + cgen fnretlookupmod
cgen.ww:2263). The N_MLET backfill then just consumes the resolved tuple,
matching harec create_unpack_bindings (check.c:1354-1419), which does no callee
resolution — single path, no third copy.
The SK_USE gate leaves the module-leaf==type/fn-name collision cases
(random/fnmatch) on bare lookup — that nominal-resolution gap is a separate
fold. Beyond destructure, the root fix also closes a latent cs!=ww divergence
on non-destructure cross-module same-leaf calls (a head-ordered shadow was
mis-sizing the receive slot).
asserttyped is ww-stage only, so the live ww-driver suite can't see this — the
net is the warn count (checked 94->0, collision cases unchanged) + cs==ww .s
(probe 956). Compiler binary unchanged; 990-997 byte-id hold.
The RETURN twin of #165: a qualifying float-struct was returned GP-only
(struct{f64,f64} in AX/DX instead of X0/X1) — value-correct via GP transport
but not SysV register-class conformant. Route each float eightbyte through the
SSE return cursor (X0/X1) and each integer eightbyte through GP (AX/DX) via
independent cursors, at the struct-return SEND and RECV, both stages, reusing
struct_float_class verbatim. Closes the temporary tuple-SSE/struct-GP
divergence opened across #164/#165.
A qualifying struct has >=1 lone f64 so maxalign is 8 and the ABI slot is an
8-multiple — no sub-8 tail — so #169's sized tail is unreachable here and the
integer eightbyte uses a full MOVQ (cstage agrees, proven by the f64i32
cs==ww byte-id). f32 / multi-float-per-eightbyte stays GP (deferred #171b);
>16B stays sret.
Gate-blind and value-correct, so the discriminator is the SEND/RECV register
class (MOVSD X0/X1 vs MOVQ AX/DX) — covered by probe 946_structret_run.
wwstage struct-return RECV and RETURN used unrounded / round-to-8 sizes where
cstage uses the maxalign-rounded lu->size / rt->size, so a struct with maxalign
8 and a sub-8 tail (e.g. struct{i64,i32}) — or a maxalign<8 struct on the
return path — unpacked with a different trailing-word width (MOVL vs MOVQ)
between stages. Value-correct either way, but a cs!=ww asm divergence.
Add a dedicated structabisize = round(natural, maxalign) used only at the two
register-ABI sites. structnaturalsize stays unrounded: cstage's >24B sret and
memory-move path (cgen.c:8150, Task #33) genuinely uses the unrounded natural
size, so the two are different sizes — rounding the shared metric breaks 995.
maxalign derives from each field's tinfo.align (mirrors cstage check.c:708),
not an fsz ladder (a ladder over-rounds composite [N]u8 fields).
Gate-blind (no bootstrap struct hits the maxalign-8+tail shape) — the
discriminator is the cs==ww .s byte-cmp; covered by probe 698.
struct params were passed GP-only, so a struct{f64,f64} argument landed in
DI/SI instead of X0/X1 — value-correct for internal ww calls (the bits
round-trip) but not SysV register-class conformant. Add a per-eightbyte
classifier (struct_float_class) routing a qualifying struct's float eightbytes
through the SSE arg cursor, reusing #163's dual-cursor plumbing and #164's
field classification. A struct qualifies only when every eightbyte is
pure-integer or a lone f64 exactly filling it (and >=1 f64); anything else —
any f32, multiple floats per eightbyte, a straddling or aggregate field —
falls back to the unchanged GP path (f32 sub-eightbyte packing deferred #165b).
Both stages' predicates are alias-aware and identical in coverage.
Gate-blind and value-correct either way, so the discriminator is the callee's
receive instruction (MOVSD vs MOVQ), scoped per-function — covered by probe
946.
Tuples were unhandled as parameters — no tuple arm in arg-push, arg-pop, or
callee-recv in either stage — so a tuple param fell to the 1-GP-word else and
dropped all but its first element (integer tuple params too; floats doubly
lost). Add tuple-param arms (SEND push+pop, callee RECV) across both stages,
reusing #164's per-element SysV classify with the 6-GP (DI,SI,DX,CX,R8,R9) +
8-SSE (X0-X7) arg cursors. A frame slot @tupargscr decouples the producing
call's return cursor from the overlapping arg cursor (capture-before-clobber).
Overflow (>6 GP / >8 SSE) fails loud (rule 7). Scoped to the N_CALL producer;
first-class tuple values (ident/literal) remain a separate unimplemented gap.
Gate-blind (the bootstrap passes no tuple params) — covered by table-driven
probe 905, which proves pre-fix element-drop and the loud-stop.
A multi-float tuple return mis-routed: SEND pushed a stale AX leaving the
float stranded in X0, while RECV (#105) read every float from X0 — so a
(f64,f64) return collided both floats. Add an SSE cursor [X0,X1] parallel to
the GP cursor [AX,DX,CX,R8], placing each element by its SysV class +
within-class index (ref/qbe/amd64/sysv.c retr), symmetric send/recv across
both stages, via a generic tuple_store/tupstore+tupsse helper that #171 will
reuse for struct-return convergence. (f64,f64,f64) = 3 SSE eightbytes exceeds
the 2-register cap and now fails loud (rule 7) rather than colliding.
Unifying the 16B and 32B whole-tuple-single-var branches onto the dual cursor
was required for f64+str coexistence; it also fixes a latent str-first
single-var bug (the old 32B branch read .ptr from DX while the send placed it
in AX). No str-first or 32B tuple exists in-tree, so integer paths stay
byte-identical (990-997 green).
The two local-binds were #168 dodges: a CSE before `% 10u32` to avoid the
signed-IDIVQ-on-call-result shape that #168 has now fixed. Inline to the
natural form, faithful to ftos_ryu.ha:418-421,444-445 — this exercises #168
in real ported code. The dividends are zero-extended u32 (always positive as
64-bit), so IDIVQ and DIVQ agree on the value; the fix is a cs==ww byte-id
shape correction, not a value change. strconv is compiler-imported, so this
regenerates the w6c/wwdump/smoke amalgamations.
Twin of #134 (N_INDEX arm): the wwstage signedness classifier did not
consult the checker-stamped type_ for an N_CALL result, so an
unsigned-returning call got signed IDIVQ/SARQ instead of DIVQ/SHRQ.
cstage was already correct (reads the stamped operand type; check.c:1433),
so this is a wwstage-only arm — symmetric outcome both stages.
Gate-blind (the bootstrap lacks the shape) — covered by table-driven
runtime probe 906, which also asserts w6c==w6c_ww .s byte-identity.
The f32 coda of Drew's strconv 5-fold plan — f64tos shipped in fold-5a
(0e66073); this completes the plan. Re-lands the f32-exclusive Ryū path
that fold-5a removed under the dead-code rule (it was #143-blocked):
pow5fac32/pow5multiple32/pow2multiple32, mulshift32, mulpow5inv_divpow2/
mulpow5_divpow2, decf32, f32todecf32 (ftos_ryu.ha), and the f32tos driver
(ftos.ha:448). Plus F32_POW5_*_BITCOUNT in ftos_data.ww.
The f32 path REUSES the shared u64 core (mulshiftall64/u128mul/u128rshift/
log*) and f64computeinvpow5/f64computepow5 — and thus the f64 SPLIT2
tables — exactly as ftos_ryu.ha does; there are no separate f32 tables.
Unblocked by #143 (aff7725): f32tos calls math.f32bits(n), passing an f32
arg, which now spills MOVSS (4B) in both stages. Verified: f32tos's arg
push/pop is MOVSS, w6c vs w6c_ww 0-diff on the strconv-embedding
combined.ww (w6c/wwdump/smoke regenerated).
Dodges (cgen bugs still deferred, each cited at-site): decf32.exponent:i64
sidesteps the #169 narrow-second-field struct-return unpack (byte-id
gate-confirmed, not an ABI guarantee); #168 div/mod local-bind on the two
`%10` sites; *decimal pointer field reads (#170); [32]u8 buffer reuses
f64tos's byte-id-clean band over Hare's [14] (#43). mulshift32's U32_MAX
bound inlines the literal — ww's types.U32_MAX is package-private (#172).
Test: ftostest.ww gains f32 vectors — the tcs G/void rows (shared f32/f64
shortest), the f32-exclusive tcsf32 extremes (1e-45 / 1.1754944e-38 /
3.4028235e38, full 24-bit mantissa), specials, a negative-normal, and
33554432 (the sole e2>=0/q<=9 runtime cover). make test 187/187 incl
908_ftos_run + 990-997 byte-id + combined_ww_fresh.
cstage spilled f32 args via MOVSD (8-byte); ABI-correct is MOVSS (4-byte,
single class) per SysV (ref/qbe amd64/emit.c:524 — slot-copy-through-XMM
follows the float class). wwstage already emitted MOVSS; align cstage up
via op_for(node_isf32) at the arg PUSH (cgen.c:5366) + POP (cgen.c:5469).
Byte-id-only divergence (callee reads the f32 param low-32 regardless),
but it blocked cs==ww — closes the f32-arg-push half of the float-register
family (#119/#122/#125/#157). Bootstrap-NEUTRAL (no f32-arg caller in the
990-997 gated path). Test 907_f32arg_run (f32-arg push single/multi/mixed/
stack, cs==ww byte-id). Make test 187/187 incl 990-997.
Unblocks fold-5b (strconv f32tos passes f32 to f32bits).
The N_RETURN tagged-pack scalar-variant arm did MOVQ AX,DX, but a float
variant's value is in X0 not AX -> packed stale int (broke stof64/stof32
return (f64|invalid|overflow)). Fix: float variant bridges X0->DX via a
stack slot (SUBQ $8,SP; MOVQ $0,(SP); MOVSS|MOVSD X0,(SP); MOVQ (SP),DX;
ADDQ $8,SP), gated type_isfloat/exprfloatkind. No MOVQ-xmm->gp form
exists, hence the spill (715-class, cgreturn-register-pack twin of 715's
store-to-slot). Zero-slot-first -> deterministic f32 high-4. AX-independent
-> also resolves the multi-variant cs!=ww. Bootstrap-NEUTRAL (compiler has
no float-tagged-return). Test 707 +3 rows (f64/f32/multi, slot+8 bit-exact;
f32 no-f32-arg to isolate #143). Make test 184/184 incl 990-997 byte-id.
Close A.3's deferred shape-14 (nested array). (a) emit_array_lit_bytes
gains a TY_ARRAY-element arm (mechanical clone of the TY_STRUCT-element
arm — recurses; esz=etype->size, rule-13; ...-nested loud-reject). (b)
double-index read tbl[i][j]: when the indexed element is TY_ARRAY, leave
the sub-array ADDRESS in AX instead of dereferencing (sister of #135's
N_DOT-base fix, on the N_INDEX path) — new elemisarrayc/tinfoisarray
helpers, both stages. Storage + read = one 2D-end-to-end concern (A.2/A.3
storage+LOAD precedent).
Unblocks strconv fold-4's powers_of_ten[596][2]u64 (direct double-index
access). Bootstrap-NEUTRAL (new arms gate on TY_ARRAY-element; 1D
consumers byte-identical, 990-997 green). Test 919 +2D rows + 3D +
...-nested-reject. Deferred siblings: #155 (sub-array bind / whole-
aggregate copy), #160 (global-struct-field index base).
Extract emit_array_data + emit_array_lit_bytes helpers (both stages,
mirrored) for module-level let/def with N_ARRLIT initializer or no-rhs
zero-init. Two-pass validate-then-emit: validate pass walks elements
and fails atomically on any non-foldable element (no partial-byte
emit on failure); emit pass writes element bytes after success.
Element-kind dispatch: integer via fold_int_literal byte-for-byte
preserved from pre-A.3 inline arm (bootstrap NEUTRAL — 6 live consumers
in lib/os/bufio/strings/encoding-utf8/strconv-stof_data), float via
inline bitcast + sign-XOR byte-loop (A.1 shape, no INT64_MIN — sibling
#144), struct via recursion into emit_struct_lit_bytes (A.2 helper).
Out-of-scope element kinds (ptr-elem, nested-array) rule-7 fatal.
emit_struct_lit_bytes gains TY_ARRAY field arm calling emit_array_lit_
bytes recursively — closes A.2 parked shape-15 (array-in-struct
`def D: dt = dt{tag=42, buf=[1u8,2u8,3u8,4u8]};`).
LOAD-side widened symmetric to A.2 precedent: cstage cgindex N_INDEX
direct-ident isglobal gate widened via new DefArray registry
(def_isarraydef populated in let_collect parallel to DefStruct);
wwstage cgindex N_INDEX falls through to defvartnode on letvartnode nil
(reads defent.dtnode field added in A.2). Both stages materialise
array-def via LEAQ name(SB) same as array-let.
Mid-impl rule-7 stop: refactor initially routed only rhs==N_ARRLIT
through emitarraydata, leaving nil-rhs zero-init arrays (e.g.
`let f64tos_buf: [64]u8;` in lib/strconv) silently SKIPPED → undef-ref
at link of wwstage-rebuilt selfhost binaries. Caught on first gate run
via bootstrap 994/995 RED. Fixed by adding nil-rhs branch to
emitarraydata (zero-fills arrt.size bytes) + widening wwstage caller
to route both N_ARRLIT and nil through helper. Same-class-lower-stratum
pattern (recurring across A.1 N_UN-peel, A.2 sz==8-short-circuit, A.3
nil-rhs-drop); banked as feedback memory.
Test 919 (11 rows: int-elem 1B/4B/8B + signed-N_UN-peel + float-elem
f64/f32 + def-int / def-float / struct-with-array-field shape-15 +
explicit-zero + single-elem-regression) registered. Make test:
182/182 incl. 990-997 byte-id + combined_ww_fresh.
Followups filed:
- #43 — wwstage emitletdataw str/slice-size arms lack !isarr guards;
hypothetical no-rhs [16/24]u8 triple-emits (NOT A.3-introduced;
no live consumer; 2-line parity fix)