From 01189f32df573f536c68724acf68f7f3f3c05911 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Sun, 30 Aug 2026 18:14:04 -0500 Subject: [PATCH 1/5] make multiplication fail on overflow instead of wrapping --- src/runtime_z.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++- src/zcode.h | 1 + 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/runtime_z.c b/src/runtime_z.c index 43dd094f..ee976ab0 100644 --- a/src/runtime_z.c +++ b/src/runtime_z.c @@ -2280,6 +2280,53 @@ struct rtroutine rtroutines[] = { {Z_END}, } }, + { + R_MULT_WOULD_OVERFLOW, + 5, + // 0 (param): L (first factor) and L0 (lower part) + // 1 (param): R (second factor) and R0 (lower part) + // 2: L1 (higher part) + // 3: R1 (higher part) + // 4: constant -7 + // Fails on overflow, returns otherwise + (struct zinstr []) { + // We're using Hanna's algorithm from https://intfiction.org/t/an-odd-bit-of-undefined-behavior/80914/5 + // Basically, we do the skeleton of a base-128 long multiplication, only enough to determine if things would overflow or not + // Unlike the "divide by one factor and see if you get another" strategy I originally used, this one is guaranteed to always work + {Z_STORE, {SMALL(REG_LOCAL+4), LARGE(-7 & 0xffff)}}, // Keeping this in a register since we need it several times + {Z_ASHIFT, {VALUE(REG_LOCAL+0), VALUE(REG_LOCAL+4)}, REG_LOCAL+2}, // l1 = l >> 7 + {Z_ASHIFT, {VALUE(REG_LOCAL+1), VALUE(REG_LOCAL+4)}, REG_LOCAL+3}, // r1 = r >> 7 + {Z_JNZ, {VALUE(REG_LOCAL+2)}, 0, 1}, + // l1 == 0 + {Z_JZ, {VALUE(REG_LOCAL+3)}, 0, RFALSE}, // if r1 == 0, no overflow + {Z_AND, {VALUE(REG_LOCAL+1), SMALL(0x7f)}, REG_LOCAL+1}, // r0 = r & $7f + // now we overflow iff (l0 * r1) + ((l0 * r0) >> 7) >= 128 + // we repurpose r0 and r1 as intermediates here because we only need each one once + {Z_MUL, {VALUE(REG_LOCAL+0), VALUE(REG_LOCAL+3)}, REG_LOCAL+3}, + {Z_MUL, {VALUE(REG_LOCAL+0), VALUE(REG_LOCAL+1)}, REG_LOCAL+1}, + {Z_ASHIFT, {VALUE(REG_LOCAL+1), VALUE(REG_LOCAL+4)}, REG_LOCAL+1}, + {Z_ADD, {VALUE(REG_LOCAL+1), VALUE(REG_LOCAL+3)}, REG_LOCAL+1}, + {Z_JL, {VALUE(REG_LOCAL+1), SMALL(128)}, 0, RFALSE}, + {Z_JUMP, {REL_LABEL(2)}}, + + {OP_LABEL(1)}, + // l1 != 0 + {Z_JNZ, {VALUE(REG_LOCAL+3)}, 0, 2}, // if r1 != 0, yes overflow + {Z_AND, {VALUE(REG_LOCAL+0), SMALL(0x7f)}, REG_LOCAL+0}, // l0 = l & $7f + // now we overflow iff (l1 * r0) + ((l0 * r0) >> 7) >= 128 + // this time we repurpose l0 and l1 as our intermediates + {Z_MUL, {VALUE(REG_LOCAL+1), VALUE(REG_LOCAL+2)}, REG_LOCAL+2}, + {Z_MUL, {VALUE(REG_LOCAL+1), VALUE(REG_LOCAL+0)}, REG_LOCAL+0}, + {Z_ASHIFT, {VALUE(REG_LOCAL+0), VALUE(REG_LOCAL+4)}, REG_LOCAL+0}, + {Z_ADD, {VALUE(REG_LOCAL+0), VALUE(REG_LOCAL+2)}, REG_LOCAL+0}, + {Z_JL, {VALUE(REG_LOCAL+0), SMALL(128)}, 0, RFALSE}, + + {OP_LABEL(2)}, // fail + {Z_THROW, {SMALL(0), VALUE(REG_FAILJMP)}}, + + {Z_END} + } + }, { R_TIMES, 2, @@ -2298,8 +2345,13 @@ struct rtroutine rtroutines[] = { {OP_LABEL(2)}, {Z_AND, {VALUE(REG_LOCAL+0), VALUE(REG_3FFF)}, REG_LOCAL+0}, {Z_AND, {VALUE(REG_LOCAL+1), VALUE(REG_3FFF)}, REG_LOCAL+1}, + + // New: we check for overflow, and fail if it happens + // R_MULT_WOULD_OVERFLOW throws to FAILJMP on overflow, so if it returns at all, then it means we're fine to continue + {Z_CALLVN, {ROUTINE(R_MULT_WOULD_OVERFLOW), VALUE(REG_LOCAL+0), VALUE(REG_LOCAL+1)}}, + {Z_MUL, {VALUE(REG_LOCAL+0), VALUE(REG_LOCAL+1)}, REG_LOCAL+0}, - {Z_AND, {VALUE(REG_LOCAL+0), VALUE(REG_3FFF)}, REG_LOCAL+0}, + // {Z_AND, {VALUE(REG_LOCAL+0), VALUE(REG_3FFF)}, REG_LOCAL+0}, // No longer necessary since we've guaranteed no overflow {Z_OR, {VALUE(REG_LOCAL+0), VALUE(REG_4000)}, REG_LOCAL+0}, {Z_RET, {VALUE(REG_LOCAL+0)}}, {Z_END}, diff --git a/src/zcode.h b/src/zcode.h index 256fc4fe..74e84444 100644 --- a/src/zcode.h +++ b/src/zcode.h @@ -465,6 +465,7 @@ enum { // Runtime routines R_PLUS, R_MINUS, + R_MULT_WOULD_OVERFLOW, // Needed for R_TIMES R_TIMES, R_DIVIDED, R_MODULO, From 1283581efc271fa4e50526665025b18497ab8d9f Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Sun, 30 Aug 2026 18:23:41 -0500 Subject: [PATCH 2/5] update documentation --- manual/modules/lang/pages/builtins.adoc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manual/modules/lang/pages/builtins.adoc b/manual/modules/lang/pages/builtins.adoc index 88c50d6b..d1ea30d5 100644 --- a/manual/modules/lang/pages/builtins.adoc +++ b/manual/modules/lang/pages/builtins.adoc @@ -73,8 +73,10 @@ fails. `A` and `B` must be bound to numbers; `C` is unified with their product. If the product is outside the valid range of numbers, the query -succeeds, but the numeric result is unpredictable (i.e. it depends on the -interpreter). +fails. + +NOTE: Before version 1c/03, this predicate would return an incorrect value +instead of failing. ($A divided by $B into $C):: From 0d0d8d90f6b127ab2306973667f328769e92f3f4 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Sun, 30 Aug 2026 18:24:47 -0500 Subject: [PATCH 3/5] update debugger --- src/eval.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/eval.c b/src/eval.c index 3f4cc484..382cb9ec 100644 --- a/src/eval.c +++ b/src/eval.c @@ -1182,8 +1182,11 @@ static int eval_compute(struct eval_state *es, int op, int a, int b, int *res) { } break; case BI_TIMES: - *res = (a * b) & 16383; - return 1; + r = a * b; + if(r < 16384) { + *res = r; + return 1; + } break; case BI_DIVIDED: if(b) { From 19361b32688b3155fb69cd07b8e31929e83925e0 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Sun, 30 Aug 2026 18:29:07 -0500 Subject: [PATCH 4/5] add test case --- test/simple/language/new_multrange.dg | 15 +++++++++++++++ test/simple/language/new_multrange.gold | 2 ++ 2 files changed, 17 insertions(+) create mode 100644 test/simple/language/new_multrange.dg create mode 100644 test/simple/language/new_multrange.gold diff --git a/test/simple/language/new_multrange.dg b/test/simple/language/new_multrange.dg new file mode 100644 index 00000000..5250c7dd --- /dev/null +++ b/test/simple/language/new_multrange.dg @@ -0,0 +1,15 @@ +(program entry point) + 100 times 100 is + (if) (100 times 100 into $X) (then) + $X + (else) + not computable + (endif) + . + 150 times 150 is + (if) (150 times 150 into $Y) (then) + $Y + (else) + not computable + (endif) + . diff --git a/test/simple/language/new_multrange.gold b/test/simple/language/new_multrange.gold new file mode 100644 index 00000000..edcb00ac --- /dev/null +++ b/test/simple/language/new_multrange.gold @@ -0,0 +1,2 @@ +[ZD] 100 times 100 is 10000. 150 times 150 is not computable. +[A] 100 times 100 is 10000. 150 times 150 is 6116. From 0f7f3e7899551e7a6a329c5fbccbeef6545df06c Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Sun, 30 Aug 2026 18:29:45 -0500 Subject: [PATCH 5/5] readme --- readme.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.txt b/readme.txt index 3e9c5f76..45d4cebb 100644 --- a/readme.txt +++ b/readme.txt @@ -45,6 +45,9 @@ Release notes: 1c/03, Lib 1.2.4: + Language: ($ times $ into $) now fails if the product is too + large, rather than succeeding with the wrong result. + Compiler: Patched over a bug with non-ASCII word separators. The problem isn't really fixed, but it will no longer corrupt your game text.