From b7e1ad1a4b39eabaee008096cdebd1e75ba7967c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 25 May 2026 11:09:55 +0900 Subject: [PATCH] lib/math: port math::checked (overflow + saturating arithmetic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 5 + lib/math/checked/checked.ww | 159 +++++++++++++ lib/math/checked/checked_test.ww | 386 +++++++++++++++++++++++++++++++ lib/math/checked/saturating.ww | 169 ++++++++++++++ test/wcc/969_checked_run.c | 51 ++++ 5 files changed, 770 insertions(+) create mode 100644 lib/math/checked/checked.ww create mode 100644 lib/math/checked/checked_test.ww create mode 100644 lib/math/checked/saturating.ww create mode 100644 test/wcc/969_checked_run.c diff --git a/Makefile b/Makefile index 7ff4e480..040f2c96 100644 --- a/Makefile +++ b/Makefile @@ -325,6 +325,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_adler32_run $(BIN)/test_crc16_run \ $(BIN)/test_crc32_run $(BIN)/test_crc64_run \ $(BIN)/test_siphash_run \ + $(BIN)/test_checked_run \ $(BIN)/test_bufio_run $(BIN)/test_random_run $(BIN)/test_smoke: test/wcc/000_smoke.c $(LIB)/libwcc.a | $(BIN) @@ -1048,6 +1049,10 @@ $(BIN)/test_random_run: test/wcc/999_random_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_checked_run: test/wcc/969_checked_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + sizelint: @sh tools/sizelint diff --git a/lib/math/checked/checked.ww b/lib/math/checked/checked.ww new file mode 100644 index 00000000..908d20ba --- /dev/null +++ b/lib/math/checked/checked.ww @@ -0,0 +1,159 @@ +// math/checked — overflow-checked integer arithmetic. Ported from +// Hare's math::checked (ref/hare/math/checked/checked.ha). add*/sub*/ +// mul* return (result, overflow) with wrapping semantics. The saturating +// (clamp-on-overflow) siblings live in saturating.ww — same `checked` +// module. +// +// Subset of Hare's surface (Hare splits per type, so these are clean +// omissions, not divergences): +// - size-typed *z variants (addz/subz/mulz): no `size` type yet (#85). +// - int/uint native-width variants: ww int/uint are 64-bit (type.c), +// so they'd silently behave like the i64/u64 forms — a width +// divergence in the overflow boundary. +// - 64-bit muls (muli64/mulu64/powi64) and the muli/mulu dispatchers: +// need math::mulu64 (128-bit product), not yet exposed by lib/math. + +package checked; + +export fn addi8(a: i8, b: i8) (i8, bool) = { + let res: i8 = a + b; + let overflow: bool = a < 0 == b < 0 && a < 0 != res < 0; + return (res, overflow); +}; + +export fn addi16(a: i16, b: i16) (i16, bool) = { + let res: i16 = a + b; + let overflow: bool = a < 0 == b < 0 && a < 0 != res < 0; + return (res, overflow); +}; + +export fn addi32(a: i32, b: i32) (i32, bool) = { + let res: i32 = a + b; + let overflow: bool = a < 0 == b < 0 && a < 0 != res < 0; + return (res, overflow); +}; + +export fn addi64(a: i64, b: i64) (i64, bool) = { + let res: i64 = a + b; + let overflow: bool = a < 0 == b < 0 && a < 0 != res < 0; + return (res, overflow); +}; + +export fn addu8(a: u8, b: u8) (u8, bool) = { + let res: u8 = a + b; + let overflow: bool = res < a; + return (res, overflow); +}; + +export fn addu16(a: u16, b: u16) (u16, bool) = { + let res: u16 = a + b; + let overflow: bool = res < a; + return (res, overflow); +}; + +export fn addu32(a: u32, b: u32) (u32, bool) = { + let res: u32 = a + b; + let overflow: bool = res < a; + return (res, overflow); +}; + +export fn addu64(a: u64, b: u64) (u64, bool) = { + let res: u64 = a + b; + let overflow: bool = res < a; + return (res, overflow); +}; + +export fn subi8(a: i8, b: i8) (i8, bool) = { + let res: i8 = a - b; + let overflow: bool = a < 0 != b < 0 && a < 0 != res < 0; + return (res, overflow); +}; + +export fn subi16(a: i16, b: i16) (i16, bool) = { + let res: i16 = a - b; + let overflow: bool = a < 0 != b < 0 && a < 0 != res < 0; + return (res, overflow); +}; + +export fn subi32(a: i32, b: i32) (i32, bool) = { + let res: i32 = a - b; + let overflow: bool = a < 0 != b < 0 && a < 0 != res < 0; + return (res, overflow); +}; + +export fn subi64(a: i64, b: i64) (i64, bool) = { + let res: i64 = a - b; + let overflow: bool = a < 0 != b < 0 && a < 0 != res < 0; + return (res, overflow); +}; + +export fn subu8(a: u8, b: u8) (u8, bool) = { + let res: u8 = a - b; + let overflow: bool = res > a; + return (res, overflow); +}; + +export fn subu16(a: u16, b: u16) (u16, bool) = { + let res: u16 = a - b; + let overflow: bool = res > a; + return (res, overflow); +}; + +export fn subu32(a: u32, b: u32) (u32, bool) = { + let res: u32 = a - b; + let overflow: bool = res > a; + return (res, overflow); +}; + +export fn subu64(a: u64, b: u64) (u64, bool) = { + let res: u64 = a - b; + let overflow: bool = res > a; + return (res, overflow); +}; + +// muli8: Hare compares the truncated `res` against the full-width +// product directly (checked.ha:321 `res != fullres`), relying on its +// implicit i8->int promotion. ww has no implicit integer promotion +// (cmd/wcc/check.c unify_arith errors on differing concrete types), so +// the widening cast is written explicitly. Same shape in the muls below. +export fn muli8(a: i8, b: i8) (i8, bool) = { + let fullres: int = a: int * b: int; + let res: i8 = fullres: i8; + let overflow: bool = res: int != fullres; + return (res, overflow); +}; + +export fn muli16(a: i16, b: i16) (i16, bool) = { + let fullres: int = a: int * b: int; + let res: i16 = fullres: i16; + let overflow: bool = res: int != fullres; + return (res, overflow); +}; + +export fn muli32(a: i32, b: i32) (i32, bool) = { + let fullres: i64 = a: i64 * b: i64; + let res: i32 = fullres: i32; + let overflow: bool = res: i64 != fullres; + return (res, overflow); +}; + +export fn mulu8(a: u8, b: u8) (u8, bool) = { + let fullres: uint = a: uint * b: uint; + let res: u8 = fullres: u8; + let overflow: bool = res: uint != fullres; + return (res, overflow); +}; + +export fn mulu16(a: u16, b: u16) (u16, bool) = { + let fullres: uint = a: uint * b: uint; + let res: u16 = fullres: u16; + let overflow: bool = res: uint != fullres; + return (res, overflow); +}; + +export fn mulu32(a: u32, b: u32) (u32, bool) = { + let fullres: u64 = a: u64 * b: u64; + let res: u32 = fullres: u32; + let overflow: bool = res: u64 != fullres; + return (res, overflow); +}; diff --git a/lib/math/checked/checked_test.ww b/lib/math/checked/checked_test.ww new file mode 100644 index 00000000..111ad218 --- /dev/null +++ b/lib/math/checked/checked_test.ww @@ -0,0 +1,386 @@ +package checked; + +// Directory import pulls both checked.ww and saturating.ww (the two +// files of this module); a bare `import checked` would resolve only to +// the checked.ww file. Referenced as `checked.*` (last path component). +import math.checked; +import types; + +// Vectors ported verbatim from ref/hare/math/checked/checked.ha @test +// blocks. The subi* boundary cases there feed types::I{8,16,32,64}_{MAX, +// MIN}; written here as the literal boundary values (the value IS the +// boundary) — a test vector, not a stand-in for the types def. Restore +// the types-symbol form once it reads cleanly cross-module (#88). + +@test fn test_addi8() void = { + let (res, overflow) = checked.addi8(100, 20); + if (res != 120) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.addi8(100, 50); + if (res2 != -106) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_addi16() void = { + let (res, overflow) = checked.addi16(32700, 60); + if (res != 32760) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.addi16(32700, 100); + if (res2 != -32736) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_addi32() void = { + let (res, overflow) = checked.addi32(2147483600, 40); + if (res != 2147483640) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.addi32(2147483600, 100); + if (res2 != -2147483596) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_addi64() void = { + let (res, overflow) = checked.addi64(9223372036854775800, 5); + if (res != 9223372036854775805) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.addi64(9223372036854775800, 10); + if (res2 != -9223372036854775806) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_addu8() void = { + let (res, overflow) = checked.addu8(200u8, 50u8); + if (res != 250u8) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.addu8(200u8, 100u8); + if (res2 != 44u8) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_addu16() void = { + let (res, overflow) = checked.addu16(65500u16, 30u16); + if (res != 65530u16) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.addu16(65500u16, 50u16); + if (res2 != 14u16) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_addu32() void = { + let (res, overflow) = checked.addu32(4294967200u32, 90u32); + if (res != 4294967290u32) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.addu32(4294967200u32, 100u32); + if (res2 != 4u32) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_addu64() void = { + let (res, overflow) = checked.addu64(18446744073709551600u64, 10u64); + if (res != 18446744073709551610u64) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.addu64(18446744073709551610u64, 50u64); + if (res2 != 44u64) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_subi8() void = { + let (res, overflow) = checked.subi8(-100, 20); + if (res != -120) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.subi8(-100, 50); + if (res2 != 106) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; + // I8_MAX, I8_MIN boundary + let (res3, overflow3) = checked.subi8(127, -128); + if (res3 != -1) { let _: i32 = 1/0; }; + if (!overflow3) { let _: i32 = 1/0; }; +}; + +@test fn test_subi16() void = { + let (res, overflow) = checked.subi16(-32700, 60); + if (res != -32760) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.subi16(-32700, 100); + if (res2 != 32736) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; + // I16_MAX, I16_MIN boundary + let (res3, overflow3) = checked.subi16(32767, -32768); + if (res3 != -1) { let _: i32 = 1/0; }; + if (!overflow3) { let _: i32 = 1/0; }; +}; + +@test fn test_subi32() void = { + let (res, overflow) = checked.subi32(-2147483600, 40); + if (res != -2147483640) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.subi32(-2147483600, 100); + if (res2 != 2147483596) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; + // I32_MAX, I32_MIN boundary + let (res3, overflow3) = checked.subi32(2147483647, -2147483648); + if (res3 != -1) { let _: i32 = 1/0; }; + if (!overflow3) { let _: i32 = 1/0; }; +}; + +@test fn test_subi64() void = { + let (res, overflow) = checked.subi64(-9223372036854775800, 5); + if (res != -9223372036854775805) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.subi64(-9223372036854775800, 10); + if (res2 != 9223372036854775806) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; + // I64_MAX, I64_MIN boundary — Hare's subi64 @test 3rd case + // subi64(I64_MAX, I64_MIN). Omitted while #89 is open: the I64_MIN + // literal -9223372036854775808 makes wwstage emit `MOVQ $-, AX` + // (digits dropped — abs(i64-min) overflow in the wwstage literal + // formatter) vs cstage's correct value, a rule-10 break. The + // i8/i16/i32 boundary cases above are unaffected; restore this one + // once #89 lands. +}; + +@test fn test_subu8() void = { + let (res, overflow) = checked.subu8(250u8, 50u8); + if (res != 200u8) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.subu8(44u8, 100u8); + if (res2 != 200u8) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_subu16() void = { + let (res, overflow) = checked.subu16(65530u16, 30u16); + if (res != 65500u16) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.subu16(14u16, 50u16); + if (res2 != 65500u16) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_subu32() void = { + let (res, overflow) = checked.subu32(4294967290u32, 90u32); + if (res != 4294967200u32) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.subu32(4u32, 100u32); + if (res2 != 4294967200u32) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_subu64() void = { + let (res, overflow) = checked.subu64(18446744073709551610u64, 10u64); + if (res != 18446744073709551600u64) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.subu64(44u64, 50u64); + if (res2 != 18446744073709551610u64) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_muli8() void = { + let (res, overflow) = checked.muli8(11, 11); + if (res != 121) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.muli8(12, 12); + if (res2 != -112) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_muli16() void = { + let (res, overflow) = checked.muli16(181, 181); + if (res != 32761) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.muli16(182, 182); + if (res2 != -32412) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_muli32() void = { + let (res, overflow) = checked.muli32(46340, 46340); + if (res != 2147395600) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.muli32(46341, 46341); + if (res2 != -2147479015) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_mulu8() void = { + let (res, overflow) = checked.mulu8(15u8, 15u8); + if (res != 225u8) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.mulu8(16u8, 16u8); + if (res2 != 0u8) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_mulu16() void = { + let (res, overflow) = checked.mulu16(255u16, 255u16); + if (res != 65025u16) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.mulu16(256u16, 256u16); + if (res2 != 0u16) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +@test fn test_mulu32() void = { + let (res, overflow) = checked.mulu32(65535u32, 65535u32); + if (res != 4294836225u32) { let _: i32 = 1/0; }; + if (overflow) { let _: i32 = 1/0; }; + let (res2, overflow2) = checked.mulu32(65536u32, 65536u32); + if (res2 != 0u32) { let _: i32 = 1/0; }; + if (!overflow2) { let _: i32 = 1/0; }; +}; + +// Saturating vectors from ref/hare/math/checked/saturating.ha @test +// blocks. Clamp cases compare to the types limit (as Hare does); the +// I64_MIN comparisons use types.I64_MIN rather than the literal +// -9223372036854775808 (which the wwstage formatter miscompiles, #89). + +@test fn test_sat_addi8() void = { + if (checked.sat_addi8(100, 20) != 120) { let _: i32 = 1/0; }; + if (checked.sat_addi8(100, 50) != types.I8_MAX) { let _: i32 = 1/0; }; + if (checked.sat_addi8(-100, -50) != types.I8_MIN) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_addi16() void = { + if (checked.sat_addi16(32700, 60) != 32760) { let _: i32 = 1/0; }; + if (checked.sat_addi16(32700, 100) != types.I16_MAX) { let _: i32 = 1/0; }; + if (checked.sat_addi16(-32700, -100) != types.I16_MIN) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_addi32() void = { + if (checked.sat_addi32(2147483600, 40) != 2147483640) { let _: i32 = 1/0; }; + if (checked.sat_addi32(2147483600, 100) != types.I32_MAX) { let _: i32 = 1/0; }; + if (checked.sat_addi32(-2147483600, -100) != types.I32_MIN) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_addi64() void = { + if (checked.sat_addi64(9223372036854775800, 5) != 9223372036854775805) { let _: i32 = 1/0; }; + if (checked.sat_addi64(9223372036854775800, 10) != types.I64_MAX) { let _: i32 = 1/0; }; + if (checked.sat_addi64(-9223372036854775800, -10) != types.I64_MIN) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_addu8() void = { + if (checked.sat_addu8(200u8, 50u8) != 250u8) { let _: i32 = 1/0; }; + if (checked.sat_addu8(200u8, 100u8) != types.U8_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_addu16() void = { + if (checked.sat_addu16(65500u16, 30u16) != 65530u16) { let _: i32 = 1/0; }; + if (checked.sat_addu16(65500u16, 50u16) != types.U16_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_addu32() void = { + if (checked.sat_addu32(4294967200u32, 90u32) != 4294967290u32) { let _: i32 = 1/0; }; + if (checked.sat_addu32(4294967200u32, 100u32) != types.U32_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_addu64() void = { + if (checked.sat_addu64(18446744073709551600u64, 10u64) != 18446744073709551610u64) { let _: i32 = 1/0; }; + if (checked.sat_addu64(18446744073709551600u64, 50u64) != types.U64_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_subi8() void = { + if (checked.sat_subi8(-100, 20) != -120) { let _: i32 = 1/0; }; + if (checked.sat_subi8(-100, 50) != types.I8_MIN) { let _: i32 = 1/0; }; + if (checked.sat_subi8(100, -50) != types.I8_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_subi16() void = { + if (checked.sat_subi16(-32700, 60) != -32760) { let _: i32 = 1/0; }; + if (checked.sat_subi16(-32700, 100) != types.I16_MIN) { let _: i32 = 1/0; }; + if (checked.sat_subi16(32700, -100) != types.I16_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_subi32() void = { + if (checked.sat_subi32(-2147483600, 40) != -2147483640) { let _: i32 = 1/0; }; + if (checked.sat_subi32(-2147483600, 100) != types.I32_MIN) { let _: i32 = 1/0; }; + if (checked.sat_subi32(2147483600, -100) != types.I32_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_subi64() void = { + if (checked.sat_subi64(-9223372036854775800, 5) != -9223372036854775805) { let _: i32 = 1/0; }; + if (checked.sat_subi64(-9223372036854775800, 10) != types.I64_MIN) { let _: i32 = 1/0; }; + if (checked.sat_subi64(9223372036854775800, -10) != types.I64_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_muli8() void = { + if (checked.sat_muli8(11, 11) != 121) { let _: i32 = 1/0; }; + if (checked.sat_muli8(12, 12) != types.I8_MAX) { let _: i32 = 1/0; }; + if (checked.sat_muli8(12, -12) != types.I8_MIN) { let _: i32 = 1/0; }; + if (checked.sat_muli8(-12, 12) != types.I8_MIN) { let _: i32 = 1/0; }; + if (checked.sat_muli8(-12, -12) != types.I8_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_muli16() void = { + if (checked.sat_muli16(181, 181) != 32761) { let _: i32 = 1/0; }; + if (checked.sat_muli16(182, 182) != types.I16_MAX) { let _: i32 = 1/0; }; + if (checked.sat_muli16(182, -182) != types.I16_MIN) { let _: i32 = 1/0; }; + if (checked.sat_muli16(-182, 182) != types.I16_MIN) { let _: i32 = 1/0; }; + if (checked.sat_muli16(-182, -182) != types.I16_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_muli32() void = { + if (checked.sat_muli32(46340, 46340) != 2147395600) { let _: i32 = 1/0; }; + if (checked.sat_muli32(46341, 46341) != types.I32_MAX) { let _: i32 = 1/0; }; + if (checked.sat_muli32(46341, -46341) != types.I32_MIN) { let _: i32 = 1/0; }; + if (checked.sat_muli32(-46341, 46341) != types.I32_MIN) { let _: i32 = 1/0; }; + if (checked.sat_muli32(-46341, -46341) != types.I32_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_mulu8() void = { + if (checked.sat_mulu8(15u8, 15u8) != 225u8) { let _: i32 = 1/0; }; + if (checked.sat_mulu8(16u8, 16u8) != types.U8_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_mulu16() void = { + if (checked.sat_mulu16(255u16, 255u16) != 65025u16) { let _: i32 = 1/0; }; + if (checked.sat_mulu16(256u16, 256u16) != types.U16_MAX) { let _: i32 = 1/0; }; +}; + +@test fn test_sat_mulu32() void = { + if (checked.sat_mulu32(65535u32, 65535u32) != 4294836225u32) { let _: i32 = 1/0; }; + if (checked.sat_mulu32(65536u32, 65536u32) != types.U32_MAX) { let _: i32 = 1/0; }; +}; + +export fn main() i32 = { + test_addi8(); + test_addi16(); + test_addi32(); + test_addi64(); + test_addu8(); + test_addu16(); + test_addu32(); + test_addu64(); + test_subi8(); + test_subi16(); + test_subi32(); + test_subi64(); + test_subu8(); + test_subu16(); + test_subu32(); + test_subu64(); + test_muli8(); + test_muli16(); + test_muli32(); + test_mulu8(); + test_mulu16(); + test_mulu32(); + test_sat_addi8(); + test_sat_addi16(); + test_sat_addi32(); + test_sat_addi64(); + test_sat_addu8(); + test_sat_addu16(); + test_sat_addu32(); + test_sat_addu64(); + test_sat_subi8(); + test_sat_subi16(); + test_sat_subi32(); + test_sat_subi64(); + test_sat_muli8(); + test_sat_muli16(); + test_sat_muli32(); + test_sat_mulu8(); + test_sat_mulu16(); + test_sat_mulu32(); + return 0; +}; diff --git a/lib/math/checked/saturating.ww b/lib/math/checked/saturating.ww new file mode 100644 index 00000000..27deb6c4 --- /dev/null +++ b/lib/math/checked/saturating.ww @@ -0,0 +1,169 @@ +// math/checked (saturating) — clamp-on-overflow arithmetic. Ported from +// Hare's math::checked saturating siblings +// (ref/hare/math/checked/saturating.ha). Part of the same `checked` +// module as checked.ww. Each sat_* clamps to the type's range on +// overflow instead of wrapping. +// +// Three ww adaptations vs Hare, all forced by language differences, none +// behavioral: +// - no if-as-expression (N_IF is statement-only, see strings.ww), so +// Hare's `return if (c) X else Y` becomes an if-statement. +// - unsigned overflow tests rely on inline wraparound in Hare; ww +// truncates sub-word arithmetic only on store to a typed lvalue, so +// the wrap is forced through a typed `res`. +// - the mul overflow compares need an explicit widening cast (no +// implicit promotion in ww). +// +// Deferred (faithful subset, Hare splits per type): sat_subu8/16/32/64 +// (reference types::U*_MIN, not yet exported by lib/types); sat_*z (no +// `size` type, #85); sat_muli64/sat_mulu64/sat_muli/sat_mulu (need +// math::mulu64); int/uint native-width sat_* (ww int is 64-bit — a width +// divergence in the overflow boundary). + +package checked; + +import types; + +export fn sat_addi8(a: i8, b: i8) i8 = { + let res: i8 = a + b; + if (a < 0 == b < 0 && a < 0 != res < 0) { + if (res < 0) { return types.I8_MAX; }; + return types.I8_MIN; + }; + return res; +}; + +export fn sat_addi16(a: i16, b: i16) i16 = { + let res: i16 = a + b; + if (a < 0 == b < 0 && a < 0 != res < 0) { + if (res < 0) { return types.I16_MAX; }; + return types.I16_MIN; + }; + return res; +}; + +export fn sat_addi32(a: i32, b: i32) i32 = { + let res: i32 = a + b; + if (a < 0 == b < 0 && a < 0 != res < 0) { + if (res < 0) { return types.I32_MAX; }; + return types.I32_MIN; + }; + return res; +}; + +export fn sat_addi64(a: i64, b: i64) i64 = { + let res: i64 = a + b; + if (a < 0 == b < 0 && a < 0 != res < 0) { + if (res < 0) { return types.I64_MAX; }; + return types.I64_MIN; + }; + return res; +}; + +export fn sat_addu8(a: u8, b: u8) u8 = { + let res: u8 = a + b; + if (res < a) { return types.U8_MAX; }; + return res; +}; + +export fn sat_addu16(a: u16, b: u16) u16 = { + let res: u16 = a + b; + if (res < a) { return types.U16_MAX; }; + return res; +}; + +export fn sat_addu32(a: u32, b: u32) u32 = { + let res: u32 = a + b; + if (res < a) { return types.U32_MAX; }; + return res; +}; + +export fn sat_addu64(a: u64, b: u64) u64 = { + let res: u64 = a + b; + if (res < a) { return types.U64_MAX; }; + return res; +}; + +export fn sat_subi8(a: i8, b: i8) i8 = { + let res: i8 = a - b; + if (a < 0 != b < 0 && a < 0 != res < 0) { + if (res < 0) { return types.I8_MAX; }; + return types.I8_MIN; + }; + return res; +}; + +export fn sat_subi16(a: i16, b: i16) i16 = { + let res: i16 = a - b; + if (a < 0 != b < 0 && a < 0 != res < 0) { + if (res < 0) { return types.I16_MAX; }; + return types.I16_MIN; + }; + return res; +}; + +export fn sat_subi32(a: i32, b: i32) i32 = { + let res: i32 = a - b; + if (a < 0 != b < 0 && a < 0 != res < 0) { + if (res < 0) { return types.I32_MAX; }; + return types.I32_MIN; + }; + return res; +}; + +export fn sat_subi64(a: i64, b: i64) i64 = { + let res: i64 = a - b; + if (a < 0 != b < 0 && a < 0 != res < 0) { + if (res < 0) { return types.I64_MAX; }; + return types.I64_MIN; + }; + return res; +}; + +export fn sat_muli8(a: i8, b: i8) i8 = { + let fullres: int = a: int * b: int; + let res: i8 = fullres: i8; + if (res: int != fullres) { + if (res < 0) { return types.I8_MAX; }; + return types.I8_MIN; + }; + return res; +}; + +export fn sat_muli16(a: i16, b: i16) i16 = { + let fullres: int = a: int * b: int; + let res: i16 = fullres: i16; + if (res: int != fullres) { + if (res < 0) { return types.I16_MAX; }; + return types.I16_MIN; + }; + return res; +}; + +export fn sat_muli32(a: i32, b: i32) i32 = { + let fullres: i64 = a: i64 * b: i64; + let res: i32 = fullres: i32; + if (res: i64 != fullres) { + if (res < 0) { return types.I32_MAX; }; + return types.I32_MIN; + }; + return res; +}; + +export fn sat_mulu8(a: u8, b: u8) u8 = { + let res: uint = a: uint * b: uint; + if (res > (types.U8_MAX): uint) { return types.U8_MAX; }; + return res: u8; +}; + +export fn sat_mulu16(a: u16, b: u16) u16 = { + let res: uint = a: uint * b: uint; + if (res > (types.U16_MAX): uint) { return types.U16_MAX; }; + return res: u16; +}; + +export fn sat_mulu32(a: u32, b: u32) u32 = { + let res: u64 = a: u64 * b: u64; + if (res > (types.U32_MAX): u64) { return types.U32_MAX; }; + return res: u32; +}; diff --git a/test/wcc/969_checked_run.c b/test/wcc/969_checked_run.c new file mode 100644 index 00000000..d751ea2b --- /dev/null +++ b/test/wcc/969_checked_run.c @@ -0,0 +1,51 @@ +/* + * 969_checked_run — execute the lib/math/checked @test fixture under + * the C-side `ww run` driver and assert exit 0. + * + * Sibling to 966_strings_run / 967_bytes_run / 968_utf8_run / 980_memio + * _run / 999_random_run. checked_test.ww carries its own `export fn + * main()` that drives the @test fns and signals which case failed via + * the exit code, so this file is just a thin wrapper — no @test + * scanning, no synthetic main generation. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "lib/math/checked/checked_test.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "checked_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("checked_run: %s ok\n", src); + return 0; +}