Skip to content

Commit 965d4db

Browse files
% of 64-bit integers is an integer remainder (#571)
`%` 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 <noreply@anthropic.com>
1 parent 00f98d8 commit 965d4db

4 files changed

Lines changed: 83 additions & 1 deletion

File tree

‎tslang/lib/TypeScript/LowerToLLVM.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4036,6 +4036,13 @@ struct ArithmeticBinaryOpLowering : public TsLlvmPattern<mlir_ts::ArithmeticBina
40364036
transformed.getOperand2(), rewriter);
40374037

40384038
case SyntaxKind::PercentToken:
4039+
// the lowered operand is signless: an unsigned one (`u64 % u64`) takes the unsigned remainder
4040+
if (arithmeticBinaryOp.getOperand1().getType().isUnsignedInteger())
4041+
{
4042+
return BinOp<mlir_ts::ArithmeticBinaryOp, arith::RemUIOp, arith::RemFOp>(arithmeticBinaryOp, transformed.getOperand1(),
4043+
transformed.getOperand2(), rewriter);
4044+
}
4045+
40394046
return BinOp<mlir_ts::ArithmeticBinaryOp, arith::RemSIOp, arith::RemFOp, arith::RemUIOp>(arithmeticBinaryOp, transformed.getOperand1(),
40404047
transformed.getOperand2(), rewriter);
40414048

‎tslang/lib/TypeScript/MLIRGenImpl.h‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6815,8 +6815,32 @@ class MLIRGenImpl
68156815

68166816
break;
68176817
}
6818-
case SyntaxKind::SlashToken:
68196818
case SyntaxKind::PercentToken:
6819+
{
6820+
// `%` of two integers, one of them 64-bit, is done in that 64-bit type: through a
6821+
// number (f64) it was wrong once the value passed 2^53. Narrower integers keep the
6822+
// number path, which is exact for them.
6823+
auto leftIntType = dyn_cast<mlir::IntegerType>(leftExpressionValue.getType());
6824+
auto rightIntType = dyn_cast<mlir::IntegerType>(rightExpressionValue.getType());
6825+
if (leftIntType && rightIntType && (leftIntType.getWidth() > 32 || rightIntType.getWidth() > 32))
6826+
{
6827+
mlir::Type intType = leftIntType.getWidth() >= rightIntType.getWidth() ? leftIntType : rightIntType;
6828+
if (leftExpressionValue.getType() != intType)
6829+
{
6830+
CAST(leftExpressionValue, location, intType, leftExpressionValue, genContext);
6831+
}
6832+
6833+
if (rightExpressionValue.getType() != intType)
6834+
{
6835+
CAST(rightExpressionValue, location, intType, rightExpressionValue, genContext);
6836+
}
6837+
6838+
break;
6839+
}
6840+
6841+
[[fallthrough]];
6842+
}
6843+
case SyntaxKind::SlashToken:
68206844
case SyntaxKind::AsteriskAsteriskToken:
68216845

68226846
// TODO: should it be int type especially PercentToken?

‎tslang/test/tester/CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ tslang_add_test(NAME test-compile-00-throw-number-untyped-catch COMMAND test-run
463463
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")
464464
tslang_add_test(NAME test-compile-00-catch-untyped-value COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00catch_untyped_value.ts")
465465
tslang_add_test(NAME test-compile-00-throw-bool-bigint COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_bool_bigint.ts")
466+
tslang_add_test(NAME test-compile-00-int64-remainder COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00int64_remainder.ts")
466467
tslang_add_test(NAME test-compile-00-throw-f32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_f32.ts")
467468
tslang_add_test(NAME test-compile-00-throw-u32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_u32.ts")
468469
tslang_add_test(NAME test-compile-00-cast-to-f32 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00cast_to_f32.ts")
@@ -998,6 +999,7 @@ tslang_add_test(NAME test-jit-00-throw-number-untyped-catch COMMAND test-runner
998999
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")
9991000
tslang_add_test(NAME test-jit-00-catch-untyped-value COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00catch_untyped_value.ts")
10001001
tslang_add_test(NAME test-jit-00-throw-bool-bigint COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_bool_bigint.ts")
1002+
tslang_add_test(NAME test-jit-00-int64-remainder COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00int64_remainder.ts")
10011003
tslang_add_test(NAME test-jit-00-throw-f32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_f32.ts")
10021004
tslang_add_test(NAME test-jit-00-throw-u32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00throw_u32.ts")
10031005
tslang_add_test(NAME test-jit-00-cast-to-f32 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00cast_to_f32.ts")
@@ -2060,6 +2062,7 @@ set(TSLANG_CORPUS
20602062
00symbol.ts
20612063
00throw_in_catch.ts
20622064
00throw_bool_bigint.ts
2065+
00int64_remainder.ts
20632066
00throw_f32.ts
20642067
00throw_u32.ts
20652068
00throw_in_block_debug_info.ts
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// regression test: `%` of 64-bit integers is an integer remainder.
2+
//
3+
// bug found and fixed: `%` cast both operands to number (f64), so a u64 remainder was wrong once the
4+
// value passed 2^53 (the low byte of "main_vs\0" read 108 instead of 109). An unsigned operand
5+
// also has to take the unsigned remainder.
6+
7+
function main() {
8+
const BYTE: u64 = 256;
9+
let w: u64 = 32499774568096109; // the bytes of "main_vs\0", little-endian
10+
const m: u64 = 109;
11+
assert(w % BYTE == m);
12+
assert(w % 256 == m);
13+
14+
let r: u64 = 32499774568096109;
15+
r %= 256;
16+
assert(r == m);
17+
18+
// read every byte back
19+
const expected = [109, 97, 105, 110, 95, 118, 115, 0];
20+
let word: u64 = 32499774568096109;
21+
for (const e of expected) {
22+
const byte: u64 = e;
23+
assert(word % BYTE == byte);
24+
word = word >> 8;
25+
}
26+
27+
// all 64 bits set: unsigned, not -1
28+
let max: u64 = 18446744073709551615;
29+
const five: u64 = 5;
30+
const one: u64 = 1;
31+
assert(max % 10 == five);
32+
assert(max % 7 == one);
33+
34+
// signed: the remainder takes the dividend's sign
35+
let s: s64 = -32499774568096109;
36+
const minus109: s64 = -109;
37+
const minus340597691: s64 = -340597691;
38+
assert(s % 256 == minus109);
39+
assert(s % 1000000007 == minus340597691);
40+
41+
// narrower integers keep the number path
42+
let i: s32 = 1000;
43+
assert(i % 7 == 6);
44+
let n: number = 7.5;
45+
assert(n % 2 == 1.5);
46+
47+
print("done.");
48+
}

0 commit comments

Comments
 (0)