From eb2963f8d1bd4505517bd29604bb616c5a64e4fc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 16:19:41 +0000 Subject: [PATCH] `%` of two integers of any width is an integer remainder Only a 64-bit operand made `%` an integer remainder (#571); two narrower integers were cast to number. A hash modulo the length of a table - every lookup of a Map or a Set - was an fmod call and two conversions, and modulo a length of 0 it was NaN made into an index. It is one urem/srem now. The result has the integer type, where it was a number. The type: an integer literal that fits takes the other operand's; otherwise the wider one, and of two of a width the left one. A signed and an unsigned one that an s64 holds both of are done in s64, where neither changes its value. 64-bit operands are as they were. Breaking: - the result of `int % int` is an integer, not a number. own_interface_view.ts put it in an object literal's field seen through an interface that declares number, and now declares it `: number`. - a zero divisor is undefined, as integer division by zero is in C (a fault on x86), where it was NaN; so is the least value of a signed type `% -1`. A number on either side is still a number's remainder. 00int_remainder.ts covers s8 to u32, literals on either side, mixed signedness, mixed widths, `%=`, what `>>>` gives, and the number path. Co-Authored-By: Claude Fable 5.2 --- tslang/lib/TypeScript/MLIRGenImpl.h | 60 +++++++++++- tslang/test/tester/CMakeLists.txt | 3 + tslang/test/tester/own/own_interface_view.ts | 3 +- tslang/test/tester/tests/00int64_remainder.ts | 2 +- tslang/test/tester/tests/00int_remainder.ts | 93 +++++++++++++++++++ 5 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 tslang/test/tester/tests/00int_remainder.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index ce1bce53c..3d7077798 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -8593,14 +8593,64 @@ class MLIRGenImpl } case SyntaxKind::PercentToken: { - // `%` of two integers, one of them 64-bit, is done in that 64-bit type: through a - // number (f64) it was wrong once the value passed 2^53. Narrower integers keep the - // number path, which is exact for them. + // `%` of two integers is an integer remainder, and so is its result: through a number + // (f64) a 64-bit value was wrong once it passed 2^53, and a narrower one, though exact, + // was an fmod call and two conversions where one instruction does it (a hash modulo the + // length of a table, on every lookup of a Map). A zero divisor is then undefined, as + // integer division by zero is in C - a fault on x86, not the NaN a number gives - and so is + // the least value of a signed type `% -1`. + // The type: an integer literal that fits takes the other operand's; otherwise the wider + // one, and of two of a width the left one. A signed and an unsigned one that an s64 + // holds both of are done in s64, where neither changes its value. auto leftIntType = dyn_cast(leftExpressionValue.getType()); auto rightIntType = dyn_cast(rightExpressionValue.getType()); - if (leftIntType && rightIntType && (leftIntType.getWidth() > 32 || rightIntType.getWidth() > 32)) + if (leftIntType && rightIntType && leftIntType.getWidth() >= 8 && rightIntType.getWidth() >= 8) { - mlir::Type intType = leftIntType.getWidth() >= rightIntType.getWidth() ? leftIntType : rightIntType; + // a literal that `type` holds as it is + auto literalFits = [&](mlir::Value value, mlir::IntegerType type) { + auto constantOp = value.getDefiningOp(); + if (!constantOp) + { + return false; + } + + auto intAttr = dyn_cast(constantOp.getValue()); + if (!intAttr) + { + return false; + } + + auto literal = intAttr.getValue(); + auto literalIsUnsigned = value.getType().isUnsignedInteger(); + if (literalIsUnsigned ? !literal.isIntN(63) : literal.isNegative()) + { + // negative, or past what an s64 says: only its own type holds it + return false; + } + + return literal.getActiveBits() <= (type.isUnsigned() ? type.getWidth() : type.getWidth() - 1); + }; + + mlir::Type intType; + if (literalFits(rightExpressionValue, leftIntType)) + { + intType = leftIntType; + } + else if (literalFits(leftExpressionValue, rightIntType)) + { + intType = rightIntType; + } + else + { + auto wider = leftIntType.getWidth() >= rightIntType.getWidth() ? leftIntType : rightIntType; + auto narrower = wider == leftIntType ? rightIntType : leftIntType; + // an unsigned type does not hold a signed operand's negative values, nor a + // signed one an unsigned operand of its own width + auto losesValues = wider.isUnsigned() != narrower.isUnsigned() + && (wider.isUnsigned() || wider.getWidth() == narrower.getWidth()); + intType = losesValues && wider.getWidth() <= 32 ? static_cast(SInt(64)) : wider; + } + if (leftExpressionValue.getType() != intType) { CAST(leftExpressionValue, location, intType, leftExpressionValue, genContext); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e7ad024e2..67625d35c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -545,6 +545,7 @@ tslang_add_test(NAME test-compile-00-throw-in-block-debug-info COMMAND test-runn tslang_add_test(NAME test-compile-00-catch-untyped-value COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00catch_untyped_value.ts") tslang_add_test(NAME test-compile-00-throw-bool-bigint COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_bool_bigint.ts") tslang_add_test(NAME test-compile-00-int64-remainder COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00int64_remainder.ts") +tslang_add_test(NAME test-compile-00-int-remainder COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00int_remainder.ts") tslang_add_test(NAME test-compile-00-throw-f32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_f32.ts") tslang_add_test(NAME test-compile-00-throw-u32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_u32.ts") tslang_add_test(NAME test-compile-00-cast-to-f32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cast_to_f32.ts") @@ -1224,6 +1225,7 @@ tslang_add_test(NAME test-jit-00-throw-in-block-debug-info COMMAND test-runner - tslang_add_test(NAME test-jit-00-catch-untyped-value COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00catch_untyped_value.ts") tslang_add_test(NAME test-jit-00-throw-bool-bigint COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_bool_bigint.ts") tslang_add_test(NAME test-jit-00-int64-remainder COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00int64_remainder.ts") +tslang_add_test(NAME test-jit-00-int-remainder COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00int_remainder.ts") tslang_add_test(NAME test-jit-00-throw-f32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_f32.ts") tslang_add_test(NAME test-jit-00-throw-u32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_u32.ts") tslang_add_test(NAME test-jit-00-cast-to-f32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cast_to_f32.ts") @@ -2871,6 +2873,7 @@ set(TSLANG_CORPUS 00throw_in_catch.ts 00throw_bool_bigint.ts 00int64_remainder.ts + 00int_remainder.ts 00throw_f32.ts 00throw_u32.ts 00throw_in_block_debug_info.ts diff --git a/tslang/test/tester/own/own_interface_view.ts b/tslang/test/tester/own/own_interface_view.ts index afc22e958..43671f00e 100644 --- a/tslang/test/tester/own/own_interface_view.ts +++ b/tslang/test/tester/own/own_interface_view.ts @@ -46,7 +46,8 @@ function make(k: number): I { function main() { let t = 0; for (let i = 0; i < 20000; i++) { - const n = i % 10; + // a number: `i % 10` of two integers is an integer, and the field `x` below is the interface's number + const n: number = i % 10; // a temporary class instance given as an interface t += show(new C(n)); diff --git a/tslang/test/tester/tests/00int64_remainder.ts b/tslang/test/tester/tests/00int64_remainder.ts index 113103a49..52116e21c 100644 --- a/tslang/test/tester/tests/00int64_remainder.ts +++ b/tslang/test/tester/tests/00int64_remainder.ts @@ -38,7 +38,7 @@ function main() { assert(s % 256 == minus109); assert(s % 1000000007 == minus340597691); - // narrower integers keep the number path + // narrower integers: an integer remainder too (00int_remainder.ts) let i: s32 = 1000; assert(i % 7 == 6); let n: number = 7.5; diff --git a/tslang/test/tester/tests/00int_remainder.ts b/tslang/test/tester/tests/00int_remainder.ts new file mode 100644 index 000000000..59b9e1f1b --- /dev/null +++ b/tslang/test/tester/tests/00int_remainder.ts @@ -0,0 +1,93 @@ +// regression test: `%` of two integers of any width is an integer remainder with an integer result. +// +// bug found and fixed: only a 64-bit operand made `%` an integer remainder (00int64_remainder.ts); +// two narrower ones were cast to number, so a hash modulo the length of a table - every lookup of a +// Map - was an fmod call and two conversions, and modulo a length of 0 it was NaN made into an index. + +function bucket(hashCode: int, buckets: int[]): int { + return buckets[hashCode % buckets.length]; +} + +function main() { + // signed: the remainder takes the dividend's sign + let i: s32 = 1000; + assert(i % 7 == 6); + let minus: s32 = -1000; + assert(minus % 7 == -6); + assert(i % -7 == 6); + assert(minus % -7 == -6); + + // the result is an integer: it indexes, and it stays one in an integer + const r = i % 7; + const values: s32[] = [0, 1, 2, 3, 4, 5, 6]; + assert(values[r] == 6); + let kept: s32 = i % 7; + assert(kept == 6); + + // unsigned: all 32 bits set is not -1 + let max: u32 = 4294967295; + assert(max % 10 == 5); + assert(max % 7 == 3); + let top: u32 = 2147483648; + const big: u32 = 3000000000; + assert(big % top == 852516352); + + // a literal takes the other operand's type, on either side + let u: u32 = 4000000000; + assert(u % 1000 == 0); + assert(u % 7 == 3); + let seven: u32 = 7; + assert(4000000000 % seven == 3); + + // a signed and an unsigned one: neither changes its value + const three: u32 = 3; + assert(minus % three == -1); + assert(max % minus == 295); + assert(u % -7 == 3); + + // what `>>>` gives is unsigned: all 32 bits set, not -1 + let all: s32 = -1; + assert((all >>> 0) % 5 == 0); + assert((all >>> 0) % seven == 3); + assert((all >>> 0) % i == 295); + + // narrower ones + let b: u8 = 250; + let c: u8 = 7; + assert(b % c == 5); + let small: s8 = -100; + assert(small % 7 == -2); + let h: s16 = -30000; + let k: u16 = 60000; + assert(h % 7 == -5); + assert(k % 7 == 3); + assert(h % k == -30000); + + // of different widths: the wider one + let wide: s64 = -5000000000; + assert(wide % i == 0); + assert(i % wide == 1000); + + // compound + let acc: s32 = 1000; + acc %= 7; + assert(acc == 6); + let uacc: u32 = 4000000000; + uacc %= 7; + assert(uacc == 3); + + // a hash modulo the length of a table + const buckets: int[] = [10, 11, 12, 13, 14]; + assert(bucket(7, buckets) == 12); + assert(bucket(-1, buckets) == 10); // 4294967295 % 5 + + // a number on either side is still a number's remainder + let n: number = 7.5; + assert(n % 2 == 1.5); + assert(i % 2.5 == 0); + assert(7 % n == 7); + const zero: number = 0; + assert(isNaN(n % zero)); + + print("done."); +}