Commit Graph

493 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes the gate-blind cs≠ww float family root (STATUS:14, named (b)-phase
goal). Builds on 98e1665 (A-narrow N_IDENT destructure stamp) +
1c4cea4 (transient floatness-disagreement bridge).
2026-05-26 18:30:18 +09:00
1c4cea4e2c wcc: exprfloatkind structural/stamp floatness-disagreement bridge (#121)
Add classifytinfo(n.type_) stampkind alongside structural structkind;
abort on floatness-disagreement only ((structkind!=0)!=(stampkind!=0));
return structkind (byte-id-neutral). TRANSIENT — deleted in commit 3 (the
flip + permanent residual sibling-evidence guard). The corpus-wide
validation that licenses commit 3's flip.
2026-05-26 17:46:39 +09:00
98e166504f wcc: stamp N_IDENT-callee destructure bindings in checker (#121)
resolvewalk N_MLET arm distributes the N_IDENT-callee rhs return-tuple
element types onto unannotated bindings (the A-narrow slice). Byte-id-
neutral — cgen still classifies structurally, stamps inert until the
exprfloatkind collapse. N_DOT-callee destructure deferred to #16/#17.
Prereq for the #121 collapse (commits 2/3).
2026-05-26 17:27:44 +09:00
7d7ed964b0 wcc: f32 array-element store from X0 (#122)
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.
2026-05-26 14:46:01 +09:00
0917fee48d wcc: float array-element loads to X0 + indexed-float consumer (#119)
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).
2026-05-26 13:10:46 +09:00
a1dff13ec1 wcc: stamp un-suffixed f32-context float literals (#104 fold-2)
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.
2026-05-26 12:06:47 +09:00
acaf0152da wcc: narrow f32-typed float literals at materialisation (#104 fold-1)
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.
2026-05-26 11:19:37 +09:00
345325838f lib/sort: faithful search + lbisect + rbisect port
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.
2026-05-26 10:54:35 +09:00
3a0c7442d4 wcc: opaque assignability sink + reinterpret-cast verify (#108)
#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.
2026-05-26 10:29:04 +09:00
f4970d886c wcc: opaque use-guards — reject every unsized use (incl tuple/tagged, recursive) (#108)
#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.
2026-05-26 09:52:13 +09:00
3a18d2cfe6 wcc: add the opaque abstract type (kind + UNDEFINED sentinel + name-binding) (#108)
#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.
2026-05-26 09:02:08 +09:00
d9cfb91cb9 types: add INT/UINT limit constants, derived from size(int) (#114)
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).
2026-05-26 08:31:41 +09:00
b1c598651f types: add SIZE/UINTPTR limit constants
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).
2026-05-26 03:07:01 +09:00
5e4d67d90a check: widen const def-ref to declared int type in def init (#113)
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).
2026-05-26 02:39:35 +09:00
9a265a31f5 wcc: name-bind the size type in type position (#85 fold-2)
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.
2026-05-26 01:55:54 +09:00
bd7181ae1f wcc: add the size primitive type (TY_SIZE), classify as unsigned int (#85)
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).
2026-05-26 01:26:03 +09:00
68dbb77d6c test: combined.ww freshness gate in phase-2 (#110) 2026-05-26 01:07:58 +09:00
529eb51af3 selfhost: resync smoke.combined.ww to current lib/types (#110)
#28 regen'd the 5 cmd combined.ww but missed smoke, which pulls lib/types transitively. Baseline for the freshness gate below.
2026-05-26 01:07:25 +09:00
330792b884 lib/math: checked sat_subu* unsigned saturating subtract (#28)
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().
2026-05-26 00:48:19 +09:00
c40b2df097 lib/types: add U*_MIN and RUNE_MIN
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.
2026-05-26 00:35:53 +09:00
4578ce98a0 selfhost: resync w6c/wwdump combined.ww to current wcc source (#109)
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.
2026-05-26 00:33:50 +09:00
f12676b880 test: 952 add frexpf64 runtime rows 2026-05-25 16:48:14 +09:00
9311e6ca4e lib/math: math::floats fold-2a frexpf64 decompose 2026-05-25 16:48:14 +09:00
b4752aad21 test: 952 add issubnormalf64 + normalizef64 runtime rows
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.
2026-05-25 16:43:25 +09:00
263257f2c1 lib/math: math::floats fold-2a issubnormalf64 + normalizef64
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.
2026-05-25 16:43:25 +09:00
8182342a9d test: 956 branched f64-tuple receive across all 3 forms, run + cs==ww byte-id (#105)
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).
2026-05-25 16:23:47 +09:00
ec19d0ad20 cgen: tuple receive spills f64 word from XMM, not integer reg (both stages, #105)
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.
2026-05-25 16:23:47 +09:00
4c4006d854 test: 955 add negative-f64-lit compare rows (#103 FACE X)
-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.
2026-05-25 15:38:47 +09:00
0388cb7a26 test: 955 f64 XMM-materialise run + cs==ww byte-id (#103)
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.
2026-05-25 15:26:48 +09:00
c9e39c6782 cgen: f64-typed int-literal + tuple-field materialize in X0 (both stages, #103)
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).
2026-05-25 15:26:43 +09:00
e784968dd8 test: 954 add struct-exclusion control row (#102 over-catch guard)
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.
2026-05-25 14:45:04 +09:00
734c0c36e5 test: 954 16B tuple-from-call receive cs==ww byte-id + cstage run (#102)
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.
2026-05-25 14:32:24 +09:00
6a586cf792 cgen: wwstage cglet spills both eightbytes of a 16B tuple-from-call receive (#102)
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.
2026-05-25 14:29:34 +09:00
60c3e51dc5 test: 953 add #98 XMM-pressure row (myf.g() as 1-of-3 f64 args, MOVSD->X2)
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)
2026-05-25 14:04:07 +09:00
ab22900ad1 test: 953 f64 cross-module return cast/arg cs==ww byte-id + cstage run (#98/#101)
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.
2026-05-25 13:51:13 +09:00
7b3abf0ec5 cgen: wwstage fnretlookup resolves cross-module callee f64 return type (#98/#101)
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.
2026-05-25 13:48:31 +09:00
6f8b658c17 lib/math: inline floats f64bits/f64frombits reinterpret deref
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.
2026-05-25 13:29:35 +09:00
389f314473 test: math::floats fold-1 runtime regression 2026-05-25 13:17:47 +09:00
253f13fa3b lib/math: port math::floats fold-1 (f64 classify/sign/bits) 2026-05-25 13:14:10 +09:00
ec184aade7 test: f64 cgen deref-load + NaN-compare runtime regression (#96/#97)
#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.
2026-05-25 13:04:14 +09:00