wwstage's cglet no-rhs path zero-inited only 8B primitives (MOVQ) and >8B composites (XORQ run), so an 8B *composite* local (single-field struct/tagged, e.g. struct{src:*vtable}) declared bare (let b: box;) was left uninitialized -- reading an unassigned field returned stack garbage (a silent read-before-init), and it diverged from cstage which zero-inits any 8B local (cs!=ww byte-id, surfaced by #5's bufio box{src:io.stream}). Add the missing arm: a non-array composite of size 8 emits MOVQ $0, matching cstage's no-rhs sz==8 zeroing. cstage unchanged (already correct -- align wwstage UP). Scope is 8B-only: cstage does not zero-init sub-8 composites either (sub-8 falls through to nothing on both stages, already cs==ww), so zeroing sub-8 on wwstage would create a new divergence; the sub-8 read-before-init garbage is a separate shared-both-stages latent (#20). Adds test/wcc/790 (8B byte-id row + read-before-init correctness lock reading 0 on both stages). rule-10 align-up; closes the #213 8B-composite slice; unblocks post-eFinal #5.
Variant selection (cg_tag_for_variant / flatvariantidxt) matched union variants by exact type only, so widening a bare value (e.g. *vtable) into a union with a NAMED ptr-alias variant (stream = *vtable, in handle = (file | stream)) found no match and the tag defaulted to 0 -- the wrong variant. In the compiler this hit emitbytes' io.write(&cgoutstream.vt) once io.write took a handle, writing the asm to a garbage fd -> empty .s -> w6c_ww miscompiled everything. Add a second selection pass: when the exact pass finds no variant, structurally compare the bare source against each NAMED-alias variant's unwrapped type; exact-match still wins in pass 1 (so a bare i64 stays the i64 variant, not oserror=!i64, which kept the os/errno union building). A >=2-structural-match collision guard (extending #218's) hard-errors LOUDLY on genuine nominal ambiguity (two ptr-aliases to the same struct) instead of silently first-picking, citing #199b/#10. Symmetric across cstage (cmd/w6c/cgen.c) and wwstage (selfhost/cmd/wcc/cgenutil.ww). One-level NAMED unwrap (chained ptr-aliases unmatched, unexercised -> #17). Adds test/wcc/789 (positive widen byte-id+runtime + degenerate-ambiguity reject guard, both stages). Unblocks post-eFinal #5's handle surface. rule-10 fix-up.
wwstage's checker rejected matching an imported union's variants cross-module: casevariantin/casecovers' typeeqast did a raw streq, so a union's bare variant "unsupported" failed to match the dotted case pattern "errors.unsupported" (cstage compares resolved-Type identity, qualifier-agnostic). Add a (module, leaf)-pair fallback after typeeqast: reduce both the case pattern and each variant to (module, leaf) and match on pair equality -- a dotted name keeps its own qualifier, a bare name takes the union's defining module (taggeddefmod, via the aliassym hop chain). This closes BOTH directions: the false-reject of valid cross-module match AND a false-accept of a foreign same-leaf qualifier (case othermod.foo vs errors.error now rejected, matching cstage). Handles the nested errors.error-in-io.error case (the dotted variant keeps mod=errors, not the union's mod=io). typeeqast stays the first check so currently-valid code is byte-id-unchanged; the pair-match fires only on the previously-rejected qualified-vs-bare mix. Wired into casevariantin, casecovers, and the is/as caller. Adds test/wcc/787 (cross-module positive, exhaustiveness, foreign-qualifier reject-guard, dotted-variant body). Unblocks #5's cross-module io.error/errors.error decomposition. rule-10 fix-up; #10-family (wwstage cross-module resolution).
Hare-faithful port of errors::errno (ref/hare/errors/{rt,common,opaque}.ha): the 13 named common error conditions, opaque_data/opaque_ (the type-erased tail whose strerror fn-ptr defers to os.strerror), and errno(os.errno) error mapping the ~12 mapped errnos to named conditions and wrapping the unmapped tail in opaque_. The raw errno type (!i32, kernel-int width, distinct from oserror's !i64 negative raw return), the E* constants, and the strerror message table live in lib/os: ww folds Hare's sys role into os, so os is the import floor that lib/io and lib/errors build on -- documented in lib/CLAUDE.md (os never imports io or errors). errors.error is explicitly enumerated, matching Hare; the ...errors::error spread is only io.error's (blocked by #199b). Prereq for post-eFinal #5's faithful io error mapping; retires the nomem-collapse interim. Adds errnotest (mapping / opaque-tail / strerror) + test/wcc/902_errno_run. Landing required two wwstage cgen fixes (#9 struct-variant-large-union return, #11 deref-store alias narrow). Divergences cited at-site: bare-type-name return -> let+return; switch fall-through vs Hare's exhaustiveness-only default; opaque_ const dropped.
The deref-store *p=v integer arm computed width by name-keying the pointee node (primsize(pe.str)), so a pointer to a !-flagged or otherwise non-primitive-named alias (os.errno = !i32) fell to the MOVQ default where cstage type-resolves to MOVL (cgen.c:4647-4652) -- cs!=ww and a latent 4-byte over-write. Add a primsize-first fallback to the existing typenodeprimresolved (peels N_TBANG/N_TENUM/N_TNAME alias chains to the underlying primitive) so *(!i32-alias) narrows to MOVL. primsize-first preserves *bool/*i32/*u8 byte-id (typenodeprimresolved excludes bool). Adds test/wcc/786 (store through *(!i32-alias) then read an adjacent field -- over-write guard -- plus a plain-*i32 control). The residual name-blind cases (non-ident pointers, str/float/bool aliases, size-2 i16/u16) are routed to #10/#12. Unblocks errno's opaque_ tail store. rule-10 fix-up: wwstage aligned up to cstage.
collectstructs registered a struct only when the typedecl body is N_TSTRUCT, so an error-struct (type X = !struct{...}, whose body is N_TBANG{N_TSTRUCT}) never entered wwstage's c.structs table. The name-keyed structlookup then missed at the return-widen sites, and wwstage dropped the struct construction when returning a struct variant of a large (>4-eightbyte) union -- wrong runtime value and cs!=ww. cstage has no struct name-table (pure tinfo) and was correct. Peel the N_TBANG body in collectstructs so error-structs register; both existing cstage-mirrored widen arms then fire. Provably byte-id-inert: no committed source defines a !struct today. Adds test/wcc/785 (struct-variant return + named-void control, both-stage byte-id + runtime). The >4-eightbyte 5th-word truncation on return remains, symmetric (cs==ww) and unread by the tag/early-word path; #222's sret hidden-pointer cutover is the committed fix (table-retirement tracked as the wwstage->tinfo SSoT arc). Aligns wwstage up to cstage (rule-10).
The Option-C parallel _v vstream API was scaffolding to bring the io stack up alongside the old surface; carrying both permanently is a rule-9 divergence from ref/hare, which has exactly one io surface. Collapse onto that surface (stream = *vtable, ref/hare/io/stream.ha) and rename the _v symbols to their Hare names (io vstream->stream, fmt vfprint->fprint, bufio/memio/log surfaces, log.new). Deletes the 4 lib/*/vstream.ww scaffold files; regenerates w6c/wwdump combined.ww. cstage and wwstage stay byte-identical and combined_ww_fresh holds; all 220 tests pass.
The wwstage cgdot #191 alias-peel loop broke on a name-keyed any-module
structlookup, so a receiver whose alias name collides with a struct of the
same name in ANOTHER module resolved to the foreign struct and fell through to
an undefined `name(SB)` global instead of the field load. The eFinal FLIP
renames io's `vstream` -> `stream`, which collides with memio's `stream`
struct, so io.read/io.write/io.close's `match (s.reader)` emitted
`MOVQ reader(SB), AX` (reader is also a type-alias) -> cs != ww (cstage chases
the nominal TY_NAMED.under pointer chain, module-correct). Gate-blind: on
master both stages emit the same wrong store so byte-id stays green; the FLIP
corpus is the first to put the io-alias/memio-struct collision in one build.
Fix (wwstage-only align-down; cstage is the authority and is untouched): make
the peel's struct-break MODULE-AWARE — break only on a same-module struct (a
genuine struct-value receiver); a same-module alias keeps peeling to its
underlying (io.stream -> *vtable -> the pointer field-load arm); a foreign leaf
keeps the prior any-module heuristic. New structsamemod / aliassamemod mirror
the same-module-first pass already in structlookup / aliaslookup. This is not a
naive alias-first reorder (which would reintroduce the mirror collision: a
same-module struct plus a foreign same-leaf alias). Peel-only — the direct-
struct arm's broader cross-module same-leaf-STRUCT name-keying is filed as #224.
#208-family (name-keyed resolution dropping to a wrong global) but in cgen, not
the checker; #213 is distinct (cosmetic local-struct-match divergence).
test/wcc/784_xmod_alias_struct_collide_run: collision (cross-module alias-vs-
struct, same leaf), symmetric (guards the same-module-struct break against a
naive reorder), and a no-collision control — branched callee. The discriminating
net is cs.s == ww.s (the path is gate-blind and cstage is correct, so byte-id
flips when wwstage is fixed); confirmed by source-revert. The FLIP's combined.ww
is now cs.s == ww.s byte-identical.
Assigning a >24B by-value struct-return into a GLOBAL lvalue dropped the
struct body: the sret dest was routed to a BP scratch temp and only the
8-byte return pointer was stored (`MOVQ AX, g(SB)`); the callee wrote the full
struct to the scratch, which never reached the global. A BP-relative dest
offset cannot name a global symbol. Pre-existing GATE-BLIND silent miscompile
— both stages emit the same broken store, so byte-id (990-997) stays green
while runtime is wrong — latent until the eFinal io surface put a global
`cgoutstream: memio.stream` (>24B) on the path, where it made cgen.ww's
self-built w6c_ww buffer every function body into a corrupt global (pos stayed
0) and emit prologue-only output.
Fix, both stages, byte-identical: route the sret dest pointer to the global
symbol so the callee writes the full struct through RDI straight into the
global. cstage adds cg_sret_dest_sym, mirroring the existing str/slice global
arm (skip the @sretscr scratch, emit `LEAQ masym(sym), DI`). wwstage carries
the lhs IDENT node (sretdestnode) and emits `LEAQ name(SB), DI` via emitsymname
— identical to cstage's symbol mangling, verified cs.s==ww.s on the probe and
across 990-997. #211-family (by-value struct + global/pointer), but a distinct
site: the cstage assignment-store into a global, not the wwstage call-return.
N_LET-global static-init (`let g: T = mk()` at top level) is a separate,
independently-broken path (#221) — link-fails for init-via-call, returns 0 for
constant init — not the sret-receive gap and not on the eFinal path; deferred.
test/wcc/940_global_sret_run: global assign (plus a branched callee to defeat
const-fold), through-pointer mutation (the io vtable-callback shape that
surfaced this), and local-init/assign regressions — runtime asserts on both
stages (the net, since byte-id is gate-blind here) plus cs.s==ww.s.
Discrimination confirmed by revert+rebuild: with the global arm disabled,
global_assign emits the truncated store and exits 1.
The outer widen of a NAMED multi-variant union value into an enclosing union
mis-tagged: the store took the tagged-subset path (inner value at slot+0 plus
a sub-variant remap, collapsing every inner sub-variant onto outer tag 0),
while the match-extract reads the nested layout (outer tag at +0, inner 16B
value at +8). Store and extract disagreed, so the match selected the first
arm. Pre-existing silent miscompile, latent because error-origination sites
(`let e: io.error = <leaf>; return e`) were gate-blind — no test discriminated
a freshly-originated error at a branched caller; the io vstream surface is the
first to do so.
Fix, both stages, byte-identical: cg_variant_match (cmd/w6c/cgen.c) and its
wwstage mirror cgvariantmatch (cgenutil.ww) fall back to structural equality
of the unwrapped tagged unions when the alias collapse loses nominal identity
(a NAMED outer variant vs an unwrapped-tagged source); the widen store now
writes the inner value at slot+8 and the outer tag at +0, matching the
extract. The inner union's build/payload/extract already worked (a destructure
through the outer round-trip recovers the inner payload) — only the
outer-widen store was wrong.
Collision guard (the fallback is unsound without it): structural matching
cannot disambiguate two nominally-distinct same-shape variants in one outer
union. That is unreachable under today's nominal-lossy collapse but inverts
the moment #199b lands the nominal layer, so if >=2 outer variants
structurally match the source we hard-error at compile time citing #199b —
both stages, an enforced invariant rather than a "rare, trust it" assumption.
Folds #219: the wwstage tinfo typeeq (lib/ww/typ.ww) had no TY_TAGGED branch
and fell through to `return true` (any two tagged unions compared equal);
cstage type_eq (type.c:269) has the structural branch. The structural fallback
above is the first and only caller to compare two bare tagged unions, so #219
is unexercised — and therefore ungateable — in isolation; it folds here per
the rule-11 couldn't-split carve-out (same structural reason as #206's
N_TTUPLE fold). The added branch mirrors cstage type_eq, tightening wwstage
into alignment.
test/wcc/925_nested_union_widen_run: outer-arm select, destructure-after-
propagation (payload survives the round-trip), destructure-let, single-variant
control, and the collision-guard compile-error, each with a cstage==wwstage
byte-id check (the path is gate-blind). Interim until #199b/B-full lands the
true nominal wrapped-slot layout.
wwstage cgtryprop returned the operand union's RAW tag when propagating a
`c(s)?` error, while cstage (cmd/w6c/cgen.c:6161-6184) remaps it to the
enclosing return union's variant ordering. When the operand and return
unions differ in variant order, wwstage propagated the WRONG error variant
at runtime — gate-blind: byte-id (990-997) and cstage==wwstage asm both pass
because the bootstrap only ever tries same-order unions, while the
differing-order case is silently wrong.
Port cstage's remap loop into cgtryprop (iserror-only): for each error
variant whose return-union index differs, emit the CMPQ/JNE/MOVQ/JMP that
rewrites the tag in AX; the error payload words (DX/CX/R8) are untouched and
ride the RET. Mirror cstage's emission exactly — lazy tryprop_ret allocation
on the first remap, j==i skip, j<0 fallback, no dead label when empty,
identical label strings and operand order — so same-order emits zero extra
instructions (byte-id preserved) and differing-order is now byte-identical
cstage==wwstage.
Scope: error-variant remap only. wwstage's hardcoded success-tag=0 and
iserror-only error detection (vs cstage's cg_tagged_success_tag +
cg_variant_is_error legacy fallback) diverge for non-idx-0-success or
unmarked unions — also gate-blind, also latent — filed separately as #216.
test/wcc/925_tryprop_tag_remap_run: 5 rows (differing-order for both error
variants, success unwrap, same-order byte-id witness, and a multi-word !str
payload row asserting the payload bytes survive the remap), each with a
cstage==wwstage .s byte-id check.
A bare `&fn_name` was not assignable into a `*reader` / `(*reader | void)`
vtable field without an explicit cast: cstage type_eq on TY_NAMED is
pointer-identity, so a structural `*fn(...)` referent never matched the named
`*reader` variant; wwstage accepted it only via an accidental catch-all
leniency. harec accepts bare &fn through hint-directed alias adoption at the
address-of site (check.c:3594-3626) while keeping pointer assignability
strictly nominal (types.c:1039-1066), so a materialized `*fn` value never
launders across alias names.
Mirror that decision without threading a type hint through the bottom-up
cexpr: keep type_assignable / isassignable fully nominal, and add a
caller-site helper (assignable_addrfn) at the assignment boundaries
(let-init, struct-literal field-init, assign, return, call-arg, array
element) that accepts iff the rhs is a DIRECT &-of-fn-ident and the
destination (or exactly one tagged variant) is a pointer-to-fn-alias whose
underlying fn signature structurally matches. A materialized `*fn` value, a
distinct same-signature alias, and an ambiguous multi-variant target all stay
rejected. Both stages share the rule; wwstage's lenient pointer-fn punt
becomes a confident reject. ww has no methods, so a `value.leaf` slot is only
ever a fn-pointer field and this never over-admits.
The tightening surfaced a wwstage typeeqast gap: a TY_FN result that is a
tuple (`*fn(...)(i32,i32)`) compared false where cstage type_eq handled it,
newly rejecting a legitimate structural assign. Add the N_TTUPLE structural
case (rule-10), restoring test 766.
cgen-neutral (the cast was a no-op reinterpret); pre/post bootstrap .s
zero-delta. Test 783 covers the positive paths (incl. a byte-id-clean
three-field-vtable dispatcher) and the negatives. Tagged-slot negatives
(ambiguous / tagged-laundering) are rejected on cstage but wwstage's separate
`(X|void)` void-variant leniency (#214) still admits them; 783 pins them
cstage-only, to graduate when #214 closes (required before wwstage becomes
the authoritative selfhost checker).
Note: `make clean && make test` is RED at HEAD on 4 alloc fixtures
(700/748/758/915) via a pre-existing clean-build defect (#215, malloc vs
rt_malloc); identical with or without this change, so bisect-clean for #206.
wwstage exprtype's N_CALL arm fell into a global-leaf scopelookup for an
N_DOT callee with a value or chained receiver, binding whatever same-named
global headed the scope bucket. Under a late-os combined.ww concat order
this resolved io's `s.read(...)` to os.read (i64) instead of the field's
fn type, so checkretassign confidently rejected a valid tagged return — an
import-order-sensitive false positive. cstage resolves a call result solely
from the callee expr's own type (check.c:1378-1433, mirroring harec
check_autodereference 1566-1581); drop the global-leaf else-arm so value
and chained receivers fall through to the existing fn-VALUE path at
check.ww:2455. SK_USE module-qualified calls are unchanged.
ww has no methods, so `value.leaf()` is only ever a fn-ptr field access; the
global hit was never legitimate. Zero .s delta across all 5 bootstrap tools
(the branch is dead in the bootstrap); test 776 graduates to both stages
(os-late order, byte-identical).
The fix unmasks a pre-existing wwstage cgen bug (#211): cgen also re-derives
a call's return shape by name (fnretlookup), so a value-receiver field call
whose leaf collides with a same-named global of a different register shape
mis-resolves cs!=ww. Documented at the cgen site; pinned cstage-only by
test/wcc/782 (graduates to STAGE_WW on #211 close).
V had vfprint / vfprintf but no compositions over them, so callers
needing the newline / printf-newline / bounded-buffer / heap-grow
shapes still routed through the OLD io.stream-shaped fprintln /
fprintfln / bsprintf / asprintf. Port the four compositions into
vstream.ww as the v* twins: vfprintln + vfprintfln chain a
"\n" vputbytes after the underlying primitive; vbsprintf threads a
caller buffer through memio.fixed_vstream and returns the prefix view;
vasprintf grows through memio.dynamic_vstream and shrink-copies to a
tight allocation before io.st_close.
Bundles the two memio enablers (fixed_string / dynamic_string in
lib/memio/vstream.ww) that vbsprintf / vasprintf depend on directly,
per drew-approved exception to one-class-one-commit
(feedback_refactor_routing_same_class_drops applies — helpers are
direct prereqs, not unrelated churn; the bus-routing site lives in
v* fmt code, not in memio). They mirror OLD memio.string (memio.ww:
102) over the per-flavour *fixed_ctx / *dynamic_ctx intrusive cast,
same shape as the read/write callback split at memio/vstream.ww:144.
Mirror sites:
vfprintln fmt.ww:240 fprintln ref/hare/fmt/wrappers.ha:48
vfprintfln fmt.ww:740 fprintfln ref/hare/fmt/wrappers.ha:69
vbsprintf fmt.ww:839 bsprintf ref/hare/fmt/wrappers.ha:42
vasprintf fmt.ww:873 asprintf ref/hare/fmt/wrappers.ha:29
Divergence vs Hare on vbsprintf: Hare returns `(const str | nomem)`;
ww collapses to `(str | io.error)` so the underlying vfprintf io.error
arm stays uniform. The fixed_vstream nomem widens into io.error
explicitly (no `memio.fixed_vstream(buf)?`) because #173 (TRY-on-
tagged-return both-stages broken) is still open — same shape memio/
vstream.ww adopted at line 87-99 for fixed_vstream itself. vasprintf
keeps OLD's bare `str` return (no nomem variant on public surface).
ken cs==ww mechanical: additive only, both stages compile identically.
fmt is NOT embedded in any selfhost main.combined.ww (grep verified
pre-impl: zero `^package fmt;` hits in selfhost/cmd/*/main.combined.
ww). memio.vstream.ww IS embedded in w6c + wwdump combined.ww (lib/
ww/cgen.ww uses memio.dynamic for buffer growth); the two memio
helpers regen-and-commit via ww build per #110 SSoT.
test/wcc/781_fmt_vstream_compositions_run.c (cstage-only per #209): 4
rows pin all four V wrappers — fdprintln_v_run_basic (newline shape),
fdprintfln_v_run_fmt ({n}-placeholder + newline), bsprintf_v_basic
(fixed buffer + returned view + caller bytes), asprintf_v_basic
(owned heap str + os.free roundtrip). Mirror of 777/780 cstage carve-
out (#209 wwstage formattable match-arm bail). Byte-id graduates with
#209 close. 214 total tests green (was 213).
V's vfprintf parsed mods via scanmods but dropped them after parse —
vformatfield routed straight to vwriteone (no width / alignment / pad
/ sign / base / prec honoured). Port the OLD modifier path (fmt.ww:
443-641 rawlen* / formatraw / formatone) into vstream.ww as the v*
twins, widen vformatfield to take *mods, and pass &m through vfprintf
at the call site.
The v* helpers mirror OLD verbatim (compute body identical; vputbytes
+ (size | io.error) routing replacing putbytes + (i32 | io.closed));
shared compute helpers (signof / digitsu64 / basenum) and modifier
enums (neg / alignment / mods) are reused directly from fmt.ww via
package scope. fmt.ww UNCHANGED — fold-eFinal (#50) collapses both
surfaces and dedupes the rawlen-family.
drew NaN/Inf signoff: strconv.f64tos / f32tos already render
"nan"/"infinity" with no leading '-', so the sign-peel in vrawlenf64
+ vformatraw f64 arm is a no-op on those views (same OLD path at
fmt.ww:520-562).
ken cs==ww mechanical: both stages compile the new V-side identically;
990-997 byte-id gates + combined_ww_fresh stay green (fmt is not
embedded in any selfhost main.combined.ww — grep verified pre-impl).
test/wcc/780_fmt_vstream_mods_run.c (cstage-only per #209): 5 rows
covering width / precision / base_hex / sign_plus / zero_pad. STAGE_WW
blocked by #209 (wwstage formattable match-arm bail), same carve-out
as 777_fmt_vstream_run.
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).
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.
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.
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).
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.
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.
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.
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).
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)
Fix value-loss bug introduced as a #122 boundary in the float
arr[i]=v store: when the index sub-expr clobbers X0 (e.g.
`a[geti()]=1.5f32`), the value is lost. Mirror the scalar-deref
X0-spill template (cstage cgen.c:4187; line shifted from the brief's
stale :3859 cite by intervening #133/#135/#138 commits): for float
element only, replace PUSHQ AX (junk for floats — value is in X0)
with SUBQ $8,SP + MOVSS/MOVSD X0,(SP) before the idx/base eval;
mirror replace POPQ AX with MOVSS/MOVSD (SP),X0 + ADDQ $8,SP after.
Wwstage parallel. Non-float keeps PUSHQ/POPQ AX so the str/slice
3-word {ptr,len,cap} pop order at the end of the branch is preserved.
#122 trailing-store comment updated from "Deferred to #125" to a
positive cite.
Test 916: 5 rows — f64_call_index + f32_call_index canonical repros
(geti's body clobbers X0; pre-fix exit=2 from post-call residue,
post-fix exit=1 from the spilled 1.5) + f64_lit_index / _localvar /
_arith control rows for non-X0-clobbering index paths. f32_call_index
uses an int-arg call to dodge the sibling cs/ww f32-arg-push
MOVSD-vs-MOVSS divergence (#143, task #36 — orthogonal, filed).
Bootstrap NEUTRAL (zero current float arr[i]= callers in lib; only
[N]u8 byte-buffers like f64tos_buf). cs==ww byte-identical both
stages (990-997 + 916 inline cmp). Closes the #122 boundary-doc
loose end; completes the #122 family.
Fix segfault-class memory corruption on `module.array[i]` indexed-read
where both stages emitted MOVQ-not-LEAQ on the module-qualified base
plus wrong stride. Extends the #135 cg_dotbase_addr/dotbaseaddr helper
to handle the SK_USE module-ident-base case: when bt is NULL/ty_err
and let_islet(base.str) resolves to TY_ARRAY, emit LEAQ base(SB),dst
instead of MOVQ. Wwstage parallel via letvartnode/N_TARRAY check.
Stride fix via let_var_type fallback in cgindex when n.lhs.kind==N_DOT.
Use-site fix per #135 precedent (Option B); preserves cgdot's MOVQ
semantics for the whole-array-assign defensive case (zero current
consumers). Test 915 carries 3 module-u16 indexed-read rows
(strconv.left_shift_table[0/2/4]) + 2 local-array controls; the
strconv.left_shift_table[2]:u32 probe segfaulted (exit 139) pre-fix
and exits cleanly post-fix. Broader width-variation rows (u8/u32/i32
module-imported) deferred as informational enhancement. Test 915
skips its inline cs==ww .s cmp on needs_import rows (line 217-222)
since `ww build` only drives cstage; reviewer externally verified
byte-id on /tmp/k128probe.combined.ww (driver-expanded form, no
imports). Future enhancement: 915 could read the driver-emitted
combined.ww and add a cmp leg there.
Bootstrap NEUTRAL (zero current module.array[i] consumers; strconv
decimal.ww uses IDENT-base from within package). 178/178 incl.
990-997 + combined_ww_fresh green. Sibling bugs #137 (chained N_DOT)
/ #141 (variadic-gather esz==2) / #142 (wwstage primsize-on-alias)
properly deferred to backlog.