The N_RETURN tagged-pack scalar-variant arm did MOVQ AX,DX, but a float
variant's value is in X0 not AX -> packed stale int (broke stof64/stof32
return (f64|invalid|overflow)). Fix: float variant bridges X0->DX via a
stack slot (SUBQ $8,SP; MOVQ $0,(SP); MOVSS|MOVSD X0,(SP); MOVQ (SP),DX;
ADDQ $8,SP), gated type_isfloat/exprfloatkind. No MOVQ-xmm->gp form
exists, hence the spill (715-class, cgreturn-register-pack twin of 715's
store-to-slot). Zero-slot-first -> deterministic f32 high-4. AX-independent
-> also resolves the multi-variant cs!=ww. Bootstrap-NEUTRAL (compiler has
no float-tagged-return). Test 707 +3 rows (f64/f32/multi, slot+8 bit-exact;
f32 no-f32-arg to isolate #143). Make test 184/184 incl 990-997 byte-id.
Close A.3's deferred shape-14 (nested array). (a) emit_array_lit_bytes
gains a TY_ARRAY-element arm (mechanical clone of the TY_STRUCT-element
arm — recurses; esz=etype->size, rule-13; ...-nested loud-reject). (b)
double-index read tbl[i][j]: when the indexed element is TY_ARRAY, leave
the sub-array ADDRESS in AX instead of dereferencing (sister of #135's
N_DOT-base fix, on the N_INDEX path) — new elemisarrayc/tinfoisarray
helpers, both stages. Storage + read = one 2D-end-to-end concern (A.2/A.3
storage+LOAD precedent).
Unblocks strconv fold-4's powers_of_ten[596][2]u64 (direct double-index
access). Bootstrap-NEUTRAL (new arms gate on TY_ARRAY-element; 1D
consumers byte-identical, 990-997 green). Test 919 +2D rows + 3D +
...-nested-reject. Deferred siblings: #155 (sub-array bind / whole-
aggregate copy), #160 (global-struct-field index base).
Extract emit_array_data + emit_array_lit_bytes helpers (both stages,
mirrored) for module-level let/def with N_ARRLIT initializer or no-rhs
zero-init. Two-pass validate-then-emit: validate pass walks elements
and fails atomically on any non-foldable element (no partial-byte
emit on failure); emit pass writes element bytes after success.
Element-kind dispatch: integer via fold_int_literal byte-for-byte
preserved from pre-A.3 inline arm (bootstrap NEUTRAL — 6 live consumers
in lib/os/bufio/strings/encoding-utf8/strconv-stof_data), float via
inline bitcast + sign-XOR byte-loop (A.1 shape, no INT64_MIN — sibling
#144), struct via recursion into emit_struct_lit_bytes (A.2 helper).
Out-of-scope element kinds (ptr-elem, nested-array) rule-7 fatal.
emit_struct_lit_bytes gains TY_ARRAY field arm calling emit_array_lit_
bytes recursively — closes A.2 parked shape-15 (array-in-struct
`def D: dt = dt{tag=42, buf=[1u8,2u8,3u8,4u8]};`).
LOAD-side widened symmetric to A.2 precedent: cstage cgindex N_INDEX
direct-ident isglobal gate widened via new DefArray registry
(def_isarraydef populated in let_collect parallel to DefStruct);
wwstage cgindex N_INDEX falls through to defvartnode on letvartnode nil
(reads defent.dtnode field added in A.2). Both stages materialise
array-def via LEAQ name(SB) same as array-let.
Mid-impl rule-7 stop: refactor initially routed only rhs==N_ARRLIT
through emitarraydata, leaving nil-rhs zero-init arrays (e.g.
`let f64tos_buf: [64]u8;` in lib/strconv) silently SKIPPED → undef-ref
at link of wwstage-rebuilt selfhost binaries. Caught on first gate run
via bootstrap 994/995 RED. Fixed by adding nil-rhs branch to
emitarraydata (zero-fills arrt.size bytes) + widening wwstage caller
to route both N_ARRLIT and nil through helper. Same-class-lower-stratum
pattern (recurring across A.1 N_UN-peel, A.2 sz==8-short-circuit, A.3
nil-rhs-drop); banked as feedback memory.
Test 919 (11 rows: int-elem 1B/4B/8B + signed-N_UN-peel + float-elem
f64/f32 + def-int / def-float / struct-with-array-field shape-15 +
explicit-zero + single-elem-regression) registered. Make test:
182/182 incl. 990-997 byte-id + combined_ww_fresh.
Followups filed:
- #43 — wwstage emitletdataw str/slice-size arms lack !isarr guards;
hypothetical no-rhs [16/24]u8 triple-emits (NOT A.3-introduced;
no live consumer; 2-line parity fix)
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).
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).
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.
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.
`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.
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.
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).
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.
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).
Float array-element stores (array-literal init, [v...] repeat-fill, and
arr[i]=v) now route from X0 via MOVSS/MOVSD in both stages; the AX path
stored the raw double low-bits, garbage for f32 (f64 worked by accident).
A clobbering call-index (a[geti()]=v) loses the X0 value — deferred to #125.
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.
fold-1: type exists + classifies; mirrors TY_UINTPTR at every site, both stages. size(T)/len() return types UNCHANGED (fold-2). Regenerates the 5 combined.ww (lib/ww embedded).
Port the deferred sat_subu8/16/32/64 from
ref/hare/math/checked/saturating.ha:196,206,216,226 — clamp to
types.U*_MIN (0) on underflow. Mirrors the existing sat_addu* shape
(typed res forces the sub-word wrap, then the >a underflow test).
Tests: sat_subu* normal+clamp rows plus a direct types min check
(U*_MIN==0, RUNE_MIN=='\0'), wired into checked_test main().
Mirror Hare's types::limits U8_MIN..U64_MIN (all 0) and RUNE_MIN
('\0'), ref/hare/types/limits.ha:30,36,42,48,54. Pure literals,
byte-id-neutral; the U*_MIN unblock checked sat_subu* which clamp to
types.U*_MIN.
Catch-up regen only; no source change. w6c and wwdump embed the wcc cgen, whose post-#97 edits landed without regenerating these two tools' combined.ww -- the byte-id gates are freshness-blind, so master stayed green while shipping a stale artifact. Permanent freshness gate filed as #110.
Four cstage build+run rows for the fold-2a decompose step: issubnormalf64
across normals/zero/a constructed subnormal (f64frombits(1u64)), and
normalizef64 on both the normal path (n, 0) and the subnormal multiply
path (exp -52, result no longer subnormal). normalizef64's (f64, i64)
return rides the #102 16B-tuple-from-call receive; tuple-field f64s are
spilled to a let before any literal comparison to dodge the same
gate-blind XMM mis-load that holds back frexpf64 (see 952 header +
lib/math/floats.ww). Byte-id of a normalize-calling program is covered by
954's tuple-receive gate.
Ports the subnormal-normalize step of the f64 decompose half from
ref/hare/math/floats.ha: issubnormalf64 (floats.ha:179) and normalizef64
(floats.ha:256, the f64-multiply-on-subnormal that yields (f64, i64)).
frexpf64 (floats.ha:278) is held back, not ported: its Hare-exact zero
guard `n == 0f64` miscompiles. A no-decimal `0f64` literal used as an f64
comparison operand is materialized into a GPR and never moved to XMM, so
the UCOMISD reads a stale operand and `n == 0f64` is wrong for every n.
Both stages emit this identically, so the byte-id gates are blind to it.
`0.0` compiles correctly but substituting it would be a workaround
(rule 7), so frexpf64 waits for the cgen fix. normalizef64/issubnormalf64
touch neither the broken literal form nor any tuple-field comparison, so
they are correct and land now.