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.
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).
A (f64,i64)/(i64,f64) tuple returns its f64 word in X0 (the SSE return
reg) and its integer word in an integer reg (tuple_rseq AX/DX). All three
tuple-from-call receive forms — single-var (cglet), destructure (N_MLET),
reassign (N_MASSIGN) — share the #83 tuple_rseq cursor and all spilled
the f64 word via MOVQ from the integer cursor; that reg holds garbage
(the float is in X0), and #103-FACE-Z's field read (MOVSD slot,X0) then
reads it. A single-return callee masked it (a float-literal return leaves
the f64 bits in AX, and X0 stays live); a branched callee with a non-
literal f64 word has an inner CALL clobber AX, exposing the corruption.
Make every receive spill class-aware: an f64/f32 word spills MOVSD/MOVSS
from X0 (the single SSE return reg, which survives the reg->mem stores
regardless of the word's position), an integer word spills MOVQ from its
tuple_rseq reg as before. cstage applies this at all three inline sites
(cglet, N_MLET, N_MASSIGN); wwstage at the cglet branch and in the shared
tupstore helper (covering cgmlet and cgmassign). The integer/str/slice
path is byte-identical to before, so bootstrap codegen is unperturbed.
Multi-float tuples collide on X0 at the RETURN (#107), out of scope here.
-8f64 is N_UN(TK_MINUS) wrapping a float-typed N_INTLIT; the wwstage
exprfloatkind must recurse through the unary into the N_INTLIT-float
arm. Two rows: a true-case catcher (g(-8.0) == -8f64 -> 1; pre-fix the
literal never reaches X0 so the compare is always false and g(-8.0)
wrongly returns 0, plus a cs!=ww .s divergence) and a false-case
control (g(8.0) -> 0). Both pass byte-identically on the fix.
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.
Two sites, same class: an f64 value failing to reach XMM (X0) before an
SSE op. Both gate-blind — cstage and wwstage emitted the same wrong asm —
so the fix touches both stages identically.
FACE X — a no-decimal float-typed integer literal (`0f64`, `8f64`) is an
N_INTLIT carrying float TYPE. The integer-immediate path stranded it in
AX, so `n == 0f64` compared a stale X0 (true for all n) and
`(8f64 * 10.0): i32` read garbage. Route the float-typed N_INTLIT through
the float-constant-in-X0 emit (cgen.c cgexpr_float, factored from
N_FLOATLIT; cgenexpr.ww cgfloatbits). The wwstage also needs the
exprfloatkind N_INTLIT arm so the downstream f64->i32 cast emits
CVTTSD2SI not MOVSXD — cstage reads the checker-stamped type directly,
so this is the same #101 structural-vs-stamped asymmetry.
FACE Z — a tuple positional f64 field read (`r.0`, r:(f64,i64)) loaded
via the integer op into AX, so `r.0 == 0.0` was wrongly true. Add a
fld_isfloat branch -> MOVSD/MOVSS into X0 (cgen.c:5910 tuple arm;
cgenexpr.ww tuple arm), mirroring the struct-field float load at
cgen.c:1462,1838 (the #96 pattern).
The fix's new cglet branch is gated on sz==16 AND rettupleof != nil. The
existing rows prove it CATCHES 16B tuple receives but nothing pinned that
it does NOT catch a 16B struct{i64,i64} receive (same sz==16, but
rettupleof returns nil for a non-tuple return). A future rettupleof
refactor returning non-nil for a struct would silently route the struct
through the AX/DX tuple spill and miscompile it, gate-blind. The new
ctl_struct row is byte-id and runs correctly (exit 12) both pre- and
post-fix, pinning the tuple/struct discrimination.
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.
wwstage-only. cglet had no 16B whole-tuple-from-call receive branch, so
`let t = call()` whose callee returns a 2-eightbyte (16B) tuple fell
through to the generic single-word store (MOVQ AX, off(BP)) and never
spilled word1 (the DX eightbyte) — silent loss of t.1. Align to cstage
cgen.c:6652, which spills both AX->off+0 and DX->off+8.
Not a tupstore cursor off-by-one and not f64-specific: the destructure
form `let (a,b) = call()` (cgmlet + tupstore cursor) was already byte-id;
only the whole-tuple N_LET receive dropped word1, for any element mix
incl. all-integer (i64,i64). An f64 element surfaced it first. The f64
element rides its eightbyte in AX/DX at receive and is re-read from
X0/XMM at field-read (already byte-id), so no SSE cursor is needed.
Basic x98_nested_arg only spills the imported f64 call-result into the
first float slot (X0); a multi-f64-arg call forces the spill into X2
while two more f64 args are live, exercising pushargsrev's float spill
under XMM register pressure — a distinct path. Verified .s diverges on
master 6f8b658 (integer PUSHQ AX/POPQ DI for the myf.g() arg) and is
byte-identical post-fix; cstage run exits 8 (sum3(-7,10.5,4.5)=8.0:i32).
(#98)
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.
exprfloatkind's N_CALL arm only set the callee name for an N_IDENT
callee, so a module-qualified `mod.g()` callee never reached any
return-type lookup and fell through to integer (kind 0). Both f64
consumers then took the integer path for an imported f64-returning
fn: cgcast emitted MOVSXD instead of CVTTSD2SI (#101), and pushargsrev
spilled the call result as a GPR PUSHQ/POPQ instead of the MOVSD float
spill (#98) — one root, two symptoms.
Route the N_DOT callee through fnretlookupmod with the module
qualifier, mirroring nodeisslice / nodeisstr's #34 N_DOT arm, so a
cross-module f64 call resolves to kind 2 exactly like same-module
already does. This aligns wwstage UP to cstage, whose cg_isfloat reads
the resolved call result type directly (cmd/w6c/cgen.c:117,155) and is
correct for both cases. Consumers (cgcast, pushargsrev) unchanged.
Mirror Hare's single-expression `*(&n: *T)` structure (CLAUDE.md
rule 12) instead of a let-temp two-step that added a binding Hare
has no counterpart for. Document the load-bearing parens (rule 8):
ww's `:` cast binds tighter than unary `&`, so the bare Hare form
parses as `*(&(n: *T))`; `(&n): *T` is what reinterprets the address.
ref/hare/math/floats.ha:5,11. Byte-identical both stages; 952 6/6.
#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.
UCOMISD/UCOMISS set PF=ZF=CF=1 on unordered (a NaN operand). The old
arms keyed on ZF/CF only, so 4 of the 6 relops mishandled NaN:
`nan != nan` was false (JNE keys on ZF=0), `nan == nan` was true, and
`<`/`<=` (JB/JBE) fired on the unordered CF=1. IEEE-754: any relop
with a NaN operand is unordered — `!=` true, the rest false. `!=` now
jumps to true on JNE OR JP; `==`/`<`/`<=` jump to false on JP before
the ordered Jcc.
`>`/`>=` (JA/JAE) are LEFT UNCHANGED: they require CF=0, which an
unordered UCOMISD never produces, so they already reject NaN
correctly. Adding a PF guard there would only churn their .s (an extra
JP on every >/>= float compare) for no correctness gain, so their arm
stays byte-identical to the pre-#97 single template.
Bundles the cgen fix with JP-mnemonic support in both assemblers
(w6c enum/printer + w6a/w6a_ww parse+encode, 0F 8A). They can't split:
the cgen emits JP, which has no encoding without the assembler change,
so a cgen-only commit would not build. JP is the only PF-sensitive
jump on amd64 — there is no alternative instruction.
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.
Delete the vestigial inline slice-let builder in N_LET; a
`let s: []T = buf[lo:hi]` now routes through cgexpr's N_SLICE path
plus the generic 3-word store -- exactly as cstage's own str-let and
the wwstage already do. cap is unchanged (still hi-lo); the
cap = base_cap-lo fix is the following commit.
The builder duplicated cgexpr's N_SLICE base/hi dispatch and was a
strict subset of it, so for local bases the deletion is value-neutral
(ptr=base+lo, len=hi-lo, cap=hi-lo); only the routing bytes move,
aligning cstage down to the leaner wwstage and closing find-4
(rule-10). Verified byte-identical cs==ww across the slice-let matrix
{array,slice}x{local,global}x{hi-default,hi-explicit}.
Also fixes a cstage miscompile: the builder loaded a global-base
sub-slice via localfind->0 + BP-relative (no let_islet/masym), so a
`let s = G[lo:hi]` over a global array or slice G emitted
LEAQ/MOVQ 0(BP) garbage instead of the symbol address. Routing
through the global-aware shared path makes these correct
(ken-confirmed broken->correct).
The G-cluster gave str its 3-word {ptr,len,cap} store/read at indexed/field/chained sites, but each arm was gated on str only; the slice arm fell through to the 1-word fldstoreop default, dropping len+cap. A []T value stored through arr[i]=, arr[i].f=, *struct.f=, or value-spine o.i.f= (and read back via arr[i] / arr[i].f) silently lost length and capacity.
Widen all six arms (4 stores + 2 read mirrors) with a kind-OR (TY_STR||TY_SLICE / typeisstr||typeisslice), never a size test: str and slice are both 24B, so a width gate would fire on both and mask the missing slice arm. The str kind stays distinct and nominal -- the arm is widened, the kinds are not collapsed. cstage and wwstage mirrored.
Gate-blind class: store and read were both short, so byte-identity and cstage==wwstage stayed green on self-consistent garbage; only a runtime len/cap round-trip exposes it (test 941, table-driven, 4 shapes x 2 stages, fail-before/pass-after on both ww and ww_ww).
Deref store (*p=) and tuple-elem store (N_MLET/N_MASSIGN, distinct DX,CX,R8 return-ABI) are the same bug class but separate folds.
Post the str->24B lifts, cgassign had SEPARATE str and slice arms emitting byte-identical 3-word {ptr,len,cap} code. Collapse each identical pair into ONE kind-gated arm (rule 12, sea-of-stars; removes a drift hazard) -- the structural str==[]u8 unification, byte-id-NEUTRAL (each stage's emission unchanged for both str and slice inputs). Pairs: field store s.f=v + ident reassign name=v. cstage gates on the EXACT predicate union (raw kind==TY_STR OR'd with TY_SLICE -- NOT type_isstr, which would also match TY_UNTYPED_STR); ww on isstrtype||isslicetype and letvarisstr||letvarisslice (ww local field/reassign were already merged). Mirrors the in-tree deep-value-chain precedent (cstage 3274). str-only arms with no slice pair (arr[i].field=/chained, G1/G2) untouched.
Verified per-stage PRE==POST byte-identical (focused 5-path fixture + 4 large real combined.ww inputs, both stages); the 5 pairs were byte-identical pre-merge. main.combined.ww regenerated via the canonical make path (md5-stable). A pre-existing global-slice-field-store divergence (g.sl=b: cstage 3-word, wwstage 1-word) surfaced during review -- filed (#26/#10), NOT a C4.4 concern (PRE==POST).
F1 set str.sub = u8; the str-element-size readers no longer need a TY_STR special-case. cstage: delete the two 'if (kind==TY_STR) esz = ty_u8->size' blocks -- the general 'esz = sub->size' path already yields 1 for str (str.sub=u8), as the third index site (which never had the special-case) proves. Provably byte-id-NEUTRAL for ALL inputs: ty_str is the sole TY_STR instance and str.sub==ty_u8, so sub->size==ty_u8->size==1 in every case. No kind-gate (type_isstr/isstrtype arm-selectors) touched.
wwstage elemsizeof (cgenutil.ww) is COMMENT-ONLY: it names primtypesize("u8") directly because it operates on a raw type node with no stamped tinfo at the ident-base index path (str.sub lives on .type_.sub, unstamped here -- cf. cgforrange's 'if sti != nil'); that IS the str.sub-equivalent value, identical asm. Added the WHY + retargeted the citation to the surviving cstage path. The structural collapse there is blocked on tinfo-stamping, not intent -- filed (task #24); byte-id 990-997 guards the residual coupling.
Zero asm change both stages (cstage/wwstage .s byte-identical pre/post and cross-stage). main.combined.ww regenerated via the canonical make path (comment propagation only).
Ranging a str (for (let b .. = s)) and reading the loop var back emitted MOVZBQ on cstage (correct u8 zero-extend) but MOVQ on wwstage (the missed case, #14). Align wwstage UP. ww cgforrange derived the element-type node only for slice/array; for a str scrutinee it left elemt=nil, so the loop var registered with no type and localloadop short-circuited to MOVQ. Fix: for a str scrutinee, synthesize a u8 element node (type_ = str.sub = u8, from F1) as elemt, so localadd hands the loop var a u8 tnode and the GENERIC narrow-load fires (MOVZBQ) -- consuming str.sub as F1 intended, mirroring how []u8 supplies its element node. NOT an if-str special-case. cstage already correct, untouched (ww-only). str's own type stays nominal.
GATE is the ASM SHAPE byte-id (cstage==wwstage at the loop-var read), NOT a runtime probe: the divergence is runtime-benign (MOVQ and MOVZBQ read the same zero-extended byte) so a runtime test passes both ways and cannot distinguish -- it was a byte-id-INVISIBLE divergence (990-997 green despite cstage!=ww, since no bootstrap input exercises a narrow-read str loop var). Verified fail-pre (the cstage-MOVZBQ vs wwstage-MOVQ 1-line diff) / pass-post (.s byte-identical). []u8/slice/array for-range emission unchanged. test/wcc/940 carries the fixture (runtime corpus coverage, both drivers).
main.combined.ww regenerated via the canonical make path.