From 2592ff279ac645a493cf0a3b45c8ad41e496769e Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 13:09:43 +0100 Subject: [PATCH 1/5] fix >> (signed right shift) producing wrong results on Lua 5.3+ --- src/transformation/utils/diagnostics.ts | 4 - .../visitors/binary-expression/bit.ts | 49 ++++++++++--- .../__snapshots__/expressions.spec.ts.snap | 73 ++++++++++--------- test/unit/expressions.spec.ts | 50 ++++++++----- 4 files changed, 109 insertions(+), 67 deletions(-) diff --git a/src/transformation/utils/diagnostics.ts b/src/transformation/utils/diagnostics.ts index 5fb1bf15e..53ba37314 100644 --- a/src/transformation/utils/diagnostics.ts +++ b/src/transformation/utils/diagnostics.ts @@ -81,10 +81,6 @@ export const unsupportedAccessorInObjectLiteral = createErrorDiagnosticFactory( "Accessors in object literal are not supported." ); -export const unsupportedRightShiftOperator = createErrorDiagnosticFactory( - "Right shift operator is not supported for target Lua 5.3. Use `>>>` instead." -); - const getLuaTargetName = (version: LuaTarget) => (version === LuaTarget.LuaJIT ? "LuaJIT" : `Lua ${version}`); export const unsupportedForTarget = createErrorDiagnosticFactory( (functionality: string, version: LuaTarget) => diff --git a/src/transformation/visitors/binary-expression/bit.ts b/src/transformation/visitors/binary-expression/bit.ts index 5ae876630..590535f71 100644 --- a/src/transformation/visitors/binary-expression/bit.ts +++ b/src/transformation/visitors/binary-expression/bit.ts @@ -3,7 +3,7 @@ import { LuaTarget } from "../../../CompilerOptions"; import * as lua from "../../../LuaAST"; import { assertNever } from "../../../utils"; import { TransformationContext } from "../../context"; -import { unsupportedForTarget, unsupportedRightShiftOperator } from "../../utils/diagnostics"; +import { unsupportedForTarget } from "../../utils/diagnostics"; export type BitOperator = ts.ShiftOperator | ts.BitwiseOperator; export const isBitOperator = (operator: ts.BinaryOperator): operator is BitOperator => @@ -33,11 +33,7 @@ function transformBinaryBitLibOperation( ); } -function transformBitOperatorToLuaOperator( - context: TransformationContext, - node: ts.Node, - operator: BitOperator -): lua.BinaryOperator { +function transformBitOperatorToLuaOperator(operator: BitOperator): lua.BinaryOperator { switch (operator) { case ts.SyntaxKind.BarToken: return lua.SyntaxKind.BitwiseOrOperator; @@ -48,8 +44,6 @@ function transformBitOperatorToLuaOperator( case ts.SyntaxKind.LessThanLessThanToken: return lua.SyntaxKind.BitwiseLeftShiftOperator; case ts.SyntaxKind.GreaterThanGreaterThanToken: - context.diagnostics.push(unsupportedRightShiftOperator(node)); - return lua.SyntaxKind.BitwiseRightShiftOperator; case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: return lua.SyntaxKind.BitwiseRightShiftOperator; } @@ -75,8 +69,7 @@ export function transformBinaryBitOperation( case LuaTarget.Lua52: return transformBinaryBitLibOperation(node, left, right, operator, "bit32"); default: - // Lua 5.3+ `>>` is arithmetic (sign-extending), but TS `>>>` is logical (zero-fill). - // Emit `(left & 0xFFFFFFFF) >> right` to convert to unsigned 32-bit first. + // TS `>>>` is logical on int32; Lua 5.3+ `>>` is logical on 64-bit. Mask to 32 bits first. if (operator === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken) { const mask = lua.createBinaryExpression( left, @@ -91,7 +84,41 @@ export function transformBinaryBitOperation( node ); } - const luaOperator = transformBitOperatorToLuaOperator(context, node, operator); + // TS `>>` is arithmetic on int32; Lua 5.3+ has no native equivalent. Sign-extend the + // low 32 bits to a 64-bit signed value, then floor-divide by 2^right. + if (operator === ts.SyntaxKind.GreaterThanGreaterThanToken) { + const masked = lua.createBinaryExpression( + left, + lua.createNumericLiteral(0xffffffff, node), + lua.SyntaxKind.BitwiseAndOperator, + node + ); + const xored = lua.createBinaryExpression( + lua.createParenthesizedExpression(masked, node), + lua.createNumericLiteral(0x80000000, node), + lua.SyntaxKind.BitwiseExclusiveOrOperator, + node + ); + const signed = lua.createBinaryExpression( + lua.createParenthesizedExpression(xored, node), + lua.createNumericLiteral(0x80000000, node), + lua.SyntaxKind.SubtractionOperator, + node + ); + const divisor = lua.createBinaryExpression( + lua.createNumericLiteral(1, node), + right, + lua.SyntaxKind.BitwiseLeftShiftOperator, + node + ); + return lua.createBinaryExpression( + lua.createParenthesizedExpression(signed, node), + lua.createParenthesizedExpression(divisor, node), + lua.SyntaxKind.FloorDivisionOperator, + node + ); + } + const luaOperator = transformBitOperatorToLuaOperator(operator); return lua.createBinaryExpression(left, right, luaOperator, node); } } diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index 720f7014e..6f0eeacea 100644 --- a/test/unit/__snapshots__/expressions.spec.ts.snap +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -372,6 +372,13 @@ ____exports.__result = a << b return ____exports" `; +exports[`Bitop [5.3] ("a>>=b") 1`] = ` +"local ____exports = {} +a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +____exports.__result = a +return ____exports" +`; + exports[`Bitop [5.3] ("a>>>=b") 1`] = ` "local ____exports = {} a = (a & 4294967295) >> b @@ -385,6 +392,12 @@ ____exports.__result = (a & 4294967295) >> b return ____exports" `; +exports[`Bitop [5.3] ("a>>b") 1`] = ` +"local ____exports = {} +____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +return ____exports" +`; + exports[`Bitop [5.3] ("a^=b") 1`] = ` "local ____exports = {} a = a ~ b @@ -443,6 +456,13 @@ ____exports.__result = a << b return ____exports" `; +exports[`Bitop [5.4] ("a>>=b") 1`] = ` +"local ____exports = {} +a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +____exports.__result = a +return ____exports" +`; + exports[`Bitop [5.4] ("a>>>=b") 1`] = ` "local ____exports = {} a = (a & 4294967295) >> b @@ -456,6 +476,12 @@ ____exports.__result = (a & 4294967295) >> b return ____exports" `; +exports[`Bitop [5.4] ("a>>b") 1`] = ` +"local ____exports = {} +____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +return ____exports" +`; + exports[`Bitop [5.4] ("a^=b") 1`] = ` "local ____exports = {} a = a ~ b @@ -514,6 +540,13 @@ ____exports.__result = a << b return ____exports" `; +exports[`Bitop [5.5] ("a>>=b") 1`] = ` +"local ____exports = {} +a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +____exports.__result = a +return ____exports" +`; + exports[`Bitop [5.5] ("a>>>=b") 1`] = ` "local ____exports = {} a = (a & 4294967295) >> b @@ -527,6 +560,12 @@ ____exports.__result = (a & 4294967295) >> b return ____exports" `; +exports[`Bitop [5.5] ("a>>b") 1`] = ` +"local ____exports = {} +____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +return ____exports" +`; + exports[`Bitop [5.5] ("a^=b") 1`] = ` "local ____exports = {} a = a ~ b @@ -748,37 +787,3 @@ exports[`Undefined Expression 1`] = ` ____exports.__result = nil return ____exports" `; - -exports[`Unsupported bitop 5.3 ("a>>=b"): code 1`] = ` -"local ____exports = {} -a = a >> b -____exports.__result = a -return ____exports" -`; - -exports[`Unsupported bitop 5.3 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; - -exports[`Unsupported bitop 5.3 ("a>>b"): code 1`] = ` -"local ____exports = {} -____exports.__result = a >> b -return ____exports" -`; - -exports[`Unsupported bitop 5.3 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; - -exports[`Unsupported bitop 5.4 ("a>>=b"): code 1`] = ` -"local ____exports = {} -a = a >> b -____exports.__result = a -return ____exports" -`; - -exports[`Unsupported bitop 5.4 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; - -exports[`Unsupported bitop 5.4 ("a>>b"): code 1`] = ` -"local ____exports = {} -____exports.__result = a >> b -return ____exports" -`; - -exports[`Unsupported bitop 5.4 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 12fd85be1..37264891e 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -1,5 +1,5 @@ import * as tstl from "../../src"; -import { unsupportedForTarget, unsupportedRightShiftOperator } from "../../src/transformation/utils/diagnostics"; +import { unsupportedForTarget } from "../../src/transformation/utils/diagnostics"; import * as util from "../util"; test.each([ @@ -49,9 +49,22 @@ test.each(["a+=b", "a-=b", "a*=b", "a/=b", "a%=b", "a**=b"])("Binary expressions `.expectToMatchJsResult(); }); -const supportedInAll = ["~a", "a&b", "a&=b", "a|b", "a|=b", "a^b", "a^=b", "a<>>b", "a>>>=b"]; -const unsupportedIn53And54 = ["a>>b", "a>>=b"]; -const allBinaryOperators = [...supportedInAll, ...unsupportedIn53And54]; +const supportedInAll = [ + "~a", + "a&b", + "a&=b", + "a|b", + "a|=b", + "a^b", + "a^=b", + "a<>b", + "a>>=b", + "a>>>b", + "a>>>=b", +]; +const allBinaryOperators = supportedInAll; test.each(allBinaryOperators)("Bitop [5.0] (%p)", input => { // Bit operations not supported in 5.0, expect an exception util.testExpression(input) @@ -103,20 +116,6 @@ test.each(supportedInAll)("Bitop [5.5] (%p)", input => { .expectLuaToMatchSnapshot(); }); -test.each(unsupportedIn53And54)("Unsupported bitop 5.3 (%p)", input => { - util.testExpression(input) - .setOptions({ luaTarget: tstl.LuaTarget.Lua53, luaLibImport: tstl.LuaLibImportKind.None }) - .disableSemanticCheck() - .expectDiagnosticsToMatchSnapshot([unsupportedRightShiftOperator.code]); -}); - -test.each(unsupportedIn53And54)("Unsupported bitop 5.4 (%p)", input => { - util.testExpression(input) - .setOptions({ luaTarget: tstl.LuaTarget.Lua54, luaLibImport: tstl.LuaLibImportKind.None }) - .disableSemanticCheck() - .expectDiagnosticsToMatchSnapshot([unsupportedRightShiftOperator.code]); -}); - // Execution tests: verify >>> produces correct results matching JS semantics for (const expression of ["-5 >>> 0", "-1 >>> 0", "1 >>> 0", "-1 >>> 16", "255 >>> 4"]) { util.testEachVersion(`Unsigned right shift execution (${expression})`, () => util.testExpression(expression), { @@ -132,6 +131,21 @@ for (const expression of ["-5 >>> 0", "-1 >>> 0", "1 >>> 0", "-1 >>> 16", "255 > }); } +// Execution tests: verify >> produces correct results matching JS semantics +for (const expression of ["-8 >> 1", "-1 >> 0", "-1 >> 16", "0x7FFFFFFF >> 0", "255 >> 4", "5 >> 1"]) { + util.testEachVersion(`Signed right shift execution (${expression})`, () => util.testExpression(expression), { + [tstl.LuaTarget.Universal]: false, + [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua51]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua52]: false, // bit32.arshift returns uint32, not int32 + [tstl.LuaTarget.Lua53]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua54]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua55]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.LuaJIT]: false, // Can't execute LuaJIT in tests + [tstl.LuaTarget.Luau]: false, + }); +} + for (const code of ["let a = -5; a >>>= 0; return a;", "let a = -1; a >>>= 16; return a;"]) { util.testEachVersion(`Unsigned right shift assignment execution (${code})`, () => util.testFunction(code), { [tstl.LuaTarget.Universal]: false, From 7d2cf1907c292acc63f9894684e5f82c1488e729 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 13:17:19 +0100 Subject: [PATCH 2/5] remove dead >> cases and add >>= execution tests --- .../visitors/binary-expression/bit.ts | 10 ++++++---- test/unit/expressions.spec.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/transformation/visitors/binary-expression/bit.ts b/src/transformation/visitors/binary-expression/bit.ts index 590535f71..bbb82ee54 100644 --- a/src/transformation/visitors/binary-expression/bit.ts +++ b/src/transformation/visitors/binary-expression/bit.ts @@ -33,7 +33,12 @@ function transformBinaryBitLibOperation( ); } -function transformBitOperatorToLuaOperator(operator: BitOperator): lua.BinaryOperator { +type NonShiftRightBitOperator = Exclude< + BitOperator, + ts.SyntaxKind.GreaterThanGreaterThanToken | ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken +>; + +function transformBitOperatorToLuaOperator(operator: NonShiftRightBitOperator): lua.BinaryOperator { switch (operator) { case ts.SyntaxKind.BarToken: return lua.SyntaxKind.BitwiseOrOperator; @@ -43,9 +48,6 @@ function transformBitOperatorToLuaOperator(operator: BitOperator): lua.BinaryOpe return lua.SyntaxKind.BitwiseAndOperator; case ts.SyntaxKind.LessThanLessThanToken: return lua.SyntaxKind.BitwiseLeftShiftOperator; - case ts.SyntaxKind.GreaterThanGreaterThanToken: - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - return lua.SyntaxKind.BitwiseRightShiftOperator; } } diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 37264891e..79107d9cf 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -160,6 +160,20 @@ for (const code of ["let a = -5; a >>>= 0; return a;", "let a = -1; a >>>= 16; r }); } +for (const code of ["let a = -8; a >>= 1; return a;", "let a = -1; a >>= 16; return a;"]) { + util.testEachVersion(`Signed right shift assignment execution (${code})`, () => util.testFunction(code), { + [tstl.LuaTarget.Universal]: false, + [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua51]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua52]: false, // bit32.arshift returns uint32, not int32 + [tstl.LuaTarget.Lua53]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua54]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua55]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.LuaJIT]: false, // Can't execute LuaJIT in tests + [tstl.LuaTarget.Luau]: false, + }); +} + test.each(["1+1", "-1+1", "1*30+4", "1*(3+4)", "1*(3+4*2)", "10-(4+5)"])( "Binary expressions ordering parentheses (%p)", input => { From 54a9d60169e3655b3dcff9c34b7d32056acc277e Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 14:35:44 +0200 Subject: [PATCH 3/5] mask shift amount to 5 bits for >> on lua 5.3+ --- .../visitors/binary-expression/bit.ts | 11 +++++++++-- .../unit/__snapshots__/expressions.spec.ts.snap | 12 ++++++------ test/unit/expressions.spec.ts | 17 ++++++++++++++++- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/transformation/visitors/binary-expression/bit.ts b/src/transformation/visitors/binary-expression/bit.ts index bbb82ee54..73b4baab7 100644 --- a/src/transformation/visitors/binary-expression/bit.ts +++ b/src/transformation/visitors/binary-expression/bit.ts @@ -87,7 +87,8 @@ export function transformBinaryBitOperation( ); } // TS `>>` is arithmetic on int32; Lua 5.3+ has no native equivalent. Sign-extend the - // low 32 bits to a 64-bit signed value, then floor-divide by 2^right. + // low 32 bits to a 64-bit signed value, then floor-divide by 2^(right & 31). Masking + // the shift amount matches JS, which only uses the low 5 bits of the right operand. if (operator === ts.SyntaxKind.GreaterThanGreaterThanToken) { const masked = lua.createBinaryExpression( left, @@ -107,9 +108,15 @@ export function transformBinaryBitOperation( lua.SyntaxKind.SubtractionOperator, node ); + const shiftAmount = lua.createBinaryExpression( + right, + lua.createNumericLiteral(31, node), + lua.SyntaxKind.BitwiseAndOperator, + node + ); const divisor = lua.createBinaryExpression( lua.createNumericLiteral(1, node), - right, + lua.createParenthesizedExpression(shiftAmount, node), lua.SyntaxKind.BitwiseLeftShiftOperator, node ); diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index 6f0eeacea..2cdeb48a1 100644 --- a/test/unit/__snapshots__/expressions.spec.ts.snap +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -374,7 +374,7 @@ return ____exports" exports[`Bitop [5.3] ("a>>=b") 1`] = ` "local ____exports = {} -a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) ____exports.__result = a return ____exports" `; @@ -394,7 +394,7 @@ return ____exports" exports[`Bitop [5.3] ("a>>b") 1`] = ` "local ____exports = {} -____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) return ____exports" `; @@ -458,7 +458,7 @@ return ____exports" exports[`Bitop [5.4] ("a>>=b") 1`] = ` "local ____exports = {} -a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) ____exports.__result = a return ____exports" `; @@ -478,7 +478,7 @@ return ____exports" exports[`Bitop [5.4] ("a>>b") 1`] = ` "local ____exports = {} -____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) return ____exports" `; @@ -542,7 +542,7 @@ return ____exports" exports[`Bitop [5.5] ("a>>=b") 1`] = ` "local ____exports = {} -a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) ____exports.__result = a return ____exports" `; @@ -562,7 +562,7 @@ return ____exports" exports[`Bitop [5.5] ("a>>b") 1`] = ` "local ____exports = {} -____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << b) +____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) return ____exports" `; diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 79107d9cf..229fcea18 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -132,7 +132,22 @@ for (const expression of ["-5 >>> 0", "-1 >>> 0", "1 >>> 0", "-1 >>> 16", "255 > } // Execution tests: verify >> produces correct results matching JS semantics -for (const expression of ["-8 >> 1", "-1 >> 0", "-1 >> 16", "0x7FFFFFFF >> 0", "255 >> 4", "5 >> 1"]) { +for (const expression of [ + "-8 >> 1", + "-1 >> 0", + "-1 >> 16", + "0x7FFFFFFF >> 0", + "255 >> 4", + "5 >> 1", + "-1 >> 31", + "0x7FFFFFFF >> 31", + "0x80000000 >> 31", + "1 >> 31", + "-8 >> 32", + "1 >> 32", + "-8 >> 33", + "-1 >> 63", +]) { util.testEachVersion(`Signed right shift execution (${expression})`, () => util.testExpression(expression), { [tstl.LuaTarget.Universal]: false, [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime From 1e56676439700ec90d2b0df4679b2c574693ab50 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 21:07:48 +0000 Subject: [PATCH 4/5] Revert "fix >> (signed right shift) producing wrong results on Lua 5.3+" This reverts commit 2592ff279ac645a493cf0a3b45c8ad41e496769e. --- src/transformation/utils/diagnostics.ts | 4 + .../visitors/binary-expression/bit.ts | 64 ++++----------- .../__snapshots__/expressions.spec.ts.snap | 73 ++++++++--------- test/unit/expressions.spec.ts | 79 +++++-------------- 4 files changed, 70 insertions(+), 150 deletions(-) diff --git a/src/transformation/utils/diagnostics.ts b/src/transformation/utils/diagnostics.ts index 53ba37314..5fb1bf15e 100644 --- a/src/transformation/utils/diagnostics.ts +++ b/src/transformation/utils/diagnostics.ts @@ -81,6 +81,10 @@ export const unsupportedAccessorInObjectLiteral = createErrorDiagnosticFactory( "Accessors in object literal are not supported." ); +export const unsupportedRightShiftOperator = createErrorDiagnosticFactory( + "Right shift operator is not supported for target Lua 5.3. Use `>>>` instead." +); + const getLuaTargetName = (version: LuaTarget) => (version === LuaTarget.LuaJIT ? "LuaJIT" : `Lua ${version}`); export const unsupportedForTarget = createErrorDiagnosticFactory( (functionality: string, version: LuaTarget) => diff --git a/src/transformation/visitors/binary-expression/bit.ts b/src/transformation/visitors/binary-expression/bit.ts index 73b4baab7..5ae876630 100644 --- a/src/transformation/visitors/binary-expression/bit.ts +++ b/src/transformation/visitors/binary-expression/bit.ts @@ -3,7 +3,7 @@ import { LuaTarget } from "../../../CompilerOptions"; import * as lua from "../../../LuaAST"; import { assertNever } from "../../../utils"; import { TransformationContext } from "../../context"; -import { unsupportedForTarget } from "../../utils/diagnostics"; +import { unsupportedForTarget, unsupportedRightShiftOperator } from "../../utils/diagnostics"; export type BitOperator = ts.ShiftOperator | ts.BitwiseOperator; export const isBitOperator = (operator: ts.BinaryOperator): operator is BitOperator => @@ -33,12 +33,11 @@ function transformBinaryBitLibOperation( ); } -type NonShiftRightBitOperator = Exclude< - BitOperator, - ts.SyntaxKind.GreaterThanGreaterThanToken | ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken ->; - -function transformBitOperatorToLuaOperator(operator: NonShiftRightBitOperator): lua.BinaryOperator { +function transformBitOperatorToLuaOperator( + context: TransformationContext, + node: ts.Node, + operator: BitOperator +): lua.BinaryOperator { switch (operator) { case ts.SyntaxKind.BarToken: return lua.SyntaxKind.BitwiseOrOperator; @@ -48,6 +47,11 @@ function transformBitOperatorToLuaOperator(operator: NonShiftRightBitOperator): return lua.SyntaxKind.BitwiseAndOperator; case ts.SyntaxKind.LessThanLessThanToken: return lua.SyntaxKind.BitwiseLeftShiftOperator; + case ts.SyntaxKind.GreaterThanGreaterThanToken: + context.diagnostics.push(unsupportedRightShiftOperator(node)); + return lua.SyntaxKind.BitwiseRightShiftOperator; + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + return lua.SyntaxKind.BitwiseRightShiftOperator; } } @@ -71,7 +75,8 @@ export function transformBinaryBitOperation( case LuaTarget.Lua52: return transformBinaryBitLibOperation(node, left, right, operator, "bit32"); default: - // TS `>>>` is logical on int32; Lua 5.3+ `>>` is logical on 64-bit. Mask to 32 bits first. + // Lua 5.3+ `>>` is arithmetic (sign-extending), but TS `>>>` is logical (zero-fill). + // Emit `(left & 0xFFFFFFFF) >> right` to convert to unsigned 32-bit first. if (operator === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken) { const mask = lua.createBinaryExpression( left, @@ -86,48 +91,7 @@ export function transformBinaryBitOperation( node ); } - // TS `>>` is arithmetic on int32; Lua 5.3+ has no native equivalent. Sign-extend the - // low 32 bits to a 64-bit signed value, then floor-divide by 2^(right & 31). Masking - // the shift amount matches JS, which only uses the low 5 bits of the right operand. - if (operator === ts.SyntaxKind.GreaterThanGreaterThanToken) { - const masked = lua.createBinaryExpression( - left, - lua.createNumericLiteral(0xffffffff, node), - lua.SyntaxKind.BitwiseAndOperator, - node - ); - const xored = lua.createBinaryExpression( - lua.createParenthesizedExpression(masked, node), - lua.createNumericLiteral(0x80000000, node), - lua.SyntaxKind.BitwiseExclusiveOrOperator, - node - ); - const signed = lua.createBinaryExpression( - lua.createParenthesizedExpression(xored, node), - lua.createNumericLiteral(0x80000000, node), - lua.SyntaxKind.SubtractionOperator, - node - ); - const shiftAmount = lua.createBinaryExpression( - right, - lua.createNumericLiteral(31, node), - lua.SyntaxKind.BitwiseAndOperator, - node - ); - const divisor = lua.createBinaryExpression( - lua.createNumericLiteral(1, node), - lua.createParenthesizedExpression(shiftAmount, node), - lua.SyntaxKind.BitwiseLeftShiftOperator, - node - ); - return lua.createBinaryExpression( - lua.createParenthesizedExpression(signed, node), - lua.createParenthesizedExpression(divisor, node), - lua.SyntaxKind.FloorDivisionOperator, - node - ); - } - const luaOperator = transformBitOperatorToLuaOperator(operator); + const luaOperator = transformBitOperatorToLuaOperator(context, node, operator); return lua.createBinaryExpression(left, right, luaOperator, node); } } diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index 2cdeb48a1..720f7014e 100644 --- a/test/unit/__snapshots__/expressions.spec.ts.snap +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -372,13 +372,6 @@ ____exports.__result = a << b return ____exports" `; -exports[`Bitop [5.3] ("a>>=b") 1`] = ` -"local ____exports = {} -a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) -____exports.__result = a -return ____exports" -`; - exports[`Bitop [5.3] ("a>>>=b") 1`] = ` "local ____exports = {} a = (a & 4294967295) >> b @@ -392,12 +385,6 @@ ____exports.__result = (a & 4294967295) >> b return ____exports" `; -exports[`Bitop [5.3] ("a>>b") 1`] = ` -"local ____exports = {} -____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) -return ____exports" -`; - exports[`Bitop [5.3] ("a^=b") 1`] = ` "local ____exports = {} a = a ~ b @@ -456,13 +443,6 @@ ____exports.__result = a << b return ____exports" `; -exports[`Bitop [5.4] ("a>>=b") 1`] = ` -"local ____exports = {} -a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) -____exports.__result = a -return ____exports" -`; - exports[`Bitop [5.4] ("a>>>=b") 1`] = ` "local ____exports = {} a = (a & 4294967295) >> b @@ -476,12 +456,6 @@ ____exports.__result = (a & 4294967295) >> b return ____exports" `; -exports[`Bitop [5.4] ("a>>b") 1`] = ` -"local ____exports = {} -____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) -return ____exports" -`; - exports[`Bitop [5.4] ("a^=b") 1`] = ` "local ____exports = {} a = a ~ b @@ -540,13 +514,6 @@ ____exports.__result = a << b return ____exports" `; -exports[`Bitop [5.5] ("a>>=b") 1`] = ` -"local ____exports = {} -a = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) -____exports.__result = a -return ____exports" -`; - exports[`Bitop [5.5] ("a>>>=b") 1`] = ` "local ____exports = {} a = (a & 4294967295) >> b @@ -560,12 +527,6 @@ ____exports.__result = (a & 4294967295) >> b return ____exports" `; -exports[`Bitop [5.5] ("a>>b") 1`] = ` -"local ____exports = {} -____exports.__result = (((a & 4294967295) ~ 2147483648) - 2147483648) // (1 << (b & 31)) -return ____exports" -`; - exports[`Bitop [5.5] ("a^=b") 1`] = ` "local ____exports = {} a = a ~ b @@ -787,3 +748,37 @@ exports[`Undefined Expression 1`] = ` ____exports.__result = nil return ____exports" `; + +exports[`Unsupported bitop 5.3 ("a>>=b"): code 1`] = ` +"local ____exports = {} +a = a >> b +____exports.__result = a +return ____exports" +`; + +exports[`Unsupported bitop 5.3 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; + +exports[`Unsupported bitop 5.3 ("a>>b"): code 1`] = ` +"local ____exports = {} +____exports.__result = a >> b +return ____exports" +`; + +exports[`Unsupported bitop 5.3 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; + +exports[`Unsupported bitop 5.4 ("a>>=b"): code 1`] = ` +"local ____exports = {} +a = a >> b +____exports.__result = a +return ____exports" +`; + +exports[`Unsupported bitop 5.4 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; + +exports[`Unsupported bitop 5.4 ("a>>b"): code 1`] = ` +"local ____exports = {} +____exports.__result = a >> b +return ____exports" +`; + +exports[`Unsupported bitop 5.4 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 229fcea18..12fd85be1 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -1,5 +1,5 @@ import * as tstl from "../../src"; -import { unsupportedForTarget } from "../../src/transformation/utils/diagnostics"; +import { unsupportedForTarget, unsupportedRightShiftOperator } from "../../src/transformation/utils/diagnostics"; import * as util from "../util"; test.each([ @@ -49,22 +49,9 @@ test.each(["a+=b", "a-=b", "a*=b", "a/=b", "a%=b", "a**=b"])("Binary expressions `.expectToMatchJsResult(); }); -const supportedInAll = [ - "~a", - "a&b", - "a&=b", - "a|b", - "a|=b", - "a^b", - "a^=b", - "a<>b", - "a>>=b", - "a>>>b", - "a>>>=b", -]; -const allBinaryOperators = supportedInAll; +const supportedInAll = ["~a", "a&b", "a&=b", "a|b", "a|=b", "a^b", "a^=b", "a<>>b", "a>>>=b"]; +const unsupportedIn53And54 = ["a>>b", "a>>=b"]; +const allBinaryOperators = [...supportedInAll, ...unsupportedIn53And54]; test.each(allBinaryOperators)("Bitop [5.0] (%p)", input => { // Bit operations not supported in 5.0, expect an exception util.testExpression(input) @@ -116,6 +103,20 @@ test.each(supportedInAll)("Bitop [5.5] (%p)", input => { .expectLuaToMatchSnapshot(); }); +test.each(unsupportedIn53And54)("Unsupported bitop 5.3 (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua53, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([unsupportedRightShiftOperator.code]); +}); + +test.each(unsupportedIn53And54)("Unsupported bitop 5.4 (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua54, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([unsupportedRightShiftOperator.code]); +}); + // Execution tests: verify >>> produces correct results matching JS semantics for (const expression of ["-5 >>> 0", "-1 >>> 0", "1 >>> 0", "-1 >>> 16", "255 >>> 4"]) { util.testEachVersion(`Unsigned right shift execution (${expression})`, () => util.testExpression(expression), { @@ -131,36 +132,6 @@ for (const expression of ["-5 >>> 0", "-1 >>> 0", "1 >>> 0", "-1 >>> 16", "255 > }); } -// Execution tests: verify >> produces correct results matching JS semantics -for (const expression of [ - "-8 >> 1", - "-1 >> 0", - "-1 >> 16", - "0x7FFFFFFF >> 0", - "255 >> 4", - "5 >> 1", - "-1 >> 31", - "0x7FFFFFFF >> 31", - "0x80000000 >> 31", - "1 >> 31", - "-8 >> 32", - "1 >> 32", - "-8 >> 33", - "-1 >> 63", -]) { - util.testEachVersion(`Signed right shift execution (${expression})`, () => util.testExpression(expression), { - [tstl.LuaTarget.Universal]: false, - [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime - [tstl.LuaTarget.Lua51]: false, // No bit library in WASM runtime - [tstl.LuaTarget.Lua52]: false, // bit32.arshift returns uint32, not int32 - [tstl.LuaTarget.Lua53]: builder => builder.expectToMatchJsResult(), - [tstl.LuaTarget.Lua54]: builder => builder.expectToMatchJsResult(), - [tstl.LuaTarget.Lua55]: builder => builder.expectToMatchJsResult(), - [tstl.LuaTarget.LuaJIT]: false, // Can't execute LuaJIT in tests - [tstl.LuaTarget.Luau]: false, - }); -} - for (const code of ["let a = -5; a >>>= 0; return a;", "let a = -1; a >>>= 16; return a;"]) { util.testEachVersion(`Unsigned right shift assignment execution (${code})`, () => util.testFunction(code), { [tstl.LuaTarget.Universal]: false, @@ -175,20 +146,6 @@ for (const code of ["let a = -5; a >>>= 0; return a;", "let a = -1; a >>>= 16; r }); } -for (const code of ["let a = -8; a >>= 1; return a;", "let a = -1; a >>= 16; return a;"]) { - util.testEachVersion(`Signed right shift assignment execution (${code})`, () => util.testFunction(code), { - [tstl.LuaTarget.Universal]: false, - [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime - [tstl.LuaTarget.Lua51]: false, // No bit library in WASM runtime - [tstl.LuaTarget.Lua52]: false, // bit32.arshift returns uint32, not int32 - [tstl.LuaTarget.Lua53]: builder => builder.expectToMatchJsResult(), - [tstl.LuaTarget.Lua54]: builder => builder.expectToMatchJsResult(), - [tstl.LuaTarget.Lua55]: builder => builder.expectToMatchJsResult(), - [tstl.LuaTarget.LuaJIT]: false, // Can't execute LuaJIT in tests - [tstl.LuaTarget.Luau]: false, - }); -} - test.each(["1+1", "-1+1", "1*30+4", "1*(3+4)", "1*(3+4*2)", "10-(4+5)"])( "Binary expressions ordering parentheses (%p)", input => { From d3c06d86779c32423c32387b9b32c3c5f4394ccc Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 21:08:21 +0000 Subject: [PATCH 5/5] reword unsupported >> diagnostic --- src/transformation/utils/diagnostics.ts | 2 +- test/unit/__snapshots__/expressions.spec.ts.snap | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/transformation/utils/diagnostics.ts b/src/transformation/utils/diagnostics.ts index 5fb1bf15e..1fb9547e6 100644 --- a/src/transformation/utils/diagnostics.ts +++ b/src/transformation/utils/diagnostics.ts @@ -82,7 +82,7 @@ export const unsupportedAccessorInObjectLiteral = createErrorDiagnosticFactory( ); export const unsupportedRightShiftOperator = createErrorDiagnosticFactory( - "Right shift operator is not supported for target Lua 5.3. Use `>>>` instead." + "Signed right shift `>>` is not supported on Lua 5.3+: Lua's native `>>` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use `>>>` if you don't need sign extension, or write your own helper." ); const getLuaTargetName = (version: LuaTarget) => (version === LuaTarget.LuaJIT ? "LuaJIT" : `Lua ${version}`); diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index 720f7014e..7eeb5961a 100644 --- a/test/unit/__snapshots__/expressions.spec.ts.snap +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -756,7 +756,7 @@ ____exports.__result = a return ____exports" `; -exports[`Unsupported bitop 5.3 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; +exports[`Unsupported bitop 5.3 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Signed right shift \`>>\` is not supported on Lua 5.3+: Lua's native \`>>\` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use \`>>>\` if you don't need sign extension, or write your own helper."`; exports[`Unsupported bitop 5.3 ("a>>b"): code 1`] = ` "local ____exports = {} @@ -764,7 +764,7 @@ ____exports.__result = a >> b return ____exports" `; -exports[`Unsupported bitop 5.3 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; +exports[`Unsupported bitop 5.3 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Signed right shift \`>>\` is not supported on Lua 5.3+: Lua's native \`>>\` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use \`>>>\` if you don't need sign extension, or write your own helper."`; exports[`Unsupported bitop 5.4 ("a>>=b"): code 1`] = ` "local ____exports = {} @@ -773,7 +773,7 @@ ____exports.__result = a return ____exports" `; -exports[`Unsupported bitop 5.4 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; +exports[`Unsupported bitop 5.4 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Signed right shift \`>>\` is not supported on Lua 5.3+: Lua's native \`>>\` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use \`>>>\` if you don't need sign extension, or write your own helper."`; exports[`Unsupported bitop 5.4 ("a>>b"): code 1`] = ` "local ____exports = {} @@ -781,4 +781,4 @@ ____exports.__result = a >> b return ____exports" `; -exports[`Unsupported bitop 5.4 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; +exports[`Unsupported bitop 5.4 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Signed right shift \`>>\` is not supported on Lua 5.3+: Lua's native \`>>\` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use \`>>>\` if you don't need sign extension, or write your own helper."`;