From 506b68b656a12c5ab2bece239a2054cd04ea26ce Mon Sep 17 00:00:00 2001 From: Ryan Young Date: Mon, 16 Oct 2023 20:03:47 +0000 Subject: [PATCH 1/2] feat: support loop continue for 5.0, 5.1, and universal targets --- src/transformation/utils/scope.ts | 7 +- src/transformation/visitors/break-continue.ts | 28 ++- src/transformation/visitors/loops/utils.ts | 32 +++- test/unit/__snapshots__/loops.spec.ts.snap | 177 ------------------ test/unit/loops.spec.ts | 11 +- 5 files changed, 55 insertions(+), 200 deletions(-) diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts index 9e89d3422..87f431162 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -22,6 +22,11 @@ interface FunctionDefinitionInfo { definition?: lua.VariableDeclarationStatement | lua.AssignmentStatement; } +export enum LoopContinued { + WithGoto, + WithRepeatBreak, +} + export interface Scope { type: ScopeType; id: number; @@ -30,7 +35,7 @@ export interface Scope { variableDeclarations?: lua.VariableDeclarationStatement[]; functionDefinitions?: Map; importStatements?: lua.Statement[]; - loopContinued?: boolean; + loopContinued?: LoopContinued; functionReturned?: boolean; } diff --git a/src/transformation/visitors/break-continue.ts b/src/transformation/visitors/break-continue.ts index 580abf73f..cfc411439 100644 --- a/src/transformation/visitors/break-continue.ts +++ b/src/transformation/visitors/break-continue.ts @@ -2,8 +2,7 @@ import * as ts from "typescript"; import { LuaTarget } from "../../CompilerOptions"; import * as lua from "../../LuaAST"; import { FunctionVisitor } from "../context"; -import { unsupportedForTarget } from "../utils/diagnostics"; -import { findScope, ScopeType } from "../utils/scope"; +import { findScope, LoopContinued, ScopeType } from "../utils/scope"; export const transformBreakStatement: FunctionVisitor = (breakStatement, context) => { void context; @@ -11,19 +10,28 @@ export const transformBreakStatement: FunctionVisitor = (brea }; export const transformContinueStatement: FunctionVisitor = (statement, context) => { - if ( + const scope = findScope(context, ScopeType.Loop); + const continuedWith = context.luaTarget === LuaTarget.Universal || context.luaTarget === LuaTarget.Lua50 || context.luaTarget === LuaTarget.Lua51 - ) { - context.diagnostics.push(unsupportedForTarget(statement, "Continue statement", context.luaTarget)); - } - - const scope = findScope(context, ScopeType.Loop); + ? LoopContinued.WithRepeatBreak + : LoopContinued.WithGoto; if (scope) { - scope.loopContinued = true; + scope.loopContinued = continuedWith; } - return lua.createGotoStatement(`__continue${scope?.id ?? ""}`, statement); + const label = `__continue${scope?.id ?? ""}`; + + switch (continuedWith) { + case LoopContinued.WithGoto: + return lua.createGotoStatement(label, statement); + + case LoopContinued.WithRepeatBreak: + return [ + lua.createAssignmentStatement(lua.createIdentifier(label), lua.createBooleanLiteral(true), statement), + lua.createBreakStatement(statement), + ]; + } }; diff --git a/src/transformation/visitors/loops/utils.ts b/src/transformation/visitors/loops/utils.ts index 7f522edb2..f34b35245 100644 --- a/src/transformation/visitors/loops/utils.ts +++ b/src/transformation/visitors/loops/utils.ts @@ -2,7 +2,7 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { TransformationContext } from "../../context"; import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; -import { performHoisting, ScopeType } from "../../utils/scope"; +import { LoopContinued, performHoisting, ScopeType } from "../../utils/scope"; import { isAssignmentPattern } from "../../utils/typescript"; import { transformAssignment } from "../binary-expression/assignments"; import { transformAssignmentPattern } from "../binary-expression/destructuring-assignments"; @@ -19,15 +19,31 @@ export function transformLoopBody( const scope = context.popScope(); const scopeId = scope.id; - if (!scope.loopContinued) { - return body; - } + switch (scope.loopContinued) { + case undefined: + return body; + + case LoopContinued.WithGoto: + return [lua.createDoStatement(body), lua.createLabelStatement(`__continue${scopeId}`)]; - const baseResult: lua.Statement[] = [lua.createDoStatement(body)]; - const continueLabel = lua.createLabelStatement(`__continue${scopeId}`); - baseResult.push(continueLabel); + case LoopContinued.WithRepeatBreak: + const identifier = lua.createIdentifier(`__continue${scopeId}`); + const literalTrue = lua.createBooleanLiteral(true); - return baseResult; + return [ + lua.createDoStatement([ + lua.createVariableDeclarationStatement(identifier), + lua.createRepeatStatement( + lua.createBlock([...body, lua.createAssignmentStatement(identifier, literalTrue)]), + literalTrue + ), + lua.createIfStatement( + lua.createUnaryExpression(identifier, lua.SyntaxKind.NotOperator), + lua.createBlock([lua.createBreakStatement()]) + ), + ]), + ]; + } } export function getVariableDeclarationBinding( diff --git a/test/unit/__snapshots__/loops.spec.ts.snap b/test/unit/__snapshots__/loops.spec.ts.snap index 7b0c40ad2..29b609730 100644 --- a/test/unit/__snapshots__/loops.spec.ts.snap +++ b/test/unit/__snapshots__/loops.spec.ts.snap @@ -11,180 +11,3 @@ return ____exports" `; exports[`forin[Array]: diagnostics 1`] = `"main.ts(3,9): error TSTL: Iterating over arrays with 'for ... in' is not allowed."`; - -exports[`loop continue (do { continue; } while (false)) [5.0]: code 1`] = ` -"repeat - do - do - goto __continue2 - end - ::__continue2:: - end -until not false" -`; - -exports[`loop continue (do { continue; } while (false)) [5.0]: diagnostics 1`] = `"main.ts(1,6): error TSTL: Continue statement is/are not supported for target Lua 5.0."`; - -exports[`loop continue (do { continue; } while (false)) [5.1]: code 1`] = ` -"repeat - do - do - goto __continue2 - end - ::__continue2:: - end -until not false" -`; - -exports[`loop continue (do { continue; } while (false)) [5.1]: diagnostics 1`] = `"main.ts(1,6): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; - -exports[`loop continue (do { continue; } while (false)) [universal]: code 1`] = ` -"repeat - do - do - goto __continue2 - end - ::__continue2:: - end -until not false" -`; - -exports[`loop continue (do { continue; } while (false)) [universal]: diagnostics 1`] = `"main.ts(1,6): error TSTL: Continue statement is/are not supported for target Lua universal."`; - -exports[`loop continue (for (;;) { continue; }) [5.0]: code 1`] = ` -"do - while true do - do - goto __continue2 - end - ::__continue2:: - end -end" -`; - -exports[`loop continue (for (;;) { continue; }) [5.0]: diagnostics 1`] = `"main.ts(1,12): error TSTL: Continue statement is/are not supported for target Lua 5.0."`; - -exports[`loop continue (for (;;) { continue; }) [5.1]: code 1`] = ` -"do - while true do - do - goto __continue2 - end - ::__continue2:: - end -end" -`; - -exports[`loop continue (for (;;) { continue; }) [5.1]: diagnostics 1`] = `"main.ts(1,12): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; - -exports[`loop continue (for (;;) { continue; }) [universal]: code 1`] = ` -"do - while true do - do - goto __continue2 - end - ::__continue2:: - end -end" -`; - -exports[`loop continue (for (;;) { continue; }) [universal]: diagnostics 1`] = `"main.ts(1,12): error TSTL: Continue statement is/are not supported for target Lua universal."`; - -exports[`loop continue (for (const a in {}) { continue; }) [5.0]: code 1`] = ` -"for a in pairs({}) do - do - goto __continue2 - end - ::__continue2:: -end" -`; - -exports[`loop continue (for (const a in {}) { continue; }) [5.0]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua 5.0."`; - -exports[`loop continue (for (const a in {}) { continue; }) [5.1]: code 1`] = ` -"for a in pairs({}) do - do - goto __continue2 - end - ::__continue2:: -end" -`; - -exports[`loop continue (for (const a in {}) { continue; }) [5.1]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; - -exports[`loop continue (for (const a in {}) { continue; }) [universal]: code 1`] = ` -"for a in pairs({}) do - do - goto __continue2 - end - ::__continue2:: -end" -`; - -exports[`loop continue (for (const a in {}) { continue; }) [universal]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua universal."`; - -exports[`loop continue (for (const a of []) { continue; }) [5.0]: code 1`] = ` -"for ____, a in ipairs({}) do - do - goto __continue2 - end - ::__continue2:: -end" -`; - -exports[`loop continue (for (const a of []) { continue; }) [5.0]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua 5.0."`; - -exports[`loop continue (for (const a of []) { continue; }) [5.1]: code 1`] = ` -"for ____, a in ipairs({}) do - do - goto __continue2 - end - ::__continue2:: -end" -`; - -exports[`loop continue (for (const a of []) { continue; }) [5.1]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; - -exports[`loop continue (for (const a of []) { continue; }) [universal]: code 1`] = ` -"for ____, a in ipairs({}) do - do - goto __continue2 - end - ::__continue2:: -end" -`; - -exports[`loop continue (for (const a of []) { continue; }) [universal]: diagnostics 1`] = `"main.ts(1,23): error TSTL: Continue statement is/are not supported for target Lua universal."`; - -exports[`loop continue (while (false) { continue; }) [5.0]: code 1`] = ` -"while false do - do - goto __continue2 - end - ::__continue2:: -end" -`; - -exports[`loop continue (while (false) { continue; }) [5.0]: diagnostics 1`] = `"main.ts(1,17): error TSTL: Continue statement is/are not supported for target Lua 5.0."`; - -exports[`loop continue (while (false) { continue; }) [5.1]: code 1`] = ` -"while false do - do - goto __continue2 - end - ::__continue2:: -end" -`; - -exports[`loop continue (while (false) { continue; }) [5.1]: diagnostics 1`] = `"main.ts(1,17): error TSTL: Continue statement is/are not supported for target Lua 5.1."`; - -exports[`loop continue (while (false) { continue; }) [universal]: code 1`] = ` -"while false do - do - goto __continue2 - end - ::__continue2:: -end" -`; - -exports[`loop continue (while (false) { continue; }) [universal]: diagnostics 1`] = `"main.ts(1,17): error TSTL: Continue statement is/are not supported for target Lua universal."`; diff --git a/test/unit/loops.spec.ts b/test/unit/loops.spec.ts index 0fff55d9d..4afb4dae2 100644 --- a/test/unit/loops.spec.ts +++ b/test/unit/loops.spec.ts @@ -1,5 +1,5 @@ import * as tstl from "../../src"; -import { forbiddenForIn, unsupportedForTarget } from "../../src/transformation/utils/diagnostics"; +import { forbiddenForIn } from "../../src/transformation/utils/diagnostics"; import * as util from "../util"; test("while", () => { @@ -515,13 +515,16 @@ for (const testCase of [ "for (const a in {}) { continue; }", "for (const a of []) { continue; }", ]) { + const expectContinueVariable: util.TapCallback = builder => + expect(builder.getMainLuaCodeChunk()).toMatch("local __continue2"); + const expectContinueGotoLabel: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toMatch("::__continue2::"); util.testEachVersion(`loop continue (${testCase})`, () => util.testModule(testCase), { - [tstl.LuaTarget.Universal]: builder => builder.expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]), - [tstl.LuaTarget.Lua50]: builder => builder.expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]), - [tstl.LuaTarget.Lua51]: builder => builder.expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]), + [tstl.LuaTarget.Universal]: expectContinueVariable, + [tstl.LuaTarget.Lua50]: expectContinueVariable, + [tstl.LuaTarget.Lua51]: expectContinueVariable, [tstl.LuaTarget.Lua52]: expectContinueGotoLabel, [tstl.LuaTarget.Lua53]: expectContinueGotoLabel, [tstl.LuaTarget.Lua54]: expectContinueGotoLabel, From 1745beae4ce0c6b2193a26a2ec6ea1c767012ffb Mon Sep 17 00:00:00 2001 From: Ryan Young Date: Tue, 17 Oct 2023 20:27:02 +0000 Subject: [PATCH 2/2] test: test loops w/ continue against all lua versions --- test/unit/loops.spec.ts | 60 ++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/test/unit/loops.spec.ts b/test/unit/loops.spec.ts index 4afb4dae2..f2225c7f5 100644 --- a/test/unit/loops.spec.ts +++ b/test/unit/loops.spec.ts @@ -1,4 +1,5 @@ import * as tstl from "../../src"; +import { LuaTarget } from "../../src"; import { forbiddenForIn } from "../../src/transformation/utils/diagnostics"; import * as util from "../util"; @@ -14,8 +15,9 @@ test("while", () => { `.expectToMatchJsResult(); }); -test("while with continue", () => { - util.testFunction` +util.testEachVersion( + "while with continue", + () => util.testFunction` let arrTest = [0, 1, 2, 3, 4]; let i = 0; while (i < arrTest.length) { @@ -36,11 +38,13 @@ test("while with continue", () => { i++; } return arrTest; - `.expectToMatchJsResult(); -}); + `, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); -test("dowhile with continue", () => { - util.testFunction` +util.testEachVersion( + "dowhile with continue", + () => util.testFunction` let arrTest = [0, 1, 2, 3, 4]; let i = 0; do { @@ -61,8 +65,9 @@ test("dowhile with continue", () => { i++; } while (i < arrTest.length) return arrTest; - `.expectToMatchJsResult(); -}); + `, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); test("for", () => { util.testFunction` @@ -85,8 +90,9 @@ test("for with expression", () => { `.expectToMatchJsResult(); }); -test("for with continue", () => { - util.testFunction` +util.testEachVersion( + "for with continue", + () => util.testFunction` let arrTest = [0, 1, 2, 3, 4]; for (let i = 0; i < arrTest.length; i++) { if (i % 2 == 0) { @@ -101,8 +107,9 @@ test("for with continue", () => { } } return arrTest; - `.expectToMatchJsResult(); -}); + `, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); test("forMirror", () => { util.testFunction` @@ -216,7 +223,20 @@ test("forin[Array]", () => { `.expectDiagnosticsToMatchSnapshot([forbiddenForIn.code]); }); -test.each([{ inp: { a: 0, b: 1, c: 2, d: 3, e: 4 } }])("forin with continue (%p)", ({ inp }) => { +const luaTargetsExceptJit = [ + LuaTarget.Lua50, + LuaTarget.Lua51, + LuaTarget.Lua52, + LuaTarget.Lua53, + LuaTarget.Lua54, + LuaTarget.Universal, +]; + +test.each( + luaTargetsExceptJit.flatMap(target => + [{ inp: { a: 0, b: 1, c: 2, d: 3, e: 4 } }].map(testCase => [target, testCase] as const) + ) +)("forin with continue (%s %p)", (luaTarget, { inp }) => { util.testFunctionTemplate` let obj = ${inp}; for (let i in obj) { @@ -227,7 +247,9 @@ test.each([{ inp: { a: 0, b: 1, c: 2, d: 3, e: 4 } }])("forin with continue (%p) obj[i] = 0; } return obj; - `.expectToMatchJsResult(); + ` + .setOptions({ luaTarget }) + .expectToMatchJsResult(); }); test.each([{ inp: [0, 1, 2] }])("forof (%p)", ({ inp }) => { @@ -331,8 +353,9 @@ test("forof destructing with existing variables", () => { `.expectToMatchJsResult(); }); -test("forof with continue", () => { - util.testFunction` +util.testEachVersion( + "forof with continue", + () => util.testFunction` let testArr = [0, 1, 2, 3, 4]; let a = 0; for (let i of testArr) { @@ -350,8 +373,9 @@ test("forof with continue", () => { a++; } return testArr; - `.expectToMatchJsResult(); -}); + `, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); test("forof with iterator", () => { util.testFunction`