From f6445aac9135919c92b5355fa71b3bf05989b55e Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sun, 22 Mar 2026 23:53:11 +0000 Subject: [PATCH 1/4] strip dead code after break in printer --- src/LuaPrinter.ts | 2 +- test/unit/printer/deadCodeAfterBreak.spec.ts | 70 ++++++++++++++++++++ test/util.ts | 16 +++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 test/unit/printer/deadCodeAfterBreak.spec.ts diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index b0a02dfaf..26b55421b 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -328,7 +328,7 @@ export class LuaPrinter { statementNodes.push(node); - if (lua.isReturnStatement(statement)) break; + if (lua.isReturnStatement(statement) || lua.isBreakStatement(statement)) break; } return statementNodes.length > 0 ? [...intersperse(statementNodes, "\n"), "\n"] : []; diff --git a/test/unit/printer/deadCodeAfterBreak.spec.ts b/test/unit/printer/deadCodeAfterBreak.spec.ts new file mode 100644 index 000000000..1bfe675d8 --- /dev/null +++ b/test/unit/printer/deadCodeAfterBreak.spec.ts @@ -0,0 +1,70 @@ +import * as util from "../../util"; + +// In Lua 5.0, 5.1, and LuaJIT, break must be the last statement in a block. +// Any code after break is a syntax error (e.g. `while true do break; local b = 8 end` +// fails with "'end' expected near 'local'"). Lua 5.2+ relaxed this restriction. +// TSTL should strip dead code after break on all targets to avoid these errors. + +function expectNoDeadCode(...deadCodeStrings: string[]): util.TapCallback { + return builder => { + const lua = builder.getMainLuaCodeChunk(); + for (const deadCode of deadCodeStrings) { + expect(lua).not.toContain(deadCode); + } + }; +} + +util.testEachVersion( + "for dead code after break", + () => util.testFunction` + for (let i = 0; i < 10; i++) { break; const b = 8; } + `, + util.expectAllVersions(expectNoDeadCode("local b = 8")) +); + +util.testEachVersion( + "for..in dead code after break", + () => util.testFunction` + for (let a in {"a": 5, "b": 8}) { break; const b = 8; } + `, + util.expectAllVersions(expectNoDeadCode("local b = 8")) +); + +util.testEachVersion( + "for..of dead code after break", + () => util.testFunction` + for (let a of [1,2,4]) { break; const b = 8; } + `, + util.expectAllVersions(expectNoDeadCode("local b = 8")) +); + +util.testEachVersion( + "while dead code after break", + () => util.testFunction` + while (true) { break; const b = 8; } + `, + util.expectAllVersions(expectNoDeadCode("local b = 8")) +); + +util.testEachVersion( + "switch dead code after break", + () => util.testFunction` + switch ("abc" as string) { + case "def": + break; + let abc = 4; + case "abc": + break; + let def = 6; + } + `, + util.expectAllVersions(expectNoDeadCode("abc = 4", "def = 6")) +); + +util.testEachVersion( + "do-while dead code after break", + () => util.testFunction` + do { break; const b = 8; } while (true); + `, + util.expectAllVersions(expectNoDeadCode("local b = 8")) +); diff --git a/test/util.ts b/test/util.ts index 2871f6e5a..88ae4ddd5 100644 --- a/test/util.ts +++ b/test/util.ts @@ -76,6 +76,22 @@ export function testEachVersion( } } +export function expectAllVersions( + expectation: (builder: T) => void +): Record void> { + return { + [tstl.LuaTarget.Universal]: expectation, + [tstl.LuaTarget.Lua50]: expectation, + [tstl.LuaTarget.Lua51]: expectation, + [tstl.LuaTarget.Lua52]: expectation, + [tstl.LuaTarget.Lua53]: expectation, + [tstl.LuaTarget.Lua54]: expectation, + [tstl.LuaTarget.Lua55]: expectation, + [tstl.LuaTarget.LuaJIT]: expectation, + [tstl.LuaTarget.Luau]: expectation, + }; +} + export function expectEachVersionExceptJit( expectation: (builder: T) => void ): Record void) | boolean> { From 2ff4e87da902ead6a70b11588937dc7775267903 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Mon, 23 Mar 2026 11:20:14 +0000 Subject: [PATCH 2/4] change tests from translation to no execution error --- test/unit/printer/deadCodeAfterBreak.spec.ts | 21 ++++++-------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/test/unit/printer/deadCodeAfterBreak.spec.ts b/test/unit/printer/deadCodeAfterBreak.spec.ts index 1bfe675d8..e780192a4 100644 --- a/test/unit/printer/deadCodeAfterBreak.spec.ts +++ b/test/unit/printer/deadCodeAfterBreak.spec.ts @@ -5,21 +5,12 @@ import * as util from "../../util"; // fails with "'end' expected near 'local'"). Lua 5.2+ relaxed this restriction. // TSTL should strip dead code after break on all targets to avoid these errors. -function expectNoDeadCode(...deadCodeStrings: string[]): util.TapCallback { - return builder => { - const lua = builder.getMainLuaCodeChunk(); - for (const deadCode of deadCodeStrings) { - expect(lua).not.toContain(deadCode); - } - }; -} - util.testEachVersion( "for dead code after break", () => util.testFunction` for (let i = 0; i < 10; i++) { break; const b = 8; } `, - util.expectAllVersions(expectNoDeadCode("local b = 8")) + util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) ); util.testEachVersion( @@ -27,7 +18,7 @@ util.testEachVersion( () => util.testFunction` for (let a in {"a": 5, "b": 8}) { break; const b = 8; } `, - util.expectAllVersions(expectNoDeadCode("local b = 8")) + util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) ); util.testEachVersion( @@ -35,7 +26,7 @@ util.testEachVersion( () => util.testFunction` for (let a of [1,2,4]) { break; const b = 8; } `, - util.expectAllVersions(expectNoDeadCode("local b = 8")) + util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) ); util.testEachVersion( @@ -43,7 +34,7 @@ util.testEachVersion( () => util.testFunction` while (true) { break; const b = 8; } `, - util.expectAllVersions(expectNoDeadCode("local b = 8")) + util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) ); util.testEachVersion( @@ -58,7 +49,7 @@ util.testEachVersion( let def = 6; } `, - util.expectAllVersions(expectNoDeadCode("abc = 4", "def = 6")) + util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) ); util.testEachVersion( @@ -66,5 +57,5 @@ util.testEachVersion( () => util.testFunction` do { break; const b = 8; } while (true); `, - util.expectAllVersions(expectNoDeadCode("local b = 8")) + util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) ); From 0cf95fd41cabd8dfdaa53c318cc02901319e5030 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Wed, 25 Mar 2026 15:30:08 +0000 Subject: [PATCH 3/4] remove unused expectAllVersions --- test/util.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/util.ts b/test/util.ts index 88ae4ddd5..2871f6e5a 100644 --- a/test/util.ts +++ b/test/util.ts @@ -76,22 +76,6 @@ export function testEachVersion( } } -export function expectAllVersions( - expectation: (builder: T) => void -): Record void> { - return { - [tstl.LuaTarget.Universal]: expectation, - [tstl.LuaTarget.Lua50]: expectation, - [tstl.LuaTarget.Lua51]: expectation, - [tstl.LuaTarget.Lua52]: expectation, - [tstl.LuaTarget.Lua53]: expectation, - [tstl.LuaTarget.Lua54]: expectation, - [tstl.LuaTarget.Lua55]: expectation, - [tstl.LuaTarget.LuaJIT]: expectation, - [tstl.LuaTarget.Luau]: expectation, - }; -} - export function expectEachVersionExceptJit( expectation: (builder: T) => void ): Record void) | boolean> { From 8b265a4d12172cda6be52566c12a176f7d5e0768 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Wed, 25 Mar 2026 16:07:10 +0000 Subject: [PATCH 4/4] improve break dead code tests: target affected versions, check output values --- test/unit/printer/deadCodeAfterBreak.spec.ts | 50 ++++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/test/unit/printer/deadCodeAfterBreak.spec.ts b/test/unit/printer/deadCodeAfterBreak.spec.ts index e780192a4..fe1b9073c 100644 --- a/test/unit/printer/deadCodeAfterBreak.spec.ts +++ b/test/unit/printer/deadCodeAfterBreak.spec.ts @@ -1,3 +1,4 @@ +import * as tstl from "../../../src"; import * as util from "../../util"; // In Lua 5.0, 5.1, and LuaJIT, break must be the last statement in a block. @@ -5,57 +6,88 @@ import * as util from "../../util"; // fails with "'end' expected near 'local'"). Lua 5.2+ relaxed this restriction. // TSTL should strip dead code after break on all targets to avoid these errors. +function expectNoDeadCode(builder: util.TestBuilder) { + const lua = builder.getMainLuaCodeChunk(); + expect(lua).not.toContain("local b = 8"); +} + +const affectedVersions: Record void) | boolean> = { + [tstl.LuaTarget.Universal]: false, + [tstl.LuaTarget.Lua50]: builder => builder.tap(expectNoDeadCode).expectToMatchJsResult(), + [tstl.LuaTarget.Lua51]: builder => builder.tap(expectNoDeadCode).expectToMatchJsResult(), + [tstl.LuaTarget.Lua52]: false, + [tstl.LuaTarget.Lua53]: false, + [tstl.LuaTarget.Lua54]: false, + [tstl.LuaTarget.Lua55]: false, + [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectNoDeadCode), + [tstl.LuaTarget.Luau]: false, +}; + util.testEachVersion( "for dead code after break", () => util.testFunction` - for (let i = 0; i < 10; i++) { break; const b = 8; } + let result = 0; + for (let i = 0; i < 10; i++) { result = i; break; const b = 8; } + return result; `, - util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) + affectedVersions ); util.testEachVersion( "for..in dead code after break", () => util.testFunction` - for (let a in {"a": 5, "b": 8}) { break; const b = 8; } + let result = ""; + for (let a in {"a": 5, "b": 8}) { result = a; break; const b = 8; } + return result; `, - util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) + affectedVersions ); util.testEachVersion( "for..of dead code after break", () => util.testFunction` - for (let a of [1,2,4]) { break; const b = 8; } + let result = 0; + for (let a of [1,2,4]) { result = a; break; const b = 8; } + return result; `, - util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) + affectedVersions ); util.testEachVersion( "while dead code after break", () => util.testFunction` + let result = "done"; while (true) { break; const b = 8; } + return result; `, - util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) + affectedVersions ); util.testEachVersion( "switch dead code after break", () => util.testFunction` + let result = "none"; switch ("abc" as string) { case "def": + result = "def"; break; let abc = 4; case "abc": + result = "abc"; break; let def = 6; } + return result; `, - util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) + affectedVersions ); util.testEachVersion( "do-while dead code after break", () => util.testFunction` + let result = "done"; do { break; const b = 8; } while (true); + return result; `, - util.expectEachVersionExceptJit(builder => builder.expectNoExecutionError()) + affectedVersions );