From 6de84d3a2068fbf6526d1aaa561e5460d8eb898b Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Wed, 1 Apr 2026 20:06:32 +0100 Subject: [PATCH 1/2] fix >>> (unsigned right shift) producing wrong results on Lua 5.3+ TS >>> is a logical right shift (zero-fills from the left), but TSTL was mapping it to Lua's >> which is arithmetic (sign-extends). This gave wrong results for negative numbers: e.g. -1 >>> 0 should be 4294967295 but produced -1. Fix by masking to unsigned 32-bit first: (left & 0xFFFFFFFF) >> right. The Lua 5.2 (bit32.rshift) and LuaJIT (bit.rshift) paths were already correct. --- .../visitors/binary-expression/bit.ts | 16 ++++++++ .../__snapshots__/expressions.spec.ts.snap | 12 +++--- test/unit/expressions.spec.ts | 40 +++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/transformation/visitors/binary-expression/bit.ts b/src/transformation/visitors/binary-expression/bit.ts index 79e9c1f3c..5ae876630 100644 --- a/src/transformation/visitors/binary-expression/bit.ts +++ b/src/transformation/visitors/binary-expression/bit.ts @@ -75,6 +75,22 @@ 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. + if (operator === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken) { + const mask = lua.createBinaryExpression( + left, + lua.createNumericLiteral(0xffffffff, node), + lua.SyntaxKind.BitwiseAndOperator, + node + ); + return lua.createBinaryExpression( + lua.createParenthesizedExpression(mask, node), + right, + lua.SyntaxKind.BitwiseRightShiftOperator, + node + ); + } 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 e2adff370..720f7014e 100644 --- a/test/unit/__snapshots__/expressions.spec.ts.snap +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -374,14 +374,14 @@ return ____exports" exports[`Bitop [5.3] ("a>>>=b") 1`] = ` "local ____exports = {} -a = a >> b +a = (a & 4294967295) >> b ____exports.__result = a return ____exports" `; exports[`Bitop [5.3] ("a>>>b") 1`] = ` "local ____exports = {} -____exports.__result = a >> b +____exports.__result = (a & 4294967295) >> b return ____exports" `; @@ -445,14 +445,14 @@ return ____exports" exports[`Bitop [5.4] ("a>>>=b") 1`] = ` "local ____exports = {} -a = a >> b +a = (a & 4294967295) >> b ____exports.__result = a return ____exports" `; exports[`Bitop [5.4] ("a>>>b") 1`] = ` "local ____exports = {} -____exports.__result = a >> b +____exports.__result = (a & 4294967295) >> b return ____exports" `; @@ -516,14 +516,14 @@ return ____exports" exports[`Bitop [5.5] ("a>>>=b") 1`] = ` "local ____exports = {} -a = a >> b +a = (a & 4294967295) >> b ____exports.__result = a return ____exports" `; exports[`Bitop [5.5] ("a>>>b") 1`] = ` "local ____exports = {} -____exports.__result = a >> b +____exports.__result = (a & 4294967295) >> b return ____exports" `; diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index ee0eab5f4..51d630e8c 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -117,6 +117,46 @@ test.each(unsupportedIn53And54)("Unsupported bitop 5.4 (%p)", input => { .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), + { + [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]: builder => builder.expectToMatchJsResult(), + [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, + [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua51]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua52]: builder => builder.expectToMatchJsResult(), + [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 83ba540f6e85e28694cc84ee99a2cceb4796d144 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Wed, 1 Apr 2026 20:13:56 +0100 Subject: [PATCH 2/2] lint --- test/unit/expressions.spec.ts | 57 ++++++++++++++--------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 51d630e8c..12fd85be1 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -119,42 +119,31 @@ test.each(unsupportedIn53And54)("Unsupported bitop 5.4 (%p)", input => { // 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), - { - [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]: builder => builder.expectToMatchJsResult(), - [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, - } - ); + util.testEachVersion(`Unsigned 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]: builder => builder.expectToMatchJsResult(), + [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, - [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime - [tstl.LuaTarget.Lua51]: false, // No bit library in WASM runtime - [tstl.LuaTarget.Lua52]: builder => builder.expectToMatchJsResult(), - [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, + [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua51]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua52]: builder => builder.expectToMatchJsResult(), + [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)"])(