Skip to content

Commit 979e9eb

Browse files
committed
(transformation):
- implement break and continue support within try-catch-finally blocks - generalize and rename async-specific try scope properties and helpers (test): add tests for break and continue inside try-catch-finally blocks
1 parent 7579194 commit 979e9eb

4 files changed

Lines changed: 223 additions & 19 deletions

File tree

src/transformation/utils/scope.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ export interface Scope {
3939
loopContinued?: LoopContinued;
4040
functionReturned?: boolean;
4141
asyncTryHasReturn?: boolean;
42-
asyncTryHasBreak?: boolean;
43-
asyncTryHasContinue?: LoopContinued;
42+
tryHasBreak?: boolean;
43+
tryHasContinue?: LoopContinued;
4444
}
4545

4646
export interface HoistingResult {
@@ -96,7 +96,7 @@ export function findAsyncTryScopeInStack(context: TransformationContext): Scope
9696
}
9797

9898
/** Like findAsyncTryScopeInStack, but also stops at Loop boundaries. */
99-
export function findAsyncTryScopeBeforeLoop(context: TransformationContext): Scope | undefined {
99+
export function findTryScopeBeforeLoop(context: TransformationContext): Scope | undefined {
100100
for (const scope of walkScopesUp(context)) {
101101
if (scope.type === ScopeType.Function || scope.type === ScopeType.Loop) return undefined;
102102
if (scope.type === ScopeType.Try || scope.type === ScopeType.Catch) return scope;

src/transformation/visitors/break-continue.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import * as ts from "typescript";
22
import { LuaTarget } from "../../CompilerOptions";
33
import * as lua from "../../LuaAST";
44
import { FunctionVisitor } from "../context";
5-
import { findAsyncTryScopeBeforeLoop, findScope, LoopContinued, ScopeType } from "../utils/scope";
6-
import { isInAsyncFunction } from "../utils/typescript";
5+
import { findScope, findTryScopeBeforeLoop, LoopContinued, ScopeType } from "../utils/scope";
76

87
export const transformBreakStatement: FunctionVisitor<ts.BreakStatement> = (breakStatement, context) => {
9-
const tryScope = isInAsyncFunction(breakStatement) ? findAsyncTryScopeBeforeLoop(context) : undefined;
8+
const tryScope = findTryScopeBeforeLoop(context);
109
if (tryScope) {
11-
tryScope.asyncTryHasBreak = true;
10+
tryScope.tryHasBreak = true;
1211
return [
1312
lua.createAssignmentStatement(
1413
lua.createIdentifier("____hasBroken"),
@@ -40,9 +39,9 @@ export const transformContinueStatement: FunctionVisitor<ts.ContinueStatement> =
4039
scope.loopContinued = continuedWith;
4140
}
4241

43-
const tryScope = isInAsyncFunction(statement) ? findAsyncTryScopeBeforeLoop(context) : undefined;
42+
const tryScope = findTryScopeBeforeLoop(context);
4443
if (tryScope) {
45-
tryScope.asyncTryHasContinue = continuedWith;
44+
tryScope.tryHasContinue = continuedWith;
4645
return [
4746
lua.createAssignmentStatement(
4847
lua.createIdentifier("____hasContinued"),

src/transformation/visitors/errors.ts

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ const transformAsyncTry: FunctionVisitor<ts.TryStatement> = (statement, context)
7979
chainCalls.push(lua.createExpressionStatement(promiseAwait, statement));
8080

8181
const hasReturn = tryScope.asyncTryHasReturn ?? catchScope?.asyncTryHasReturn;
82-
const hasBreak = tryScope.asyncTryHasBreak ?? catchScope?.asyncTryHasBreak;
83-
const hasContinue = tryScope.asyncTryHasContinue ?? catchScope?.asyncTryHasContinue;
82+
const hasBreak = tryScope.tryHasBreak ?? catchScope?.tryHasBreak;
83+
const hasContinue = tryScope.tryHasContinue ?? catchScope?.tryHasContinue;
8484

8585
// Build result in output order: flag declarations, awaiter, chain calls, post-checks
8686
const result: lua.Statement[] = [];
@@ -114,7 +114,12 @@ const transformAsyncTry: FunctionVisitor<ts.TryStatement> = (statement, context)
114114

115115
if (hasBreak) {
116116
result.push(
117-
lua.createIfStatement(lua.createIdentifier("____hasBroken"), lua.createBlock([lua.createBreakStatement()]))
117+
lua.createIfStatement(
118+
lua.createIdentifier("____hasBroken", statement),
119+
lua.createBlock([lua.createBreakStatement(statement)], statement),
120+
undefined,
121+
statement
122+
)
118123
);
119124
}
120125

@@ -125,21 +130,30 @@ const transformAsyncTry: FunctionVisitor<ts.TryStatement> = (statement, context)
125130
const continueStatements: lua.Statement[] = [];
126131
switch (hasContinue) {
127132
case LoopContinued.WithGoto:
128-
continueStatements.push(lua.createGotoStatement(label));
133+
continueStatements.push(lua.createGotoStatement(label, statement));
129134
break;
130135
case LoopContinued.WithContinue:
131-
continueStatements.push(lua.createContinueStatement());
136+
continueStatements.push(lua.createContinueStatement(statement));
132137
break;
133138
case LoopContinued.WithRepeatBreak:
134139
continueStatements.push(
135-
lua.createAssignmentStatement(lua.createIdentifier(label), lua.createBooleanLiteral(true))
140+
lua.createAssignmentStatement(
141+
lua.createIdentifier(label, statement),
142+
lua.createBooleanLiteral(true),
143+
statement
144+
)
136145
);
137-
continueStatements.push(lua.createBreakStatement());
146+
continueStatements.push(lua.createBreakStatement(statement));
138147
break;
139148
}
140149

141150
result.push(
142-
lua.createIfStatement(lua.createIdentifier("____hasContinued"), lua.createBlock(continueStatements))
151+
lua.createIfStatement(
152+
lua.createIdentifier("____hasContinued", statement),
153+
lua.createBlock(continueStatements, statement),
154+
undefined,
155+
statement
156+
)
143157
);
144158
}
145159

@@ -153,7 +167,7 @@ export const transformTryStatement: FunctionVisitor<ts.TryStatement> = (statemen
153167

154168
const tsTryBlock = statement.tryBlock;
155169
const tsCatchClause = statement.catchClause;
156-
const [tryBlock] = transformScopeBlock(context, tsTryBlock, ScopeType.Try);
170+
const [tryBlock, tryScope] = transformScopeBlock(context, tsTryBlock, ScopeType.Try);
157171

158172
if (
159173
(context.options.luaTarget === LuaTarget.Lua50 || context.options.luaTarget === LuaTarget.Lua51) &&
@@ -180,11 +194,13 @@ export const transformTryStatement: FunctionVisitor<ts.TryStatement> = (statemen
180194
result.push(lua.createVariableDeclarationStatement(tryReturnIdentifiers, tryCall, tsTryBlock));
181195

182196
const hasCatch = tsCatchClause && tsCatchClause.block.statements.length > 0;
197+
let catchScope: Scope | undefined;
183198
if (hasCatch) {
184199
// local ____catchSuccess
185200
result.push(lua.createVariableDeclarationStatement(catchSuccessIdentifier, undefined, tsCatchClause));
186201

187-
const [catchFunction] = transformCatchClause(context, tsCatchClause);
202+
const [catchFunction, cScope] = transformCatchClause(context, tsCatchClause);
203+
catchScope = cScope;
188204

189205
const catchIdentifier = lua.createIdentifier("____catch", tsCatchClause);
190206
result.push(lua.createVariableDeclarationStatement(catchIdentifier, catchFunction, tsCatchClause));
@@ -301,6 +317,71 @@ export const transformTryStatement: FunctionVisitor<ts.TryStatement> = (statemen
301317
);
302318
result.push(ifTrySuccessStatement);
303319

320+
// local ____hasBroken
321+
// local ____hasContinued
322+
const hasBreak = tryScope.tryHasBreak ?? catchScope?.tryHasBreak;
323+
const hasContinue = tryScope.tryHasContinue ?? catchScope?.tryHasContinue;
324+
325+
if (hasBreak || hasContinue !== undefined) {
326+
const flagDecls: lua.Identifier[] = [];
327+
if (hasBreak) flagDecls.push(lua.createIdentifier("____hasBroken", statement));
328+
if (hasContinue !== undefined) flagDecls.push(lua.createIdentifier("____hasContinued", statement));
329+
result.unshift(lua.createVariableDeclarationStatement(flagDecls, undefined, statement));
330+
}
331+
332+
// if ____hasBroken then
333+
// break
334+
// end
335+
if (hasBreak) {
336+
result.push(
337+
lua.createIfStatement(
338+
lua.createIdentifier("____hasBroken", statement),
339+
lua.createBlock([lua.createBreakStatement(statement)], statement),
340+
undefined,
341+
statement
342+
)
343+
);
344+
}
345+
346+
// if ____hasContinued then
347+
// goto __continueN (Lua 5.2+)
348+
// continue (Luau)
349+
// __continueN = true; break (Lua 5.0/5.1)
350+
// end
351+
if (hasContinue !== undefined) {
352+
const loopScope = findScope(context, ScopeType.Loop);
353+
const label = `__continue${loopScope?.id ?? ""}`;
354+
355+
const continueStatements: lua.Statement[] = [];
356+
switch (hasContinue) {
357+
case LoopContinued.WithGoto:
358+
continueStatements.push(lua.createGotoStatement(label, statement));
359+
break;
360+
case LoopContinued.WithContinue:
361+
continueStatements.push(lua.createContinueStatement(statement));
362+
break;
363+
case LoopContinued.WithRepeatBreak:
364+
continueStatements.push(
365+
lua.createAssignmentStatement(
366+
lua.createIdentifier(label, statement),
367+
lua.createBooleanLiteral(true),
368+
statement
369+
)
370+
);
371+
continueStatements.push(lua.createBreakStatement(statement));
372+
break;
373+
}
374+
375+
result.push(
376+
lua.createIfStatement(
377+
lua.createIdentifier("____hasContinued", statement),
378+
lua.createBlock(continueStatements, statement),
379+
undefined,
380+
statement
381+
)
382+
);
383+
}
384+
304385
return lua.createDoStatement(result, statement);
305386
};
306387

test/unit/error.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,130 @@ test("try/finally rethrow with non-string error", () => {
689689
`.expectToMatchJsResult();
690690
});
691691

692+
test("break inside try in loop", () => {
693+
util.testFunction`
694+
const result: number[] = [];
695+
for (let i = 0; i < 5; i++) {
696+
try {
697+
if (i === 3) break;
698+
result.push(i);
699+
} catch {}
700+
}
701+
return result;
702+
`.expectToMatchJsResult();
703+
});
704+
705+
test("continue inside try in loop", () => {
706+
util.testFunction`
707+
const result: number[] = [];
708+
for (let i = 0; i < 5; i++) {
709+
try {
710+
if (i === 2) continue;
711+
result.push(i);
712+
} catch {}
713+
}
714+
return result;
715+
`.expectToMatchJsResult();
716+
});
717+
718+
test("break inside catch in loop", () => {
719+
util.testFunction`
720+
const result: number[] = [];
721+
for (let i = 0; i < 5; i++) {
722+
try {
723+
throw i;
724+
} catch (e: any) {
725+
if (e === 3) break;
726+
result.push(e);
727+
}
728+
}
729+
return result;
730+
`.expectToMatchJsResult();
731+
});
732+
733+
test("continue inside catch in loop", () => {
734+
util.testFunction`
735+
const result: number[] = [];
736+
for (let i = 0; i < 5; i++) {
737+
try {
738+
throw i;
739+
} catch (e: any) {
740+
if (e === 2) continue;
741+
result.push(e);
742+
}
743+
}
744+
return result;
745+
`.expectToMatchJsResult();
746+
});
747+
748+
test("break inside try with finally in loop", () => {
749+
util.testFunction`
750+
const result: number[] = [];
751+
let finallyCalls = 0;
752+
for (let i = 0; i < 5; i++) {
753+
try {
754+
if (i === 3) break;
755+
result.push(i);
756+
} finally {
757+
finallyCalls++;
758+
}
759+
}
760+
return { result, finallyCalls };
761+
`.expectToMatchJsResult();
762+
});
763+
764+
test("continue inside try with finally in loop", () => {
765+
util.testFunction`
766+
const result: number[] = [];
767+
let finallyCalls = 0;
768+
for (let i = 0; i < 5; i++) {
769+
try {
770+
if (i === 2) continue;
771+
result.push(i);
772+
} finally {
773+
finallyCalls++;
774+
}
775+
}
776+
return { result, finallyCalls };
777+
`.expectToMatchJsResult();
778+
});
779+
780+
test("break inside catch with finally in loop", () => {
781+
util.testFunction`
782+
const result: number[] = [];
783+
let finallyCalls = 0;
784+
for (let i = 0; i < 5; i++) {
785+
try {
786+
throw i;
787+
} catch (e: any) {
788+
if (e === 3) break;
789+
result.push(e);
790+
} finally {
791+
finallyCalls++;
792+
}
793+
}
794+
return { result, finallyCalls };
795+
`.expectToMatchJsResult();
796+
});
797+
798+
test("continue inside catch with finally in loop", () => {
799+
util.testFunction`
800+
const result: number[] = [];
801+
let finallyCalls = 0;
802+
for (let i = 0; i < 5; i++) {
803+
try {
804+
throw i;
805+
} catch (e: any) {
806+
if (e === 2) continue;
807+
result.push(e);
808+
} finally {
809+
finallyCalls++;
810+
}
811+
}
812+
return { result, finallyCalls };
813+
`.expectToMatchJsResult();
814+
});
815+
692816
util.testEachVersion(
693817
"error stacktrace omits constructor and __TS_New",
694818
() => util.testFunction`

0 commit comments

Comments
 (0)