From 7db30bf6093bc8246cc0e1b5d30ee47e5ac3dd2a Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 22 Jun 2026 02:53:16 +0900 Subject: [PATCH] test: migrate callret-unsigned/sar-shr/widen-pad behavior to test/lang @test Three more value-observable behavior families ported from the C corpus to in-language @test, routed by the ratified observability rule (value -> test/lang @test; process-outcome stays in runww). Additive: the .c sources keep running in $(TESTS), so no byte-id coverage is removed -- de-dup is deferred to fold 6 (task #12). 906_callret_unsigned_arith -> callret_unsigned_test: call-result unsigned opcode select keyed by callee return type (#168, the N_CALL twin of gunsigned's module-global #134); operands flow through real calls so the return-type arm is exercised, not N_IDENT. 912_sar_shr -> sar_shr_test: signed >> / >>= must emit SAR not SHR (#136); asserts the i64/i32 value directly, dropping the C 8-bit exit-code encoding. 793_widen_pad_zero -> widen_pad_test: widening a narrow value into a wider tagged slot zeroes the high pad words (#227). --- test/lang/callret_unsigned_test.ww | 54 +++++++++++++++++++ test/lang/sar_shr_test.ww | 84 ++++++++++++++++++++++++++++++ test/lang/widen_pad_test.ww | 72 +++++++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 test/lang/callret_unsigned_test.ww create mode 100644 test/lang/sar_shr_test.ww create mode 100644 test/lang/widen_pad_test.ww diff --git a/test/lang/callret_unsigned_test.ww b/test/lang/callret_unsigned_test.ww new file mode 100644 index 00000000..3196dccf --- /dev/null +++ b/test/lang/callret_unsigned_test.ww @@ -0,0 +1,54 @@ +// callret_unsigned_test — an unsigned value returned from a CALL must select +// the UNSIGNED opcode (DIVQ / SHRQ / JA) on the div / mod / shift / relational +// path, keyed by the callee's RETURN type, migrated from +// test/wcc/906_callret_unsigned_arith_run.c (#168, the N_CALL twin of #134 / +// gunsigned's #25). The wwstage nodeisunsigned had no N_CALL arm, so a +// call-result operand fell to `return false` (signed) → signed IDIVQ/SARQ/JG +// on an unsigned-returning call → silent wrong arithmetic (cstage already read +// the N_CALL result stamp). +// +// The subject IS the CALL-RESULT shape, so each operand MUST flow through a +// real fn call: a `let a = uval();` local-bind would land on the N_IDENT arm +// (which strconv already exercised) and HIDE the bug — gate-blind, the +// bootstrap never divides/shifts a call result by an unsigned type. So the +// rows are distinct call-result SHAPES (div / mod / shift / relational), not a +// data table, mirroring gunsigned's "shapes not data" reasoning. +// +// Every unsigned row uses the high-bit value 0x8000000000000001 so the +// unsigned vs signed op diverges at RUNTIME (not just in the .s) — the @test +// asserts the unsigned semantics directly; the .s byte-id net stays in the +// .c. The signed-returning CONTROL guards against an over-broad fix: a signed +// callee must STILL pick IDIVQ/SARQ. + +package callret_unsigned_test; + +fn uval() u64 = { return 0x8000000000000001u64; }; +fn sval() i64 = { return -100i64; }; +fn sshift() i64 = { return -8i64; }; + +@test fn callret_unsigned_path() void = { + // u64_div_callret: 0x8000..1 / 2 unsigned == 0x4000..0 (signed IDIVQ + // sign-extends the high-bit dividend → 0xC000..1). + assert(uval() / 2u64 == 0x4000000000000000u64); + + // u64_mod_callret: unsigned rem == 1 (signed rem == -1). + assert(uval() % 2u64 == 1u64); + + // u64_shr_callret: SHRQ (logical) == 0x4000..0 (SARQ sign-fills the set + // MSB → 0xC000..0). + assert(uval() >> 1u64 == 0x4000000000000000u64); + + // u64_cmp_callret: 0x8000..1 > 1 is true unsigned (JA), false signed + // (JG reads the high bit as the sign). + assert(uval() > 1u64); +}; + +@test fn callret_signed_ctl() void = { + // i64_div_callret control: -100 / 7 == -14 (toward zero); must KEEP + // IDIVQ — an unsigned div of -100-as-u64 would be huge, not -14. + assert(sval() / 7i64 == -14i64); + + // i64_shr_callret control: -8 >> 1 == -4 via SARQ; SHRQ would zero-fill + // to a large positive. + assert(sshift() >> 1i64 == -4i64); +}; diff --git a/test/lang/sar_shr_test.ww b/test/lang/sar_shr_test.ww new file mode 100644 index 00000000..979dd022 --- /dev/null +++ b/test/lang/sar_shr_test.ww @@ -0,0 +1,84 @@ +// sar_shr_test — a SIGNED right-shift (both plain `>>` and compound `>>=`) +// must emit SAR (arithmetic, sign-fills the MSB), not SHR (logical, zero-fill), +// migrated from test/wcc/912_sar_shr_run.c (#136). Pre-fix BOTH stages emitted +// SHRQ for signed RSHIFT (A_SARQ was absent from the w6a opcode table), so +// `let i: i32 = -200; i >>= 2;` produced 0x3FFFFFCE (1073741774) instead of +// -50 — cs==ww held, so byte-id was GREEN while the runtime was wrong. The C +// .c keeps the .s byte-id net; this file pins the RUNTIME semantics directly, +// dropping the C original's 8-bit exit-code encoding (assert the i64/i32 value +// in-language — the whole point of the @test model). +// +// Unlike gunsigned/idxarg, the bug lives in the shift codegen itself, not in +// how the operand is sourced, so indexing a row's operand and then shifting +// STILL exercises the shift site — a row-loop is appropriate. The VALUE (neg +// vs pos) is the data dimension and loops; the (type, operator) SHAPE selects +// distinct shift sites (i32 vs i64; cgbin `>>` vs compound `>>=`), so it is one +// @test fn per shape over a shared row table. Negatives discriminate SAR from +// SHR; the positive row is a no-regression control (SAR == SHR on positives). + +package sar_shr_test; + +// One shift row: x >> sh (or x >>= sh) must equal want. Each consuming @test +// fixes the operand TYPE and OPERATOR; the rows vary only sign/magnitude. +type shrow = struct { + x: i64, + sh: i64, + want: i64, +}; + +let rows: [3]shrow = [ + shrow { x = -200i64, sh = 2i64, want = -50i64 }, + shrow { x = -8i64, sh = 1i64, want = -4i64 }, + shrow { x = 200i64, sh = 2i64, want = 50i64 }, +]; + +@test fn shift_i64_binop() void = { + let i: i32 = 0; + for (i < len(rows)) { + assert(rows[i].x >> rows[i].sh == rows[i].want); + i += 1; + }; +}; + +@test fn shift_i64_compound() void = { + let i: i32 = 0; + for (i < len(rows)) { + let v: i64 = rows[i].x; + v >>= rows[i].sh; + assert(v == rows[i].want); + i += 1; + }; +}; + +@test fn shift_i32_binop() void = { + let i: i32 = 0; + for (i < len(rows)) { + let x: i32 = rows[i].x: i32; + let r: i32 = x >> (rows[i].sh: i32); + assert(r == rows[i].want: i32); + i += 1; + }; +}; + +@test fn shift_i32_compound() void = { + let i: i32 = 0; + for (i < len(rows)) { + let v: i32 = rows[i].x: i32; + v >>= rows[i].sh: i32; + assert(v == rows[i].want: i32); + i += 1; + }; +}; + +@test fn shift_unsigned_ctl() void = { + // u32 control: 200u32 >> 2 == 50 — SHRQ unchanged by the fix. + let a: u32 = 200u32; + a >>= 2u32; + assert(a == 50u32); + + // u64 high-bit control: the fix must NOT make unsigned use SAR. A + // logical SHRQ of 0x8000..0 >> 1 == 0x4000..0; an over-broad SARQ would + // sign-fill the set MSB → 0xC000..0. + let b: u64 = 0x8000000000000000u64; + assert(b >> 1u64 == 0x4000000000000000u64); +}; diff --git a/test/lang/widen_pad_test.ww b/test/lang/widen_pad_test.ww new file mode 100644 index 00000000..7cbd0bad --- /dev/null +++ b/test/lang/widen_pad_test.ww @@ -0,0 +1,72 @@ +// widen_pad_test — widening a NARROW scalar/float value into a tagged-union +// slot whose payload is WIDER than one word must ZERO the high pad words +// (slot+16, slot+24), not leave them at whatever the frame slot last held, +// migrated from test/wcc/793_widen_pad_zero_run.c (#227). cg_widen_tagged_store +// (cstage) and cgwidentaggedstorebp (wwstage) wrote only the tag (slot+0) and +// value (slot+8) in their scalar/float arms, leaving the rest uninitialised. +// For a >16B union (e.g. `(i64 | str)`, whose str variant makes the slot 32B = +// 4 words) a later `*u8` reinterpret then read stack garbage at slot+16/+24. +// +// Both stages were wrong the SAME way, so the byte-id gates were GREEN while +// the runtime was wrong — dead in the bootstrap corpus. Each shape first +// widens a STR into the slot (which fills slot+16/+24 with the str's len/cap), +// then reassigns a SCALAR / FLOAT into the SAME slot, then reads slot+16/+24 +// back through a `*u8` reinterpret (the bit-pinning idiom from +// 715_tagged_widen_f64). Pre-fix the reassign left the str's stale len/cap in +// the pad; post-fix the pad reads back 0. These are distinct widen SHAPES +// (scalar arm, float arm, repeated-reassign), not data, so per-shape asserts. + +package widen_pad_test; + +@test fn scalar_i64_after_str() void = { + // Reassign a scalar i64 over a str-occupied (i64|str) slot: the str + // write fills slot+16 (len) and slot+24 (cap); the scalar reassign must + // zero them. payload == 123, tag == 0 (i64 is variant 0). + let a: (i64 | str) = "abcdefgh"; + a = 123i64; + let pp: *(i64 | str) = &a; + let pu: *u8 = pp: *u8; + let tagp: *i64 = pu: *i64; + let valp: *i64 = (pu + 8u64): *i64; + let pad1: *i64 = (pu + 16u64): *i64; + let pad2: *i64 = (pu + 24u64): *i64; + assert(*valp == 123i64); + assert(*pad1 == 0i64); + assert(*pad2 == 0i64); + assert(*tagp == 0i64); +}; + +@test fn float_f64_after_str() void = { + // The float arm: reassign an f64 over a str-occupied (f64|str) slot. + // f64 1.0 == 0x3FF0000000000000 == 4607182418800017408. tag == 0. + let a: (f64 | str) = "abcdefgh"; + a = 1.0; + let pp: *(f64 | str) = &a; + let pu: *u8 = pp: *u8; + let tagp: *i64 = pu: *i64; + let valp: *u64 = (pu + 8u64): *u64; + let pad1: *i64 = (pu + 16u64): *i64; + let pad2: *i64 = (pu + 24u64): *i64; + assert(*valp == 4607182418800017408u64); + assert(*pad1 == 0i64); + assert(*pad2 == 0i64); + assert(*tagp == 0i64); +}; + +@test fn scalar_after_str_twice() void = { + // Reassign a scalar over a str TWICE — confirms the tail-zero is emitted + // on EVERY scalar widen (not just a first write), with a str's len/cap + // dirtying the pad in between. Final payload == 222. + let a: (i64 | str) = "firstone"; + a = 11i64; + a = "secondxx"; + a = 222i64; + let pp: *(i64 | str) = &a; + let pu: *u8 = pp: *u8; + let valp: *i64 = (pu + 8u64): *i64; + let pad1: *i64 = (pu + 16u64): *i64; + let pad2: *i64 = (pu + 24u64): *i64; + assert(*valp == 222i64); + assert(*pad1 == 0i64); + assert(*pad2 == 0i64); +};