From cbc7c4f48e8750b3b059e9738705d9efd2d8235c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 12 Oct 2026 16:14:42 +0100 Subject: [PATCH] Shifts and bitwise operators keep a 64-bit integer operand's width `<<`, `>>`, `>>>`, `&`, `|` and `^` cast both operands to i32, the JavaScript Int32 coercion of `number`, whatever their type. So `u64 << 40` shifted by 40 mod 32 = 8 and gave 256, and `&`/`|`/`^` lost the upper half of a u64. A left operand that is a 64-bit integer now keeps its type, and the right one is cast to it; narrower operands keep the Int32 rule. Also, `>>` of an unsigned operand shifted the sign bit in: the lowering picked the shift by the lowered (signless) operand type, and constant folding always used an arithmetic shift. Both look at the operand's own type now. Found while porting native_ts_graphics samples (TSLANG_ISSUES.md 3). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/LowerToLLVM.cpp | 7 ++ tslang/lib/TypeScript/MLIRGenImpl.h | 27 +++++-- tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00int64_shift_bitwise.ts | 70 +++++++++++++++++++ 4 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 tslang/test/tester/tests/00int64_shift_bitwise.ts diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 77ab06349..e77719b1a 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -4005,6 +4005,13 @@ struct ArithmeticBinaryOpLowering : public TsLlvmPattern( + arithmeticBinaryOp, transformed.getOperand1(), maskShiftAmount(arithmeticBinaryOp->getLoc(), transformed.getOperand2(), rewriter), rewriter); + } + return BinOp( arithmeticBinaryOp, transformed.getOperand1(), maskShiftAmount(arithmeticBinaryOp->getLoc(), transformed.getOperand2(), rewriter), rewriter); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index ff6123a2c..7130e7dfb 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5744,7 +5744,9 @@ class MLIRGenImpl result = leftInt << rightInt.urem(leftInt.getBitWidth()); break; case SyntaxKind::GreaterThanGreaterThanToken: - result = leftInt.ashr(rightInt.urem(leftInt.getBitWidth())); + result = resultType.isUnsignedInteger() + ? leftInt.lshr(rightInt.urem(leftInt.getBitWidth())) + : leftInt.ashr(rightInt.urem(leftInt.getBitWidth())); break; case SyntaxKind::GreaterThanGreaterThanGreaterThanToken: result = leftInt.lshr(rightInt.urem(leftInt.getBitWidth())); @@ -6733,18 +6735,31 @@ class MLIRGenImpl case SyntaxKind::AmpersandToken: case SyntaxKind::BarToken: case SyntaxKind::CaretToken: - // cast to int - if (leftExpressionValue.getType() != builder.getI32Type()) + { + // cast to int: JavaScript's Int32 coercion, but a 64-bit integer left operand keeps its + // type (and signedness), the right one is cast to it. Cast to i32, `u64 << 40` shifted + // by 40 mod 32 and dropped the upper half. + mlir::Type intType = builder.getI32Type(); + if (auto leftIntType = dyn_cast(leftExpressionValue.getType())) { - CAST(leftExpressionValue, location, builder.getI32Type(), leftExpressionValue, genContext); + if (leftIntType.getWidth() > 32) + { + intType = leftIntType; + } } - if (rightExpressionValue.getType() != builder.getI32Type()) + if (leftExpressionValue.getType() != intType) { - CAST(rightExpressionValue, location, builder.getI32Type(), rightExpressionValue, genContext); + CAST(leftExpressionValue, location, intType, leftExpressionValue, genContext); + } + + if (rightExpressionValue.getType() != intType) + { + CAST(rightExpressionValue, location, intType, rightExpressionValue, genContext); } break; + } case SyntaxKind::SlashToken: case SyntaxKind::PercentToken: case SyntaxKind::AsteriskAsteriskToken: diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index f1d97fc0d..105610c15 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -271,6 +271,7 @@ tslang_add_test(NAME test-compile-01-enums COMMAND test-runner "${PROJECT_SOURCE tslang_add_test(NAME test-compile-00-numbers COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00numbers.ts") tslang_add_test(NAME test-compile-00-equals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00equals.ts") tslang_add_test(NAME test-compile-00-bool-arith-ops COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00bool_arith_ops.ts") +tslang_add_test(NAME test-compile-00-int64-shift-bitwise COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00int64_shift_bitwise.ts") tslang_add_test(NAME test-compile-00-bool-const-array COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00bool_const_array.ts") tslang_add_test(NAME test-compile-00-mixed-type-ops COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00mixed_type_ops.ts") tslang_add_test(NAME test-compile-00-generator-manual-next COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_manual_next.ts") @@ -802,6 +803,7 @@ tslang_add_test(NAME test-jit-01-enums COMMAND test-runner -jit "${PROJECT_SOURC tslang_add_test(NAME test-jit-00-numbers COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00numbers.ts") tslang_add_test(NAME test-jit-00-equals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00equals.ts") tslang_add_test(NAME test-jit-00-bool-arith-ops COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00bool_arith_ops.ts") +tslang_add_test(NAME test-jit-00-int64-shift-bitwise COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00int64_shift_bitwise.ts") tslang_add_test(NAME test-jit-00-bool-const-array COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00bool_const_array.ts") tslang_add_test(NAME test-jit-00-mixed-type-ops COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00mixed_type_ops.ts") tslang_add_test(NAME test-jit-00-generator-manual-next COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00generator_manual_next.ts") @@ -1789,6 +1791,7 @@ set(TSLANG_CORPUS 00async_gc_threading.ts 00async_result_types.ts 00bool_arith_ops.ts + 00int64_shift_bitwise.ts 00bool_const_array.ts 00break_continue_scope_exit.ts 00break_continue.ts diff --git a/tslang/test/tester/tests/00int64_shift_bitwise.ts b/tslang/test/tester/tests/00int64_shift_bitwise.ts new file mode 100644 index 000000000..d83939679 --- /dev/null +++ b/tslang/test/tester/tests/00int64_shift_bitwise.ts @@ -0,0 +1,70 @@ +// regression test: shifts and bitwise operators on 64-bit integers keep the 64-bit width. +// +// bug found and fixed: `<<`, `>>`, `>>>`, `&`, `|` and `^` cast both operands to i32 (the +// JavaScript Int32 coercion of `number`) whatever their type, so `u64 << 40` shifted by +// 40 mod 32 = 8 and gave 256, and the upper half of a u64 was lost in `&`/`|`/`^`. Also `>>` +// of an unsigned operand shifted the sign bit in. + +function main() { + let a: u64 = 1; + const twoTo40: u64 = 1099511627776; + const twoTo33: u64 = 8589934592; + + const b = a << 40; + print(b); + assert(b == twoTo40); + + let c: u64 = 1; + c = c << 33; + assert(c == twoTo33); + + let g: u64 = 1; + g <<= 40; + assert(g == twoTo40); + + let i: s64 = 1; + const twoTo40s: s64 = 1099511627776; + assert(i << 40 == twoTo40s); + + // right shifts of a 64-bit value + const one: u64 = 1; + let f: u64 = 1099511627776; + const r1: u64 = 128; + assert(f >> 33 == r1); + assert(f >>> 33 == r1); + + // an unsigned `>>` shifts zeros in + let h: u64 = 1; + h = h << 63; + assert(h >> 63 == one); + assert(h >>> 63 == one); + + const three: u64 = 3; + let k: u64 = 3; + assert(k << 62 >> 62 == three); + + // a signed `>>` keeps the sign, `>>>` doesn't + let s: s64 = -8; + const minusFour: s64 = -4; + const fifteen: s64 = 15; + assert(s >> 1 == minusFour); + assert(s >>> 60 == fifteen); + + // bitwise operators keep the upper half + let m: u64 = 0xFFFFFFFFFF; + const upper: u64 = 0xFF00000000; + const zero: u64 = 0; + assert((m & 0xFF00000000) == upper); + assert((m | 1) == m); + assert((m ^ m) == zero); + + // a `number` operand still shifts by its count mod 32, as in JavaScript + let n = 1; + assert(n << 33 == 2); + assert(1 << 33 == 2); + let x = -8; + assert(x >> 1 == -4); + assert(x >>> 28 == 15); + + print("done."); +}