Commit Graph

530 Commits

Author SHA1 Message Date
66a91c8969 wcc: converge let-IDENT memcpy, IDENT-assign recv, nested struct call-recv onto structabisize
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.
2026-05-28 14:07:58 +09:00
39f9267bc9 wcc: converge DOT-recv, structlit-fill, and bare-let zero-init onto structabisize
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.
2026-05-28 13:02:47 +09:00
d92c199d25 wcc: retire exprfloatkind, read the checker stamp directly
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.
2026-05-28 11:35:45 +09:00
719893743e wcc: arm asserttyped bail — a nil-typed value node is now fatal
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.
2026-05-28 10:40:08 +09:00
f91d93e827 test: 990 standalone probes feed resolved units (asserttyped-bail precondition)
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.
2026-05-28 09:55:45 +09:00
f892a91223 wcc: stamp for-range tuple-binds and massign blank via shared helper
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.
2026-05-28 09:06:54 +09:00
3252bd1709 wcc: stamp computed enum-member value-exprs
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.
2026-05-28 08:07:19 +09:00
360b58b267 wcc: stamp fn-ptr-value call results from the callee fn-type
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).
2026-05-28 06:28:59 +09:00
884dbb402b wcc: dot-lhs prefers SK_USE module over same-leaf type/fn name
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).
2026-05-28 05:17:28 +09:00
8161b19de9 test: ww-stage asserttyped gap-corpus probe (901)
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.
2026-05-28 04:27:18 +09:00
ea1579a6fd wcc: exprtype resolves SK_USE module-qual N_DOT call results (#16, #17)
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.
2026-05-28 03:55:22 +09:00
c882bcf27c wcc: struct-return float fields via SSE return regs (#171a)
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.
2026-05-28 01:50:28 +09:00
d4e500f61c wcc: struct ABI size maxalign-rounded via structabisize (#169)
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.
2026-05-28 01:02:41 +09:00
a917533fbf wcc: struct-param float fields via SSE arg regs (#165)
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.
2026-05-27 23:31:19 +09:00
0465c423c1 wcc: tuple-param ABI via SSE/GP arg cursors (#163)
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.
2026-05-27 22:13:46 +09:00
153c7b3b46 wcc: multi-float tuple return via SSE cursor (#164, #107)
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).
2026-05-27 21:01:25 +09:00
028109513e lib/strconv: retire workarounds in f32todecf32 (#168)
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.
2026-05-27 19:29:46 +09:00
393ac1a882 wcc: nodeisunsigned N_CALL reads stamped type_ (#168)
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.
2026-05-27 18:43:08 +09:00
0e72556120 lib/strconv: f32tos Ryū shortest float→string (#106 fold-5b)
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.
2026-05-27 17:18:27 +09:00
aff7725f8f w6c: f32 arg-push/pop uses MOVSS not MOVSD (#143)
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).
2026-05-27 16:17:24 +09:00
0e66073e32 lib/strconv: f64tos Ryū shortest float→string (#106 fold-5a)
Graduate f64tos from the lossy fixed-point placeholder to Ryū shortest-
round-trippable (ftos.ha:432 + ftos_ryu.ha). f64tos(n:f64) str, G-format
(void/NONE) — the faithful documented subset (full parametric fftosf is
dead code for G/void/NONE → deferred #64). Ryū core decomposed: struct-
RETURN + scalar params (Hare's r128 idiom; avoids tuple-ABI #163-166).
f64 powers tables [15][2]/[13][2]u64 (2D #156). Zero float literals.
Both E+F encode paths reachable+tested, no dead code.

Graduation (lib-note "don't keep both"): old lossy f64tos deleted; fmt
fprintf_f64_huge "huge"→"9.5e18" (improvement). 5 value-faithful filed-
bug dodges (byte-id, documented): #167/#169/#170/#43/#144. Test 908.
Make test 186/186 incl 990-997 byte-id + combined_ww_fresh.

Drew's strconv 5-fold plan — primary completion (f64tos). f32tos coda
#67 (behind #143); parametric ftosf #64.
2026-05-27 15:39:03 +09:00
81796cd533 lib/strconv: stof.ha port — Eisel-Lemire string→float (#106 fold-4)
stof64/stof32 (f64|f32 | invalid | overflow) via Eisel-Lemire fast-path
(powers_of_ten[596][2]u64 + eisel_lemire 128-bit multiply) + decimal
slow-path fallback (decimal.ww, fold-3). 16 fns + faithful powers_of_ten
(byte-identical to Hare). u128 via pure-u64 64×64→128 (ftos_ryu.ha).
Consumes &math.f64info (γ-cleanup), tagged-float-return (PREREQ-2 #157),
2D double-index (PREREQ-1 #156).

13 documented spelling-divergences (rule-9, each cites stof.ha): #155
(po10 double-index + per-field struct-copy), #161 (compound-assign explicit
form), #144 (-0.0 via 1u64<<63), #158, #138, test-only #143/parsef64.
Test 909 (DEC+hex+NaN/Inf/invalid/overflow, bit-exact, cstage ww run).
Make test 185/185 incl 990-997 byte-id + combined_ww_fresh. Makefile:
stof.ww added to w6c_ww/wwdump_ww deps (freshness, fold-3 precedent).

Drew's strconv 5-fold plan 4/5. Followup #162 (wwstage lexer parsef64
1-ULP — could adopt stof64).
2026-05-27 13:55:56 +09:00
4a91bdc8db wcc: tagged float-variant return packs float bits via X0-spill (#157)
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.
2026-05-27 13:04:02 +09:00
cbeffea7d8 wcc: 2D array [N][M]T static-init + double-index read (#156, A.3 capstone)
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).
2026-05-27 12:21:15 +09:00
88f3d67b28 lib/math: re-fold F64_EXPBIAS to const f64info/f32info struct (#40)
Restore the Hare-faithful floatinfo struct defs (ref/hare/math/floats.ha
:117,126), removing the Drew-flattened-(b) #129-era bypass (F64_EXPBIAS:int
scalar). #149 (db7523e) now lowers cross-module &def to LEAQ, so the struct
exports are addressable via the stof consumer pattern (&math.f64info ->
f: *floatinfo -> f.expbias). First real consumer of A.2 struct-composite
static-init (DATA byte-validated: f64info/f32info 40B each).

ADD export def f64info/f32info: floatinfo (hex masks value-identical to
Hare's (1<<52)-1 etc.; A.2 helper folds bare literals only, documented
at-site). DELETE export def F64_EXPBIAS (zero consumers). KEEP NAN_BITS/
INF_BITS u64 sentinels (ruling b: def NAN=0.0/0.0 blocked by #147) +
F64_EXPONENT_BIAS:u64 (bit-ops alias, Hare keeps both).

Test 952: pointer-param row reads &math.f64info + &math.f32info through
*math.floatinfo, all 5 fields each (pointer-param not direct field-read,
dodges #150). Make test 184/184 incl 990-997 byte-id + combined_ww_fresh.
lib/math not compiler-imported -> no regen.

Unblocks fold-4 stof.ha.
2026-05-27 10:55:31 +09:00
db7523e0e7 wcc: address-of symbol-bearing def + cross-module module-qual (#149)
Widen cgaddr / N_UN TK_AMP for addressable globals — the address-of
twin of A.2/A.3's value-LOAD widening (which covered cgexpr N_DOT /
cgindex but never the &-path, so &<mod>.<def> emitted uninitialized-AX
garbage). Two shapes, one class:
- Shape 1 (&G, N_IDENT def): LEAQ masym(G) for struct/array/scalar
  defs. Scalar gate = emit_defs/emit_floatlit_data eligibility exactly
  (fold_int_literal OR rhs-peels-to-N_FLOATLIT) so the addressable set
  equals the symbol-bearing set — never LEAQs a missing symbol. Reuses
  DefStruct/DefArray registries (A.2/A.3); new def_isscalardef/DefAny.
- Shape 2 (&mod.G, N_DOT module-qual): LEAQ leaf (TY_FN -> mafn),
  mirroring the value-READ resolution (cgen.c:6043). Kind-agnostic —
  fixes &mod.def, &mod.let, &mod.scalar, &mod.func. This is fold-4's
  &math.f64info pattern.
Non-addressable def (str-def, computed-float-def) -> loud error both
stages; upgrades #147 &NAN(0.0/0.0) from silent wild-deref to loud
compile error. Computed-float-def symbol emission split to #147
(needs float-const-fold; not needed by gamma/fold-4). wwstage Shape-2
gate carries !deflookup so a def base routes to silent-drop matching
cstage's type-gate (rule-10 parity).

Unblocks gamma-cleanup (#40) + strconv fold-4 (&math.f64info).
Bootstrap-NEUTRAL (no &global in lib/selfhost; only &local). Test 921
(12 rows: same-pkg struct/array/scalar-int/scalar-float + cross-pkg
def_struct/let_struct/scalar_let/func + regression &let/&arr[i] +
2 loud-reject). Make test: 184/184 incl 990-997 byte-id +
combined_ww_fresh.

Followups: #147 (computed-float-def symbol), #152/#153 (pre-existing
ptr-param element read divergences, dodged by 921), #154 (address-of
def subparts &def.field/&def_array[i]).
2026-05-27 10:27:55 +09:00
0546bda6c4 wcc: array-init accept-if-fits coercion (#130, merges #146)
Align bare-int array-init assignability to Hare's literal-fits rule
(ref/harec/src/types.c promote_flexible): accept untyped-int array
elements that FIT the element type, reject out-of-range loud. cstage
(rejected all bare-int arrays, over-strict) and wwstage (accepted +
silently truncated out-of-range, over-loose) converge to the same
accept-if-fits rule. Per-element: foldable int literal range-checked
against element type [min,max] via def_cast_fits (rule-13 type-table
widths); non-foldable element falls back to type_assignable.

cstage: new arrlit_init_fits, N_LET decl-check fallback after whole-
array type_assignable fails. wwstage: checkletassign array branch +
route top-level lets through checkletassign (were unchecked — only
function-body lets ran assignability; closes #146 wwstage str->u8
over-accept).

Scalar-init range-check (let X:u8=300 truncates, both stages,
pre-existing) deferred to #148 — language-wide, needs bootstrap audit
+ explicit-cast conversion. def-array accept-if-fits deferred to #151
(def constfold machinery, different risk). Both bootstrap-NEUTRAL.

Test 920 (14 rows — accept: in-range u8/u32/u64/i32 + u8/i8 boundary +
typed regression + non-foldable-body; reject: over-range + over-256 +
i8-over + neg-for-unsigned + str->u8 + non-foldable-wider). Non-
foldable else-branch cs==ww verified (matching-type accept + byte-id;
wider-runtime-int reject both stages). Make test: 183/183 incl 990-997
byte-id + combined_ww_fresh.
2026-05-27 09:18:03 +09:00
9e3bc4ea37 wcc: array static-init let/def DATA emit via SSoT helper (#129 A.3)
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)
2026-05-27 05:56:24 +09:00
0ed0b3933c wcc: struct-composite let/def DATA emit via SSoT helper (#129 A.2)
Extract emit_struct_data + emit_struct_lit_bytes helpers (both stages,
mirrored) for module-level let/def with N_STRUCTLIT initializer. Walks
Tfield linked-list in declaration order, zero-fills padding via per-
field offset (rule 13, no hardcoded sizes), dispatches per field kind:
integer via fold_int_literal, float via inline bitcast + sign-XOR byte-
loop (A.1 shape, no INT64_MIN materialised — sibling #144), nested
struct via recursion (#145 inner-field-name-leak gates the test row).
Out-of-scope field kinds (str/slice/ptr/array) fatal loud per rule 7.

LOAD-side widened symmetric to A.1 precedent: cstage cgexpr N_DOT
direct-struct-ident + chained-N_DOT widened via new DefStruct registry
(def_isstructdef populated in let_collect); wwstage cgdot direct-struct-
global falls through to defvarstructinfo on letvarstructinfo nil
(defent.dtnode field added, populated in collectdefs). Both stages
materialise struct-def via LEAQ name(SB) same as struct-let.

Pre-existing cstage scalar 8B short-circuit at emit_lets caused silent
fold-fail-continue on 8B struct lits (`struct{i32,i32}`); gate now
excludes let_isstruct so 8B struct lits route through emit_struct_data.
Wwstage's `!issg` gate was already correct; symmetric ordering restored.

Closes (all bootstrap-NEUTRAL pre-impl; γ-cleanup #40 first consumer):
- emit_lets `is_struct continue` skip → struct lets emitted no DATA
- emit_defs no struct arm → struct defs emitted no DATA
- cstage cgexpr N_DOT for struct-def emitted MOVSXD (BP), AX (broken
  stack-frame read)
- cstage emit_lets sz==8 short-circuit silently skipped 8B struct lits

Test 918 (7 rows: let_int_struct / def_int_struct / let_float_field /
def_float_field / let_empty_struct / let_int_struct_8b / let_norhs_
struct_regression) registered. Make test: 181/181 incl. 990-997 byte-id
+ combined_ww_fresh.

Followups filed:
- #145 (task #41) — nested struct-lit inner field-name leaks as extern
- #42 — wwstage dotchainresolve missing defvarstructinfo lookup (A.2-
  scope-clean today; surfaces post-#145 nested-struct shapes)
- A.3 (task #39) — array static-init audit (parks shape-4 array-in-struct)
- γ-cleanup (task #40) — lib/math const-floatinfo re-fold, blocked-by A.2
2026-05-27 05:04:05 +09:00
1f8fcdc0ee wcc: float-typed def/let DATA emit via SSoT helper (#129 A.1)
Extract emit_floatlit_data helper for def/let with float-typed top-level
initializer; replaces inline emit_lets float arm and adds previously-
absent emit_defs float arm. Helper peels N_CAST then N_UN(±, N_FLOATLIT),
bit-casts magnitude (f32 via union narrow), emits in little-endian byte
order, applies sign-XOR to top byte inside the loop (bit 31 for f32, bit
63 for f64). The byte-loop XOR avoids materialising 2^63, sidestepping
the strconv.i64tos INT64_MIN bug (#144 / task #37) on the wwstage self-
build path. Mirrored cstage (cgen.c) and wwstage (cgen.ww). Both stages'
float-typed def materialisation (cgexpr N_IDENT / cgident def-branch)
widened to route through the same LEAQ+MOVSS/MOVSD shape as float-typed
let.

Closes 3 latent bugs (all bootstrap-NEUTRAL, no current consumer):
- def: f64 = literal silently emitted undefined ref
- let: f64 = -literal silently emitted undefined ref (N_UN peel absent)
- wwstage let: f32 = literal silently truncated to low 4 of f64 bits

Test 917 (7 rows: f64_def_pos / f64_def_neg / f32_def_pos / f32_def_neg
matrix-closure / f64_let_neg / f64_let_pos / f32_let_pos) registered.
Make test: 180/180 incl. 990-997 byte-id + combined_ww_fresh + 995_self_
rebuild.

#144 (strconv.i64tos INT64_MIN two's-complement-overflow root) filed
separately as task #37 for its own fold.
2026-05-27 04:19:37 +09:00
61e6aab384 wcc: float arr[i]= spills X0 across index eval (#125)
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.
2026-05-27 03:30:15 +09:00
1de1b9a8a9 wcc: module-imported array indexed-load via cg_dotbase_addr (#128b)
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.
2026-05-27 03:07:49 +09:00
7230f3aa61 wcc: cstage array-init dispatches MOVW for esz==2 (#128a)
cstage cgen.c array-literal init dispatch now uses MOVW for esz==2
(u16/i16 element width). Was deferred (cgen.c:7194-7198 explicit
TODO: "Add MOVW to w6a if real i16 arrays land") until A_MOVW
landed in both stages' w6a; that prereq is now met. Fixes silent
partial-init clobber where MOVQ writes 8B over a 2B slot,
overwriting neighbouring elements/locals.

Wwstage was already correct (selfhost/cmd/wcc/cgenutil.ww:872-877
emits MOVW for sz==2 in tnodestoreop) — cstage aligns UP to
wwstage's correctness here, a rule-10 inversion from the usual
align-richer-DOWN.

Test 914 (4 rows: u16 full-init, u16 small-values, u8 control,
i16 signed) catches the bug via the rule-10 cs==ww byte-id gate.
Runtime is not a reliable lever — ww rejects truly-partial inits,
and fully-init [N]u16 accident-corrects via MOVQ-overlap (each
write rewrote the prior write's trailing 6B). Reviewer non-vacuity:
stash the fix → 3/4 rows fail on byte-id, restore → 4/4 green.

Bootstrap NEUTRAL: 990-997 byte-id + combined_ww_fresh green; zero
pre-existing partial-init narrow-element callers in lib/+selfhost/.
strconv stof_data tables emit DATAW (raw bytes) and bypass this
path, which is why fold-2 landed clean despite the bug.

Sibling bugs filed for backlog (reviewer-128a flag-don't-bundle per
rule-11): #141 (cgen.c:4894-4898 variadic-gather array-store has
the same dispatch gap) and #142 (wwstage cgenstmt.ww:976-990
primsize(elemn.str) returns 0 for TY_NAMED alias names → wrong-
stride store on [N]alias-of-u16; cstage already TY_NAMED-peeled).
2026-05-27 02:32:31 +09:00
684f59c48c lib/strconv: decimal.ww header documents i_sz/lowbit_lit hoist sub-cases (#32 c2)
Add a sub-bullet to the 8-spelling-divergences list documenting two
implementation sub-cases reviewer-fold3 surfaced during the 07e57ff
decimal.ha port: (1) `i_sz` per-iteration size-cast hoist inside
leftshift_newdigits' for-loop (decimal.ww:93); (2) `lowbit_lit`
stepwise boolean decomposition in should_round_up (decimal.ww:242)
dodging ww parser precedence on Hare's `(nd > 0 && d.digits[nd - 1]
& 1 != 0)` (ref/hare/strconv/decimal.ha:158). Both are in-file
instances of the documented hoist+restructure patterns — rule-9
doc-completeness, not new divergence. Combined.ww regen for
lib/strconv (compiler-imported into w6c + wwdump + smoke) uses the
build's include paths (`-I lib/ww -I lib/ww/lex -I lib/ww/parse
-I selfhost/cmd/wcc`) for transitive import closure; bare `ww build`
without these flags produces truncated output (reviewer-32c2 +
reviewer-fold3 both hit this).
2026-05-27 02:03:04 +09:00
bdfb5cda58 wcc: route DATA-emit through emitsymname mangler (#127)
wwstage cgen.ww emitdefconstants now uses the same emitsymname
mangler that LOAD/CALL sites use, replacing 8 lines of duplicate
`d.exported`/`d.nmod` logic. Rule-12 sea-of-stars consolidation —
one path, not two parallel paths that can desynchronise.

Cstage twin: cmd/w6c/cgen.c:8510 (mod_mangle in emit_defs). Bootstrap-
neutral post-90d31c5 (the duplicate PATH_MAX def that motivated the
divergence was cleaned up in drew's source-hygiene fold); all 5 tool
combined.ww emit cs==ww byte-identical asm post-fix. New test 913
(4 rows: exported i64 def, main-local i64 def, u64 width, multi-def
sequence) pins the simple-shape invariant forward — a future caller
introducing a colliding name produces the same symbol from both
stages by construction.

Drew's (a) ruling. Reviewer-127 noted the new path additionally
consults FFI (ffiresolve) which the old d.exported/d.nmod block did
not — incidental improvement to cs/ww symmetry beyond the mod-mangle
consolidation.
2026-05-27 01:51:12 +09:00
13441c5e2e w6a, wcc: add A_SARQ for signed arithmetic right-shift (#136)
Add SAR/SARQ to both assemblers' opcode tables (cstage cmd/w6a +
wwstage selfhost/cmd/w6a) — REX.W + D3 /7, parallel to SHR's D3 /5.
Encoding is the unary-on-CL form (SAR r/m64, CL), the only variant
the cgen emits today. cstage cgen + wwstage cgen sweep all 12 SHRQ
emission sites (6 per stage) so signed RSHIFT and signed RSHIFTEQ
route through SARQ (arithmetic, sign-extends MSB) instead of SHRQ
(logical, zero-fill). Pre-fix `let i: i32 = -200; i >>= 2;`
produced 0x3FFFFFCE (1073741774) instead of -50; cs==ww held because
BOTH stages emitted SHRQ, so the 990-997 byte-id gates were
gate-blind to this silent miscompile.

Sites covered (per stage 6, same shape in both):
  - plain TK_RSHIFT (cgbin / N_BIN ordered binop) — derives unsignd
    from operand types via type_isunsigned / nodeisunsigned, picks
    SHRQ vs SARQ at emit
  - chained-ptr-field compound RSHIFTEQ (cgen.c:3281-3317 area)
  - N_INDEX-lhs compound RSHIFTEQ (#133-expanded N_INDEX site)
  - deref-target compound RSHIFTEQ
  - top-level let compound RSHIFTEQ
  - IDENT-local compound RSHIFTEQ
All sites reuse the in-scope unsignd variable from the surrounding
SLASHEQ block (or derive one locally when not available). LSHIFTEQ
unchanged — SHL == SAL at the encoder, no signedness dispatch needed.

912_sar_shr_run: 5 rows. i32_neg_rshifteq (lead's repro, was wrong
1073741774 → now -50), i64_neg_rshifteq (wider type), i32_pos_
rshifteq (positive control, SARQ ≡ SHRQ on positives, no regression),
u32_rshifteq (unsigned control, still SHRQ), i32_neg_rshift_binop
(plain >> not compound, cgbin TK_RSHIFT site). Exit codes use small
absolute values with u8 wrap (-50 = 206) per Unix 8-bit exit.

Bootstrap-NEUTRAL — `grep -rE '>>=|>>\b'` in lib/+selfhost/ (excl.
combined.ww) returned zero callers of signed RSHIFT today; the only
asm shifts are on previously-broken paths. 990-997 + combined_ww_
fresh stay green. Closes the silent-misbehavior class on signed
right-shift across all 12 cgen emission paths in one fold per
rule-11. Foundation for Eisel-Lemire (strconv fold-4) big-int signed
shifts.
2026-05-27 01:29:24 +09:00
90d31c5b41 cmd/ww: drop duplicate PATH_MAX def; use os.PATH_MAX
main.ww was declaring `def PATH_MAX: u64 = 4096u64;` alongside the
imported os module's `export def PATH_MAX: i32 = 4096;` (lib/os/os.ww:91).
The duplicate triggered the cstage mod_map last-write-wins attribution
bug (#127): cstage emitted `main.PATH_MAX` for os's DATA entry too,
producing two 4096-byte slots and a wrong-module symbol. Both stages
shipped working binaries because LOADs all routed to main.PATH_MAX(SB),
but the divergence was real.

Removing the duplicate closes the only known #127 consumer per rule-7;
the defensive cstage cgen fix stays filed (task #11) until a new
duplicate-def collision surfaces. PATH_MAX use-sites in main.ww cast
`os.PATH_MAX: u64` at point-of-use for alloc/arithmetic, and use the
bare i32 form directly for slice .len assigns (which expect i32).

No cgen surface touched; byte-id-neutral on the 990-997 gates.
2026-05-27 01:16:16 +09:00
07e57ff9a6 lib/strconv: decimal arbitrary-precision arithmetic (#106 fold-3)
Port ref/hare/strconv/decimal.ha (~202 LOC Hare) → 314 LOC
lib/strconv/decimal.ww — decimal struct + 11 fns (trim,
decimal_shift, leftshift, leftshift_newdigits, rightshift, round,
decimal_round, helpers). 1:1 mechanical Hare-fidelity with 8
documented spelling-divergences. Shared engine for stof (fold-4) +
ftos (fold-5). Built atop 5 wwstage cgen prereqs
(#131/#133-expanded/#134/#135/#138) that closed gate-blind silent
miscompiles surfaced by the port. Test 922_decimal_run +
lib/strconv/test/decimaltest.ww (6 @test fns covering all 11 impl
fns).
2026-05-27 00:44:16 +09:00
d960971c6e wcc: continue runs post-step in 3-clause for and range form (#138)
`for (init; cond; post) { ... continue; ... }` and `for (let i .. xs)
{ ... continue; ... }` now emit a `post` (3-clause) or `rpost` (range)
label between the body and the JMP back to the cond-test. `continue`
jumps to that label, runs the post-step, then re-tests the loop
condition — mirrors C/Go/Hare semantics. Pre-fix both stages emitted
`JMP loop_top` for continue, SKIPPING the post-step → the value that
triggered continue never advanced → silent infinite loop on the first
matching iteration. Found by impl-strconv-fold2 during the fold-3
decimal.ha port: `leftshift_newdigits`'s `for (... i+=1) { ... else
if (d.digits[i]==p5[i]) continue; ... }` would infinite-loop at the
first equal digit.

BOTH stages were identically buggy → 990-997 cs==ww byte-id held →
gate-blind. Bootstrap audit (`grep -rE 'for \(let .*\.\.' lib/
selfhost/`) confirmed zero existing callers with continue in either
the 3-clause or range form; bootstrap-NEUTRAL.

Sites: cmd/w6c/cgen.c N_FOR + N_FORRANGE; selfhost/cmd/wcc/
cgenstmt.ww cgfor + cgforrange. 1-clause `for (cond)` byte-id
preserved (cont_target stays = loop_top when n.rhs == nil). Rule-11
carve-out: 3-clause and range share the lowered structure; fixing
one without the other would leave the same silent miscompile in
N_FORRANGE — one-class closure on the continue-skips-post bug, same
precedent as #133-expanded.

911_continue_run: 4 rows. for3_skip_one (lead's repro, was infinite
loop, now 4), for3_skip_two (nested continues, 30), range_skip
(Hare-range continue, was infinite loop, now 120), for1_continue_
byteid (1-clause regression assertion — bootstrap shape unchanged).
Pre-existing parser-side divergences (cstage silently drops post in
the never-used 2-clause `for (cond; post)`; wwstage doesn't support
infinite `for {}`) deferred to #139 — not in decimal.ha, no shared
class with the cgen continue-skips-post.
2026-05-27 00:12:39 +09:00
ade6840610 wcc: N_DOT-base address arithmetic for [N]T-field index (#135)
Strategy (a) use-site fix: new helper cg_dotbase_addr (cstage) /
dotbaseaddr (wwstage) detects `base.kind == N_DOT` whose field type
is TY_ARRAY and emits the field's address inline — LEAQ inner_off+
field_off(BP) for a value-struct inner, MOVQ inner_off(BP),reg +
ADDQ field_off,reg for a *struct inner. The TY_ARRAY-only gate (after
TY_NAMED peel) keeps the helper INERT on TY_PTR/TY_SLICE/TY_STR/
TY_TAGGED field kinds where the existing cgexpr(base) path is
correct (loads pointer/header value, then adds scaled index). Wired
at 6 sites: cstage cgassign N_INDEX-lhs plain ASSIGN + #133 compound
arm + cgindex N_INDEX read fallback; wwstage twin × 3. Closes the
silent-segfault on `(*struct).array_field[i]` reads and writes —
pre-fix cgexpr on the N_DOT base auto-derefed and loaded the field's
first 8 bytes as if they were a pointer, faulting on packed [N]u8
arrays (small u64 → unmapped page).

Bootstrap-NEUTRAL: zero working callers in either direction pre-fix
(symmetric READ + WRITE segfault evidence). All corpus + 990-997
byte-id + combined_ww_fresh stay green post-fix.

949_dotbase_arr_run: 3 rows direct runtime + cs==ww byte-id (READ
u8, plain WRITE u8, compound WRITE u8). Wider element widths and
value-struct base / pointer-field-control rows deferred — blocked by
orthogonal pre-existing wwstage divergences (i32-return ABI MOVSXD
vs MOVL, uninit-struct-let zero-init asymmetry) documented in the
test body. The TY_ARRAY-gate no-over-fire is implicitly verified by
994/995 (corpus exercises thousands of struct.pointerfield[i]
shapes; any over-fire would shift bytes).

Chained N_DOT (`outer.inner.array[i]` depth ≥2) deferred to #137 —
confirmed not in ref/hare/strconv/decimal.ha or sibling strconv/.
Not a fold-3 blocker; helper bails (returns false) on chained shape,
caller falls back to existing cgexpr path.
2026-05-26 22:56:00 +09:00
3986818172 wcc: compound-assign load-op-store for indexed + chained-ptr-field lvalues (#133)
Both stages had silent miscompiles on compound assignment for two
shapes: indexed lvalue (`arr[i] OP= v`) and chained-pointer-field
(`d.fld.fld OP= v` through a *struct chain). The cstage N_INDEX-lhs
branch did not gate on TK_ASSIGN and silently DEMOTED compound ops to
plain stores (RHS stored, no load, no op). The wwstage equivalents
silently DROPPED the line entirely (no instructions emitted). The
chained-pointer-field compound template at cgen.c:3281-3317 also
silently identity-stored on unwired compound ops (SLASHEQ / PERCENTEQ /
LSHIFTEQ / RSHIFTEQ all fell to the switch default = no-op = load, pop
RHS, store ORIGINAL value back) and silently no-op'd on float / str /
slice / tagged element compound; its wwstage twin at cgenexpr.ww:5471
only handled TK_ASSIGN, dropping any chained-ptr-field compound
entirely.

Wire all 10 integer compound ops (PLUSEQ MINUSEQ STAREQ AMPEQ PIPEEQ
CARETEQ SLASHEQ PERCENTEQ LSHIFTEQ RSHIFTEQ) at all 4 sites in both
stages: SLASHEQ/PERCENTEQ via CQO+IDIVQ (signed) or zero-DX+DIVQ
(unsigned), with PERCENTEQ moving DX->AX for the result; LSHIFTEQ/
RSHIFTEQ via SHLQ/SHRQ on CX (rhs already in CX after the pop).
Signedness keyed off the field/element type via type_isunsigned /
typeisunsigned. Float / str / slice / tagged element compound now
LOUD-ERRORS at codegen with a distinct per-site diagnostic citing
#133/rule-7 instead of silent fall-through. Site 3 (the wwstage
chained-pointer-field compound) is ADDED FROM SCRATCH alongside the
existing TK_ASSIGN-only arm — pre-#133 wwstage emitted zero
instructions for any `d.i.v OP= v` shape, a rule-10 silent divergence
from the cstage which handled the same shape correctly.

Multi-fix carve-out (rule 11): the 10 wired ops at 4 sites + hard-error
gate on 4 unwired payload kinds at 4 sites are ONE silent-misbehavior
class closure on indexed/chained-ptr-field compound assignment.
Splitting would muddle bisect on related cgen surfaces — the wired
ops, the hard-error gate, and the rule-10 cstage/wwstage symmetry are
inseparable correctness facts at each site. The inherited template
default-break silent-identity (cgen.c:3281-3317) was the originating
class root; close it everywhere or leave the class open.

948_idx_compound_run: 21 rows total. 11 runtime+byte-id rows for the
original 6 ops on u8/i32/i64/u32 array bases and one slice base, with
a plain-assign control row asserting the ASSIGN path is byte-id-
unchanged. 7 new runtime+byte-id rows for SLASHEQ/PERCENTEQ on signed
i32 + unsigned u32, LSHIFTEQ on i32, RSHIFTEQ on signed-positive i32
and unsigned u32. 3 builderr rows (he_float_indexed, he_str_indexed,
he_float_chained_ptr) asserting both stages exit non-zero AND stderr
carries the cited diagnostic substring (rule-7 — never silent).
Mirrors 945_tuple_nary's builderr/experr pattern.

Bootstrap NEUTRAL — `grep -rE '\][[:space:]]*(\+=|-=|\*=|/=|&=|\|=|\^=|<<=|>>=)' lib/ selfhost/`
(excluding combined.ww) returns ZERO existing callers for the indexed
compound shape, and the chained-ptr-field compound shape was silent-
no-op in wwstage pre-fix (no working caller possible). 990-997 byte-
id gates green, 994 explicit confirms 18 corpus inputs identical
pre/post. combined.ww (w6c + wwdump) regen deterministic across
re-touch+rebuild.

A_SARQ is not in w6a's opcode table; signed RSHIFTEQ uses SHRQ at all
4 sites for parity with the pre-existing deref-lvalue compound site
(TK_RSHIFTEQ→A_SHRQ at cgen.c:4145). Documented technical debt
filed as #136 — pre-existing concern that a fix would need w6a
opcode addition + cgen sweep across every SHRQ-for-signed-RSHIFT
site, out of scope for this fold.
2026-05-26 22:27:32 +09:00
36bf60350c wcc: nodeisunsigned N_INDEX reads stamped type_ (#134)
wwstage cgenutil nodeisunsigned N_INDEX arm now reads
typeisunsigned(n.type_) directly, mirroring the N_DOT arm at line
1007 and cstage cgen.c:2541 which reads type_isunsigned on the
stamped operand. Embodies the #121 principle (collapse structural
onto stamp). Byte-id-neutral at master (no current N_INDEX-of-non-
IDENT-base unsigned compare sites in bootstrap); fix is for forward
consumers in strconv decimal.ha (>= 5u8 on d.digits[nd] with N_DOT
base) and similar Hare idioms. Closes #134.
2026-05-26 21:28:14 +09:00
4acab6e0bb wcc: cgen N_CALL "len" TY_ARRAY/TY_SLICE/TY_STR intercept (#131)
wwstage cgenexpr cgcall now intercepts the N_IDENT-callee `len` like
cstage cgen.c:4283-4297 — TY_ARRAY folds to MOVQ $alen,AX at compile
time, TY_SLICE/TY_STR + N_IDENT loads the .len slot from local header,
fallback to cgexpr. Rule-9 Hare-fidelity (Hare/Rust/Go compile-time-fold
len(fixedarray)) + rule-10 align wwstage UP to cstage. Byte-id-neutral
at master (bootstrap has no current len(fixedarray) call-form uses);
prereq for fold-3 decimal.ha port (`len(d.digits)` at decimal.ha:66/
77/86/124).
2026-05-26 20:39:03 +09:00
bb6f8406c7 lib/strconv: stof_data left_shift_table + pow5_table (#106 fold-2)
Port Hare's stof_data.ha tables: `let left_shift_table: [65]u16`
(decimal-expansion metadata for leftshift_newdigits) + `let pow5_table:
[0x051C]u8` (digits of 5^k for k=1..60). Cite ref/hare/strconv/
stof_data.ha. Literal-suffix init form (`0x0000u16`, `5u8`) — the only
form cstage and wwstage both accept (cstage rejects bare-int literals
in [N]u8 init as "not assignable", candidate #130). Module-level inits
emit DATAW (raw .data) so bypass candidate #128's runtime store-width
divergence. `powers_of_ten: [596][2]u64` (Eisel-Lemire fast-path)
deferred to consumer-driven port — only stof.ha references it.
Prerequisite for fold-3 (decimal.ha port) where leftshift_newdigits
consumes both tables.
2026-05-26 20:01:47 +09:00
e28d6b2e9d lib/math: NAN/INF bits + flattened F64_EXPBIAS (#106 fold-1b)
Flattened from Hare's const struct instances per #129 cgen module-let-
init gap; NAN/INF expressed as `def NAN_BITS:u64 = 0x...;` and
`def INF_BITS:u64 = 0x...;` materialized via f64frombits(NAN_BITS) at
use-site. F64_EXPBIAS:int flattened from f64info.expbias (cite
ref/hare/math/floats.ha:117); floatinfo struct definition preserved for
γ-cleanup re-fold when #129 closes. F32_EXPBIAS deferred (option-ii:
stof.ha consumers reach f.expbias via struct-field only).
2026-05-26 19:19:48 +09:00
e6beb566ea wcc: narrow integer pointer-deref load width via localloadop (#116)
TK_STAR integer arm now routes through localloadop (cstage cgen.c) /
localloadop (wwstage cgenexpr.ww) — load-twin of the landed signed-
narrow-scalar-reads fix, was omitting TK_STAR. Closes the *p (CMPQ,
full-width arith) miscompile family (#116 + 962/963 instances all
fixed by the same width-aware load). Float arm untouched (#96 already
routed via X0). New test 947 (10 rows): packed CMPQ + signed/unsigned
narrow widths + TY_NAMED/TBANG alias + TY_ENUM peel + i64/bool controls.
2026-05-26 19:14:14 +09:00
e9620953a0 lib/math: F32 family + f32bits/f32frombits + floatinfo struct (#106 fold-1a)
F32 width consts + bit converters mirror existing F64 family; floatinfo
struct definition (instances deferred to fold-1b per #129 cgen module-
let-init gap). Cite ref/hare/math/floats.ha; expbias: int per Drew.
2026-05-26 19:10:02 +09:00
0a2747ce5b wcc: collapse exprfloatkind structural body onto checker stamp (#121)
wwstage exprfloatkind now reads n.type_ via classifytinfo (typeisf32→1,
typeisfloat→2, else 0) — wwstage classifies float-ness from the cstage-
equal stamp; delete the 9-arm structural body + the transient bridge.

Permanent residual sibling-evidence guards: cgbin float-arith (primary,
fires on unstamped float operand), tupstore + cgwidentaggedstore float
arms (pins, unreachable today, contract assertions against future
regression). cgcast excluded (int→float CVTSI2SD legitimately nil-typed).

Closes the gate-blind cs≠ww float family root (STATUS:14, named (b)-phase
goal). Builds on 98e1665 (A-narrow N_IDENT destructure stamp) +
1c4cea4 (transient floatness-disagreement bridge).
2026-05-26 18:30:18 +09:00
1c4cea4e2c wcc: exprfloatkind structural/stamp floatness-disagreement bridge (#121)
Add classifytinfo(n.type_) stampkind alongside structural structkind;
abort on floatness-disagreement only ((structkind!=0)!=(stampkind!=0));
return structkind (byte-id-neutral). TRANSIENT — deleted in commit 3 (the
flip + permanent residual sibling-evidence guard). The corpus-wide
validation that licenses commit 3's flip.
2026-05-26 17:46:39 +09:00
98e166504f wcc: stamp N_IDENT-callee destructure bindings in checker (#121)
resolvewalk N_MLET arm distributes the N_IDENT-callee rhs return-tuple
element types onto unannotated bindings (the A-narrow slice). Byte-id-
neutral — cgen still classifies structurally, stamps inert until the
exprfloatkind collapse. N_DOT-callee destructure deferred to #16/#17.
Prereq for the #121 collapse (commits 2/3).
2026-05-26 17:27:44 +09:00