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.
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).
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.
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.
`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.
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.
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.
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.
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.
cgindex's element-load sites ended in the integer loadopsz (MOVQ/MOVL
into AX), with no float branch — so an f32/f64 array element landed in
a GPR while the consumer's ADDSD/MOVSD read a stale X0. Add a float-
element branch (MOVSS f32 / MOVSD f64 into X0) at all three wwstage
cgindex sites (global, baselocal, fallback) and both cstage N_INDEX
element-load sites, deriving float-ness from the SAME stamped element
tinfo the esz already reads: new elemisfloatc/elemisf32c helpers
(mirroring elemissignedc) for ident bases, typeisfloat/typeisf32(n.type_)
for N_DOT/N_INDEX bases — never a fresh node-stamp that could hit an
unstamped base (#121).
The load fix cannot land alone: the wwstage consumer (cgbin/cgcast)
classified an indexed float operand as INTEGER (no exprfloatkind N_INDEX
arm) and fell to PUSHQ/ADDQ/MOVSXD, while the cstage read the stamped
operand type and used ADDSD/CVTTSD2SI. That divergence is pre-existing
on master (proven: master cs vs ww already differ on `a[0]+a[1]`),
contradicting the original "consumer already expects X0, cs==ww"
premise; load-only would leave the wwstage incoherent (value in X0,
consumed from AX) and still cs!=ww. So this also adds the exprfloatkind
N_INDEX arm — safe because the index-result type_ IS checker-stamped
(cgindex reads it for esz), unlike the unstamped-N_MLET case deferred
under #121. With both, f64 arrays are runtime-correct and both stages
emit byte-identical asm.
946_floatarr_run: f64 element add / trunc / non-adjacent index assert
the value + cs==ww; the f32 row asserts cs==ww only — its runtime value
is blocked by a SEPARATE store-side bug (f32 array-element store writes
AX raw double low-bits instead of CVTSD2SS-narrowed X0), filed as
#119-store. Regen w6c/wwdump combined.ww (cgenexpr.ww + cgenutil.ww
embedded).
fold-1 narrows a float literal at materialisation only when its node
already carries an f32 type — the `f32` suffix. The common un-suffixed
case `let x: f32 = 1.0` stays ty_untyped_float through the checker, so
the node is never f32-typed: the literal materialises as a 64-bit double
and the f32 consumer reads the low 4 bytes (0.0f for clean values).
Stamp such a literal f32 when an f32 target type is in context, the way
harec's lower_implicit_cast does (ref/harec/src/check.c:148): a float
literal's bit pattern is target-dependent, unlike a width-agnostic int
immediate, so the value-producing node must carry the type. Scoped to
untyped_float -> f32 only (f64 already works via cgen's double default).
coerce_floatlit (cstage clet + cstmt N_RETURN) / coercefloatlit (wwstage
resolvewalk's post-order N_LET / N_RETURN handler) are logically
identical. The wwstage stamp is placed AFTER the child re-walk: the
post-order exprtype dispatch re-stamps a bare N_FLOATLIT back to
untyped_float, so coercing earlier (checkletassign) would be undone.
Scope is let-init and return ONLY, aligned down to the leaner wwstage
(rule 10). The wwstage cgen's exprfloatkind hardcodes a float literal to
f64 and cgbin / the unary negate pick f32 off the operands, not the node
stamp — so a stamped literal in an arith-binop / behind a unary minus
narrows in cstage (ADDSS) but not wwstage (ADDSD), a byte-id break. The
wwstage checker also has no assign / param-typed call-arg / per-field
struct-lit site. binop, unary-minus, assign, call-arg, struct-field wait
on #120 (wwstage cgen + checker build-out).
965_f32stamp_run: cstage run + cs==ww byte-id over un-suffixed let-init
and return literals, the hole 964 left open. Regen w6c/wwdump
combined.ww embeds.
Both stages materialise a float literal as a 64-bit double in X0 (MOVQ
bits -> MOVSD), ignoring the node type. For an f32-typed literal the
downstream MOVSS reads the low 4 bytes of that double — garbage (0.0f
for clean values, which is why 0.0 survived the bug and 951's f32 rows,
which only assert NaN ordering, never caught it). Append CVTSD2SS X0,X0
at both literal sites (N_FLOATLIT + the float-typed N_INTLIT arm) when
the node is f32-typed, so the value reaches X0 as a true single. Mirror
in cgenexpr.ww (rule-10) and regen the w6c/wwdump combined.ww embeds.
Covers literals carrying an explicit f32 type (the `f32` suffix and the
no-decimal `8f32` N_INTLIT arm). An un-suffixed literal in an f32
context (`let x: f32 = 1.0`) stays ty_untyped_float through the checker,
so its node is never f32-typed and this branch can't fire — that needs
fold-2 (checker untyped-float -> f32 lowering, both checkers).
964_f32lit_run: cstage run + cs==ww byte-id probe over concrete f32
values (suffixed), the hole 951 leaves open.
Ports ref/hare/sort/{search,bisect}.ha and the cmpfunc type
(types.ha), replacing the experimental vtable placeholder. The
powersort sort()/shuffle() surface stays out of scope.
Divergences forced by ww's surface (rule-10 align-down, not
behavioural):
- cmp is a fn-VALUE param (cmpfunc), not Hare's *cmpfunc: ww
renders functions-in-an-interface by value, as lib/io.ww's
stream vtable does; *cmpfunc is not callable (no fn-ptr
auto-deref) and &fn is *fn(...), unassignable to the alias.
- no const (ww has none); *u8 base + uintptr stride (no [*]
unbounded array, per 962); len() is i32 so cast : size;
single-condition for, so Hare's afterthought is a body tail.
- merged into one sort.ww (ww per-module convention; 900_stdlib
smoke-compiles the file standalone, which a split breaks).
963_sort_run exercises all three on a []i32 with a real cmpfunc,
mirroring +test.ha's search/lbisect/rbisect @test fns. The
comparator binds its derefs to locals to dodge the pre-existing
inline-deref-in-comparison cgen bug (#116); that bug is in the
user comparator, not search/bisect, so the port is faithful.
lib/sort is not compiler-imported: byte-id-neutral, no combined.ww
change, 990-997 unaffected.
#108 sub-fold (c): opaque as a type-erasure sink. Two implicit
assignability rules + the reinterpret casts sort's impl relies on.
rule 1 `*T -> *opaque` IMPLICIT — any pointer is the universal
void-pointer. harec type_is_assignable pointer arm
(ref/harec/src/types.c:1053: `case STORAGE_OPAQUE: break;`
— the referent need not match).
rule 2 `[]T -> []opaque` IMPLICIT — any slice is the erased slice;
{ptr,len,cap} header is normal, byte stride supplied at
runtime. harec slice arm (types.c:1094).
Both fire only when the destination element is opaque, so they are
inert on the opaque-free selfhost corpus.
Rule-10 (per-rule, empirical): rules 1 & 2 are CSTAGE-ONLY. cstage
type_assignable gains the sink; the wwstage check.ww isassignable is a
resolve-only AST approximation that returns "can't tell, stay quiet"
(confident=false) for a ptr/slice whose element it cannot match, so it
already ACCEPTS every form (let-init AND call-arg). Verified: w6c_ww
compiles each probe source exit 0, byte-identically to w6c. cstage
rejected these before this change; no ww twin is needed (same
align-down precedent as 960/961's cstage-only arms).
Casts: N_CAST is validation-free in BOTH stages (the checker never
checks cast legality), so `[]opaque -> *u8` / `*opaque -> *u8`/`*i32`
are already legal. The reinterpret CGEN needed NO change: cgexpr
leaves the pointer in AX for both a slice (so slice->ptr naturally
takes .ptr) and a pointer (ptr->ptr is a no-op). drew described the
Hare idiom as `*[*]u8`; ww has no unbounded-array `[*]`, so the
ww-faithful reinterpret target is `*u8` + uintptr stride arithmetic.
cs==ww byte-id proven on every probe row.
Array->[]opaque (harec array->slice decay, types.c:1080-1099) is
deliberately EXCLUDED: ww has no implicit array->slice for any element
type (`let s: []i32 = a` is rejected too — a slice is built only via
an explicit `a[0:n]`), so there is no array->slice-header cgen.
Accepting array->[]opaque alone would assign a fat array local into a
24-byte slot with no decay: a silent miscompile (rule 7). sort's
caller passes a slice, so slice->[]opaque suffices.
opaque is unused by the bootstrap → INERT → 990-997 stay
byte-identical; combined.ww unchanged (no embedded source touched).
New probe 962_opaque_assign_cast_run carries both dimensions per row
(cstage build+run asserting type-erasure round-trips, AND a w6c-vs-
w6c_ww .s byte-id gate — the 990-997 gates never exercise opaque, so
the test pins rule-10 symmetry itself): rule1_implicit_ptr,
rule2_implicit_slice, and sort_pattern (byte-swap via uintptr stride
through []opaque, read back through the *opaque path and the original
[]i32 view). Probe binds call results before comparing to dodge a
pre-existing inline-call-result-in-comparison cgen bug (#116 family,
reproduces with zero opaque) — same dodge 960 uses.
#108 sub-fold (b): close the footgun #108(a) opened. opaque is abstract
and UNSIZED (size = align = SIZE_UNDEFINED = (u64)-1), legal only behind
indirection. Without guards a bare use would fabricate a (u64)-1-byte
slot — a silent miscompile (rule 7). opaque is illegal by-value in FOUR
aggregate positions (array element, struct field, tuple member, tagged-
union variant) + as a bare value, under size/align, and as a []opaque
element-index. LOUD guards, mirroring harec's scattered `size ==
SIZE_UNDEFINED` checks:
1. bare value/local/param/return-by-value (check.c clet, build_fn_type,
top-level let; harec check.c:1524, :3931)
2. opaque struct field (resolve_type N_TSTRUCT)
3. [N]opaque array element (resolve_type N_TARRAY)
3t. opaque tuple member (resolve_type N_TTUPLE;
harec type_store.c:1147)
3u. opaque tagged-union variant (resolve_type N_TTAGGED;
harec type_store.c:449)
4. size(opaque) / align(opaque) (size/align fold;
harec check.c:2720)
5. indexing []opaque (N_INDEX; harec check.c:384)
Detection is via the SIZE_UNDEFINED sentinel the guard consults, so the
sized forms `*opaque` (8B) and `[]opaque` (24B header) pass untouched.
Rule-10 per-guard stage placement:
- Guards 1/2/3/3t/3u/5 are CSTAGE-ONLY. The wwstage check.ww is an
AST-level approximation with no binding-size computation (g1) and no
type-decl field/element/member validation walk (g2/g3/3t/3u); its
N_INDEX indexresult returns the element type without consulting its
size and defers invalid-index rejection to the cstage (g5). Same
cstage-only neg-case precedent as 712_redecl / 708_param_shadow_mod.
- Guard 4 is BOTH-STAGES. The wwstage HAS the size()/align() fold
(astsize/astalign would otherwise fold opaque to a bogus 0 — a silent
miscompile); twinned via astunsized + deffolderr. Because the wwstage
has NO per-construction guards, its fold alone must catch every
opaque-containing type: astunsized is RECURSIVE — a type is unsized
iff it is opaque OR an aggregate (array/struct/tuple/tagged) with a
recursively-unsized member. This both reaches the tuple/tagged folds
AND closes the leaf-only size([4]opaque)/size(struct{x:opaque})→0
leak. The cstage size/align guard stays leaf — the cstage rejects
unsized aggregates at construction, so its fold only ever sees a leaf.
opaque is unused by the bootstrap, so every guard is inert on the
selfhost corpus — 990-997 stay byte-identical. Regenerates the w6c/wwdump
combined.ww (check.ww embed). New compile-fail probe 961_opaque_guards
(14 build-fails rows incl tuple/tagged/nested + 2 *opaque/[]opaque
positive controls); 960 positive probe unchanged.
#108 sub-fold (a): TY_OPAQUE exists, is name-bindable, and carries an
UNDEFINED size sentinel. Mirrors the #85 `size` fold pattern at every
site, both stages (rule-10).
opaque is abstract + UNSIZED: prim()'d with size=align=SIZE_UNDEFINED
(NOT 0 — a 0 would let a bare `let x: opaque` fabricate a 0-byte local),
mirroring harec builtin_type_opaque (ref/harec/src/types.c:1446). ww had
no incomplete-size sentinel, so this fold ADDS one: cstage
`#define SIZE_UNDEFINED ((u64)-1)` (== harec types.h:58 (size_t)-1) and
wwstage `def SIZE_UNDEFINED: u64 = 18446744073709551615`.
Legal only behind indirection: `*opaque` (8B ptr) and `[]opaque` (24B
slice header) construct correctly because type_ptr/type_slice (and the
wwstage typeptr/typeslice) size themselves independent of the element.
opaque is deliberately absent from is-int/unsigned/num/float and from
the size-classification switches (let_emit_size / tupleelemslot /
fieldslotsize) on both stages — it only reaches those as TY_PTR/TY_SLICE.
The use-restriction GUARDS (reject bare opaque / size(opaque) / opaque
field / [N]opaque / []opaque-indexing), assignability, and cgen-verify
are the separate sub-folds (b)/(c)/(d) — NOT here.
opaque is unused by the bootstrap, so 990-997 stay byte-identical
(inert, like #85). Regenerates the w6c/wwdump combined.ww (typ.ww +
check.ww embedded). New probe 960_opaque_decl_run exercises `*opaque`
and `[]opaque` (.len/.ptr) behind indirection.
ww's int/uint are machine words (8B on amd64, type.c:58), not the 4B
Hare gives them on amd64 (arch+x86_64.ha maps INT_MAX->I32_MAX). So the
limits can't alias a per-arch literal; they DERIVE from size(int) the
Go way (cf math.MaxInt), staying correct on any word width:
INT_MAX: int = (1 << (size(int)*8 - 1)) - 1
INT_MIN: int = -1 << (size(int)*8 - 1)
UINT_MIN: uint = 0
UINT_MAX: uint = ~(0: uint)
All four const-fold in def-init; on amd64 they evaluate to I64_MAX,
I64_MIN, 0, U64_MAX. UINT_MAX uses the all-ones complement to dodge the
1<<64 overflow. Per the user ruling (2026-05-26): derived, not literal.
Probe 959_types_intlim_run asserts each value vs both the literal and
the i64/u64 limit const, plus wrap-through-i32 arithmetic usability.
combined.ww regenerated for all 5 selfhost tools + smoke (all embed
lib/types).
Faithful port of ref/hare/types/arch+x86_64.ha:16-26. SIZE_MAX is the
no-cast `def SIZE_MAX: size = U64_MAX;` — size is in the unsigned class
and 8B on amd64, so the u64->size init coerces without a cast (#113);
UINTPTR_MAX keeps Hare's explicit `U64_MAX: uintptr` since uintptr is
outside the unsigned class. Probe 958_types_sizelim_run asserts MIN==0,
MAX==U64_MAX, and arithmetic usability for both types.
INT_MIN/MAX + UINT_MIN/MAX deferred to #114 (ww int=8B vs Hare 4B on
amd64 leaves the value open); RUNE_MAX deferred to #112 (no \U lexer).
combined.ww regenerated for all 5 selfhost tools + smoke.combined.ww
(all embed lib/types).
A def initializer whose rhs references another def -- `def INT_MIN: int
= I32_MIN;`, `def SIZE_MAX: size = U64_MAX;` -- failed to compile: an
N_IDENT->SK_DEF types as the referent's DECLARED type (i32, u64), so the
def-init assignability check (type_assignable) rejected i32 -> int /
u64 -> size, even though the value is a compile-time constant that fits.
This blocked faithful types/types::c limit defs (no cast in the Hare
source).
In a def initializer the rhs is a flexible constant. When it folds to a
compile-time integer (the #88 eval_def_const path: sibling/imported def
refs, casts, arithmetic) and the value fits the declared integer target,
re-flexibilize it to UNTYPED_INT so the existing untyped-int->typed
assignability path accepts it. This emulates Hare's flexible-constant
promotion (ICONST -> promote_flexible/lower_flexible,
ref/harec/src/types.c:860); def_cast_fits is the range check that keeps a
genuine out-of-range narrowing a loud "not assignable" error, never a
silent truncation (rule 7). It is strictly the const subset: the general
CONCRETE (non-const) integer widening Hare does at types.c:1021-1037 is
intentionally stricter in ww -- #115.
cstage-only: the wwstage checker (selfhost/cmd/wcc/check.ww, "let init /
return assignability") intentionally never checks def-init assignability
(it stays quiet, leaving full inference to the C side), so it never
rejected the widening -- the #88 stamp already laid the correct DATA row.
Relaxing the cstage aligns the richer side DOWN to the leaner side
(rule 10); both stages stamp the identical folded value, so emitted asm
is byte-identical. The bootstrap corpus has zero cross-prim-width def-ref
defs, so the new path is dead there and 990-997 are unperturbed.
Coverage: test/wcc/760_def_widen_const (i32->int neg, u64->size, byte-id
on each, cstage-only out-of-range narrowing fail-loud).
Resolve `size` -> TY_SIZE at the type-name resolver (C lookup_builtin /
ww tinfofornode's N_TNAME chain), mirroring uintptr, both stages. This
makes `size` writable as a type (`let x: size`, struct field, etc.),
the prerequisite for lib/types SIZE_MAX.
Twins every NAME-keyed uintptr arm in the wwstage so it behaves like
the cstage's kind-keyed Type switches (already TY_SIZE-aware from
fold-1): primtypesize + astalign (8B/8-align), primsize + letscalarprim
(8B scalar slot), isinttypeast + isnumerictname (int/numeric). rule-10
symmetric; dead on the size-free selfhost corpus so 990-997 stay byte-id.
Coexists with the size(T) size-of operator (separate c.top SK_FN seed +
N_CALL fold, NOT a type path) and `.size` field access (N_DOT); neither
touched. No c.top SK_TYPE "size" seed (would collide with the operator
seed at check.ww:96). Regenerates w6c/wwdump combined.ww (checker
embedded). New probe 957_size_type_run exercises type-position `size`
and the operator in one scope.
Covers single-var (cglet), destructure (N_MLET), and reassign (N_MASSIGN)
receives, each with a BRANCHED callee whose f64 word is an f64 param
(single-return + float-literal masks #105 via AX bit-coincidence). 7 bug
rows: single-var (f64,i64) + deferred-read-after-X0-clobber + (i64,f64)
order-swap; destructure (f64,i64) + order-swap; reassign (f64,i64) +
order-swap. 4 controls: all-integer branched, destructure-with-no-f64,
#103 FACE-Z single-return field read, #103 FACE-X 0f64 compare. Each row
asserts cstage build+run exit AND w6c vs w6c_ww .s byte-identity. The bug
is gate-blind to byte-id alone (all three forms symmetric-wrong on
master, no .s divergence); the cstage-run check catches it. FAILs on
master 4c4006d (7 bug rows), PASSes on the fix (11/11).
Table-driven, modelled on 953: each row does (a) cstage build+run+exit
and (b) w6c vs w6c_ww .s cmp. Rows cover FACE X (0f64 compare false +
true case, 8f64 arith) and FACE Z (tuple-field f64 compare) plus the
let-bound control that was already correct. Fails on master e784968
(x_cmp_false 1!=0, x_arith 100!=80 + byte-id divergence, z_tuple_field
5!=9); passes on the fix.
Per-row: cstage build+run exit assert AND w6c vs w6c_ww .s cmp.
Three scalar-pair single-var bug-rows — (f64,i64), (i64,f64) order-swap,
(i64,i64) all-integer — each diverge on master 60c3e51 by one dropped
MOVQ DX and are byte-identical after the fix. The all-integer row proves
the fix is not f64-gated. Two control rows stay byte-id pre- and
post-fix: the destructure form `let (a,b) = mk()` (cgmlet + tupstore
cursor, the path bootstrap/990-997 rely on) and a 32B str-element
single-var tuple (excluded by the fix's sz==16 gate). Modelled on
953_f64crossmod_run. Master gate: 3 bug rows FAIL, 2 controls clean.
Regression net with two dimensions per row because cstage is correct
before and after the fix — a cstage-only probe is gate-blind to a
wwstage-only divergence:
(a) cstage `ww build` + run, asserting the truncated exit code;
(b) w6c vs w6c_ww `.s` cmp, FAILING on any rule-10 divergence.
Rows cover #101 (`mod.g(): i32`, neg + both truncation directions) and
#98 (`dbl(mod.g())` forwarding an imported f64 call-result as an f64
arg). Verified: dimension (b) FAILS on master 6f8b658 (pre-fix wwstage
emits MOVSXD/integer-PUSHQ) and PASSES post-fix (byte-identical);
dimension (a) passes on both, confirming cstage was always correct.
#96 and #97 are GATE-BLIND: both stages emit byte-identical asm before
and after the fix, so the 990-997 byte-id gates can never catch a
reintroduction. Only an executed-and-checked runtime probe can. Adds
test/wcc/951_f64cgen_run.c, a table-driven cstage build+run harness
(modeled on 700_e2e) asserting exit codes.
Coverage: #96 f64/f32 deref-load (MOVSD/MOVSS into X0) via bare *p, f64
return through a fn, arith-through-deref, f64frombits reinterpret round
-trip, and copysign-style sign transfer; #97 the full 6-relop NaN sweep
(UCOMISD + UCOMISS), isnan true/false, a NaN-relop true-count value
assert, and the untouched >/>= left-bare arm. Verified the suite fails
on master 0d1ae17 (7/9 rows) and passes on the fix (9/9).
cstage-only by design (mirrors 700_e2e + 969_checked_run): ww_ww run is
broken (#95) and per-program wwstage byte-id is the 990-997 gates' job.
ww top-level def rhs const-fold was literal-only (fold_int_literal at the codegen emit-defs step), so a def referencing another def, an imported def, or a cast was inexpressible -- blocking faithful types/types::c/math/strconv ports whose defs cross-reference.
Fold at CHECK time: a recursive eval_def_const (pass-2 N_DEF arm, both stages) resolves N_IDENT/N_DOT via the checker's existing scope lookup to the target def's rhs, evaluates N_BIN through a shared fold_binop core (factored out of eval_enum_value so both compile-time-int-eval paths share one wrap/shift/divide table), strips identity/widening casts, and stamps rhs -> N_INTLIT. cgen is UNTOUCHED -- its existing literal-emit lays the DATA row. Gated to fire only when the plain literal fold fails, so existing defs keep their node and emitted asm is byte-identical (990-997 unperturbed by construction).
Guards (rule 7): recursion depth cap fails loud on a def cycle (same/cross-module); a narrowing cast (rhs outside target range) fails loud rather than silently truncating. Both stages' eval_def_const stamp identically (shared fold_binop semantics) so the substituted literal -- and byte-id -- holds across stages (rule 10, at the check pass).
a1 (same-module) + a2 (cross-module imported def) land together: the driver concatenates imports into one flat scope. Coverage: test/wcc/732_def_const_fold.
Port Hare's math::checked to lib/math/checked/ as a two-file module
mirroring the upstream split:
- checked.ww (ref/hare/math/checked/checked.ha): add*/sub*/mul*
returning (result, overflow) with wrapping semantics — addi/addu/
subi/subu 8-64 and muli/mulu 8-32 (22 fns).
- saturating.ww (ref/hare/math/checked/saturating.ha): sat_* clamping
to the type's range on overflow — sat_addi/addu/subi 8-64 and
sat_muli/mulu 8-32 (18 fns).
checked_test.ww drives the verbatim Hare @test vectors (crash-trick
idiom) via cross-module tuple-return destructure for the overflow fns;
wrapped by test/wcc/969_checked_run.c. Both stages emit byte-identical
asm; make test-unit green.
Three ww adaptations vs Hare, all forced by language differences, none
behavioral (documented at the sites):
- no if-as-expression -> `return if (c) X else Y` becomes if-stmt.
- no implicit integer promotion -> the mul overflow compares use an
explicit widening cast.
- sub-word arithmetic truncates only on store to a typed lvalue, so
unsigned overflow tests force the wrap through a typed `res`.
Deferred as faithful Hare-subsets (Hare splits per type; no inlining):
- size-typed *z variants: no `size` type yet (#85).
- int/uint native-width variants: ww int/uint are 64-bit, a silent
overflow-boundary width divergence.
- 64-bit muls (muli64/mulu64/powi64, sat_muli64/sat_mulu64) and the
muli/mulu dispatchers: need math::mulu64 (128-bit product).
- sat_subu8/16/32/64: need types::U*_MIN, not yet in lib/types.
Saturating sat_* reference the types limits at RUNTIME (conditional
return, not a const-initializer), which resolves cross-module today
(#88 is const-fold-only). subi64's I64_MAX/I64_MIN boundary @test vector
is omitted while #89 is open (its I64_MIN literal miscompiles on
wwstage); the saturating I64_MIN assertions use the types.I64_MIN
def-ref, which is byte-id clean.
Replace the str-only XOR (e0_is_str ^ e1_is_str) at the tuple send
(N_RETURN) and receive (N_MLET/N_MASSIGN) sites with a positional
per-element register cursor, mirroring harec create_unpack_bindings
(ref/harec/src/check.c:1354-1416). Each element rides consecutive
eightbytes over [AX,DX,CX,R8]; a slice/str rides its 3-word
{ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8), a scalar rides 1.
Send and receive walk the SAME type-table widths so element->register
agrees. This routes []u8 elements through the 3-word path (the XOR was
slice-blind, dropping len+cap to the scalar fallback) and closes the
pre-existing (scalar,slice) cs!=ww divergence by construction. cstage
and wwstage emit byte-identical asm.
Both receive sites derive each element's width from the rhs tuple's
element types (n->rhs->type->params / the callee return type) -- the
SAME producer view the send site walks -- NOT the binding type: a `_`
lvalue is an N_IDENT with empty str the checker never type-stamps, so a
binding-typed width mis-sized a wide `_` and desynced the cursor for the
next element (cstage read DX, wwstage R8). harec `_` skips the store but
CONSUMES its tuple offset; the cursor advance honours that.
Loud-stop (rule 7): the register file holds 4 eightbytes; a tuple whose
elements sum to >4 (([]u8,[]u8)/(str,str)=6) cannot be register-returned,
so the send site aborts at compile time citing the return-ABI capacity
(#10) rather than silently miscompiling. The receive loop guards the
same predicate (defense-in-depth). Routed through each stage's EXISTING
pinned-fatal idiom: cstage fatal() (cmd/wcc/err.c), wwstage the inline
os.write(2,...)+os.exit(1) at cgen.ww:604 -- no new diagnostics path.
N_MASSIGN (`a,b=f()`, bare comma, pre-declared) is a retained
ww-EXTENSION beyond Hare's binding-only tuple-unpack (Go/rob-pike
multi-assign, rule-9 carve-out); the loop covers it identically to
N_MLET.
Test 945_tuple_nary_destructure_run: (i64,[]u8)+(i64,str) store+read
len/cap for both N_MLET and N_MASSIGN, a single-str control, a wide-
first blank `_,a=f()` row (the cursor-desync discriminator), and a
([]u8,[]u8) row asserting the loud BUILDERR carries the cited
diagnostic; dual ww/ww_ww drivers.
A slice VALUE stored through a whole-deref lhs `*p = v` dropped len+cap:
the `*p = v` arm kind-gated its 3-word {ptr,len,cap} stash+store on str
ONLY, so a slice fell to the 1-word fldstoreop default (ptr only). The
deref READ is 3-word, so the reader got garbage len/cap -- correctness,
not perf. str IS []u8 since #1, so the str machinery applies verbatim;
widen the gate str -> str||slice (kind-OR, not a sz==24 test). This is
the project #75 str-only-gate one level down (deref-store).
cstage cmd/w6c/cgen.c:3792/3800 (two gates); wwstage cgenexpr.ww `*p=v`
twin detects N_TSLICE syntactically (mirror str). Both stages dropped
identically, so cs==ww + 990-997 + byte-id are all gate-blind here --
only a store->read roundtrip catches it. New 944_deref_slice_store_run
asserts the {ptr,len,cap} survives a poisoned dst, via the direct local
and the field-deref read; str-deref + (*p).field controls guard the
untouched arms. Verified fail-before (1-word ptr store) / pass-after
(8/8), byte-identical asm both stages.
Out-of-gate, deferred to #80: the wwstage syntactic detection is
alias-BLIND -- a slice-alias `*Foo` (Foo=[]T) or non-ident deref-store
stays 1-word, the SAME retained divergence str already carries (cstage's
resolved-type vt fires in both). #80 unifies detection by aligning the
wwstage UP, not gating cstage down. Separately surfaced (filed apart,
not touched here): the whole-deref READ-into-let `let v = *p` drops
len+cap for a slice while the str form is 3-word -- the read-side twin
of this store hole.
A sub-slice base[lo:hi] advanced its data pointer by lo (element
COUNT) instead of lo*esz (BYTES), so the base pointer was wrong for
any esz>1 element. Pointer arithmetic is membsz-unit per the rt
invariant (ref/hare/rt/ensure.ha:30); esz==1 (u8/str) is unchanged.
Four emission sites, fixed byte-identically across stages (rule 10):
- value path: cmd/w6c/cgen.c N_SLICE <-> cgenexpr.ww cgslice
- call-arg: cmd/w6c/cgen.c:4646 <-> cgenutil.ww pushargsrev
Scaling mirrors the cgindex idiom: esz from the type table (rule 13;
cstage bu->sub->size, wwstage elemsizeofc) gated to an N_IDENT base,
uniform IMULQ (no SHL special-case, no immediate form -- w6a is
reg-reg only). The live lo reg is the multiplicand so the one free
GP (DX value / BX arg) holds esz*lo; lo is preserved for len (hi-lo)
and cap (base_cap-lo, #20). The esz==1 path keeps the single ADDQ,
byte-identical to before (#75/#20/str unaffected). Non-ident bases
stay unscaled in both stages (wwstage has no tnode there), tracked
as a #76 residual alongside #74.
New 943_subslice_ptresz_run: table-driven, dual-driver (ww/ww_ww),
esz in {2,4,8} array+slice base, lo>0, let-form + call-arg form;
asserts s[0]==base[lo] & s[1]==base[lo+1]. Fails on every fixture
pre-fix on both stages, passes post-fix. Registered in Makefile
(TESTS + target) so test/run builds and runs it.
A sub-slice `base[lo:hi]` now sets cap to base_cap - lo (the storage
remaining to the underlying end; Go/Hare-identical) instead of hi - lo
(== len). base_cap is the array length N for [N]T, or the .capacity
word carried in a slice/str header at +16. Authored once per stage in
the cg_base_cap / cgbasecap helper, applied at both cap sites: the
N_SLICE value path (which serves let-init since the prior commit) and
the call-arg push. Both stages stay byte-identical (find-4 closed).
cap arithmetic per ref/harec/src/eval.c:1017 (slice: slice.cap -=
start) and eval.c:1024 (array: cap = array.length - start); capacity
is a distinct field per ref/hare/rt/ensure.ha:4-8 and cap >= len per
ref/harec/src/check.c:596. Only the cap arithmetic transfers: the ptr
stays unscaled (lo*esz is #76) and eval.c's stricter start>=end bound
is not ported (ww's runtime bound is start>end).
str[lo:hi] yields str with a real .capacity (D1), so the str base uses
the same +16 load -- no downgrade to []u8. base_cap falls back to len
(prior behavior) where it isn't cleanly available: a non-ident base
(its header cap was discarded by cgexpr; len is likewise wrong for a
defaulted hi there, pre-existing) and a global str base (wwstage
cgslice has no global-str load, #73 -- the carve-out keeps both
stages byte-identical).
Test: 942_subslice_cap_run, table-driven over both drivers, array /
slice / str base + an append-no-realloc row, each shape chosen so
base_cap-lo != hi-lo.
Fold in three pre-existing fixtures that asserted the old cap == len
and so failed under the corrected semantics (project #20):
681_arr_elem_field_write (slice_field_value_write,
slice_field_ptr_write, slice_field_distinct_bytes),
693_dot_tagged_source (local_struct_slice_variant,
via_ptr_slice_variant, letinit_slice_roundtrip, top_level_global_slice),
and 695_match_bind_struct (slice_neg_control). Each cap word updated to
base_cap - lo: a [8]u8 base sliced at lo=0 yields cap 8 (5->8, 3->8);
distinct_bytes slices a [16]u8 at lo=0, yielding cap 16 (6->16). len /
mark / ptr assertions are unchanged -- only the cap word moved.