From d884705d8765ce760b35b50b158b21b6b56c109f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 13 Oct 2026 17:37:34 +0100 Subject: [PATCH] `%` of 64-bit integers is an integer remainder `%` cast both operands to number (f64), so a u64 remainder was wrong once the value passed 2^53. When both operands are integers and one is 64-bit, `%` is now done in that type; narrower integers keep the number path, which is exact for them. The lowering took the signed remainder for the signless lowered operand, so an unsigned one now takes `remui`, as `>>` does since #556. `/` still gives a number, as in JavaScript. A 64-bit `x % 0` traps like integer division in C instead of giving NaN. Found reading the bytes of a u64 word (TSLANG_ISSUES.md 16). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/LowerToLLVM.cpp | 7 +++ tslang/lib/TypeScript/MLIRGenImpl.h | 26 +++++++++- tslang/test/tester/CMakeLists.txt | 3 ++ tslang/test/tester/tests/00int64_remainder.ts | 48 +++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00int64_remainder.ts diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 5efe68599..33f6c8ab1 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -4036,6 +4036,13 @@ struct ArithmeticBinaryOpLowering : public TsLlvmPattern(arithmeticBinaryOp, transformed.getOperand1(), + transformed.getOperand2(), rewriter); + } + return BinOp(arithmeticBinaryOp, transformed.getOperand1(), transformed.getOperand2(), rewriter); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index c4b942158..78def80d8 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -6798,8 +6798,32 @@ class MLIRGenImpl break; } - case SyntaxKind::SlashToken: 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. + auto leftIntType = dyn_cast(leftExpressionValue.getType()); + auto rightIntType = dyn_cast(rightExpressionValue.getType()); + if (leftIntType && rightIntType && (leftIntType.getWidth() > 32 || rightIntType.getWidth() > 32)) + { + mlir::Type intType = leftIntType.getWidth() >= rightIntType.getWidth() ? leftIntType : rightIntType; + if (leftExpressionValue.getType() != intType) + { + CAST(leftExpressionValue, location, intType, leftExpressionValue, genContext); + } + + if (rightExpressionValue.getType() != intType) + { + CAST(rightExpressionValue, location, intType, rightExpressionValue, genContext); + } + + break; + } + + [[fallthrough]]; + } + case SyntaxKind::SlashToken: case SyntaxKind::AsteriskAsteriskToken: // TODO: should it be int type especially PercentToken? diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 891d8b56e..df7c1d891 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -461,6 +461,7 @@ tslang_add_test(NAME test-compile-00-throw-number-untyped-catch COMMAND test-run tslang_add_test(NAME test-compile-00-throw-in-block-debug-info COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_in_block_debug_info.ts") 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-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") @@ -992,6 +993,7 @@ tslang_add_test(NAME test-jit-00-throw-number-untyped-catch COMMAND test-runner tslang_add_test(NAME test-jit-00-throw-in-block-debug-info COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_in_block_debug_info.ts") 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-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") @@ -2048,6 +2050,7 @@ set(TSLANG_CORPUS 00symbol.ts 00throw_in_catch.ts 00throw_bool_bigint.ts + 00int64_remainder.ts 00throw_f32.ts 00throw_u32.ts 00throw_in_block_debug_info.ts diff --git a/tslang/test/tester/tests/00int64_remainder.ts b/tslang/test/tester/tests/00int64_remainder.ts new file mode 100644 index 000000000..113103a49 --- /dev/null +++ b/tslang/test/tester/tests/00int64_remainder.ts @@ -0,0 +1,48 @@ +// regression test: `%` of 64-bit integers is an integer remainder. +// +// bug found and fixed: `%` cast both operands to number (f64), so a u64 remainder was wrong once the +// value passed 2^53 (the low byte of "main_vs\0" read 108 instead of 109). An unsigned operand +// also has to take the unsigned remainder. + +function main() { + const BYTE: u64 = 256; + let w: u64 = 32499774568096109; // the bytes of "main_vs\0", little-endian + const m: u64 = 109; + assert(w % BYTE == m); + assert(w % 256 == m); + + let r: u64 = 32499774568096109; + r %= 256; + assert(r == m); + + // read every byte back + const expected = [109, 97, 105, 110, 95, 118, 115, 0]; + let word: u64 = 32499774568096109; + for (const e of expected) { + const byte: u64 = e; + assert(word % BYTE == byte); + word = word >> 8; + } + + // all 64 bits set: unsigned, not -1 + let max: u64 = 18446744073709551615; + const five: u64 = 5; + const one: u64 = 1; + assert(max % 10 == five); + assert(max % 7 == one); + + // signed: the remainder takes the dividend's sign + let s: s64 = -32499774568096109; + const minus109: s64 = -109; + const minus340597691: s64 = -340597691; + assert(s % 256 == minus109); + assert(s % 1000000007 == minus340597691); + + // narrower integers keep the number path + let i: s32 = 1000; + assert(i % 7 == 6); + let n: number = 7.5; + assert(n % 2 == 1.5); + + print("done."); +}