Skip to content

Commit e044313

Browse files
Merge branch 'master' of github.com:ts-defold/TypeScriptToLua
2 parents 7d13666 + b4a2ddd commit e044313

4 files changed

Lines changed: 228 additions & 58 deletions

File tree

src/transformation/visitors/break-continue.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { findScope, ScopeType } from "../utils/scope";
88
export const transformBreakStatement: FunctionVisitor<ts.BreakStatement> = (breakStatement, context) => {
99
const breakableScope = findScope(context, ScopeType.Loop | ScopeType.Switch);
1010
if (breakableScope?.type === ScopeType.Switch) {
11-
return undefined;
11+
// Break is handled by the switch statement (see transformSwitchStatement)
12+
return lua.createBreakStatement(breakStatement);
1213
} else {
1314
return lua.createBreakStatement(breakStatement);
1415
}

src/transformation/visitors/switch.ts

Lines changed: 114 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,14 @@ import * as lua from "../../LuaAST";
33
import { FunctionVisitor } from "../context";
44
import { performHoisting, popScope, pushScope, ScopeType } from "../utils/scope";
55

6-
const containsBreakStatement = (statements: ts.Node[]): boolean => {
6+
const containsBreakOrReturn = (statements: ts.Node[]): boolean => {
77
for (const s of statements) {
8-
if (
9-
ts.isSwitchStatement(s) ||
10-
ts.isWhileStatement(s) ||
11-
ts.isDoStatement(s) ||
12-
ts.isForStatement(s) ||
13-
ts.isForInStatement(s) ||
14-
ts.isForOfStatement(s)
15-
) {
16-
// Ignore: Break statements are valid as children of these
17-
// statements without breaking the clause
18-
} else if (ts.isBreakStatement(s)) {
8+
if (ts.isBreakStatement(s) || ts.isReturnStatement(s)) {
199
return true;
20-
} else if (containsBreakStatement(s.getChildren())) {
10+
} else if (!ts.isBlock(s)) {
11+
// Can only ensure a break scoped in a block is deterministic
12+
continue;
13+
} else if (containsBreakOrReturn(s.getChildren())) {
2114
return true;
2215
}
2316
}
@@ -28,62 +21,132 @@ const containsBreakStatement = (statements: ts.Node[]): boolean => {
2821
export const transformSwitchStatement: FunctionVisitor<ts.SwitchStatement> = (statement, context) => {
2922
const scope = pushScope(context, ScopeType.Switch);
3023

31-
// Give the switch a unique name to prevent nested switches from acting up.
24+
// Give the switch and condition accumulator a unique name to prevent nested switches from acting up.
3225
const switchName = `____switch${scope.id}`;
26+
const conditionName = `____cond${scope.id}`;
3327
const switchVariable = lua.createIdentifier(switchName);
28+
const conditionVariable = lua.createIdentifier(conditionName);
3429

35-
// Collect the fallthrough bodies for each case as defined by the switch.
36-
const caseBody: lua.Statement[][] = [];
37-
for (let i = 0; i < statement.caseBlock.clauses.length; i++) {
38-
const end = statement.caseBlock.clauses
39-
.slice(i)
40-
.findIndex(clause => containsBreakStatement([...clause.statements]));
41-
caseBody[i] = statement.caseBlock.clauses
42-
.slice(i, end >= 0 ? end + i + 1 : undefined)
43-
.reduce<lua.Statement[]>(
44-
(statements, clause) => [...statements, ...context.transformStatements(clause.statements)],
45-
[]
46-
);
47-
}
30+
// Collect all the expressions into a single expression for use in the default clause
31+
let allExpressions: lua.BinaryExpression;
32+
statement.caseBlock.clauses.forEach(clause => {
33+
if (!ts.isDefaultClause(clause)) {
34+
allExpressions = allExpressions
35+
? lua.createBinaryExpression(
36+
allExpressions,
37+
lua.createBinaryExpression(
38+
switchVariable,
39+
context.transformExpression(clause.expression),
40+
lua.SyntaxKind.EqualityOperator
41+
),
42+
lua.SyntaxKind.OrOperator
43+
)
44+
: lua.createBinaryExpression(
45+
switchVariable,
46+
context.transformExpression(clause.expression),
47+
lua.SyntaxKind.EqualityOperator
48+
);
49+
}
50+
});
4851

4952
let statements: lua.Statement[] = [];
5053

51-
// Default will either be the only statement, or the else in the if chain
52-
const defaultIndex = statement.caseBlock.clauses.findIndex(c => ts.isDefaultClause(c));
53-
const defaultBody = defaultIndex >= 0 ? caseBody[defaultIndex] : undefined;
54-
if (defaultBody && statement.caseBlock.clauses.length === 1) {
55-
statements.push(lua.createDoStatement(defaultBody));
54+
// If the switch only has a default clause, wrap it in a single do.
55+
// Otherwise, we need to generate a set of if statements to emulate the switch.
56+
const clauses = statement.caseBlock.clauses;
57+
if (clauses.length === 1 && ts.isDefaultClause(clauses[0])) {
58+
const defaultClause = clauses[0].statements;
59+
if (defaultClause.length) {
60+
statements.push(lua.createDoStatement(context.transformStatements(defaultClause)));
61+
}
5662
} else {
57-
let concatenatedIf: lua.IfStatement | undefined = undefined;
58-
let previousCondition: lua.IfStatement | lua.Block | undefined = defaultBody
59-
? lua.createBlock(defaultBody)
60-
: undefined;
63+
// Build up the condition for each if statement
64+
// Fallthrough is handled by accepting the last condition as an additional or clause
65+
// Default is the not of all known case expressions
66+
let previousClause: ts.CaseOrDefaultClause;
67+
let condition: lua.Expression | undefined;
68+
statement.caseBlock.clauses.forEach(clause => {
69+
if (!condition || (previousClause && containsBreakOrReturn([...previousClause.statements]))) {
70+
if (ts.isDefaultClause(clause)) {
71+
// If the default is first, or followed by a break, we can't fall into it, skip.
72+
} else {
73+
condition = lua.createBinaryExpression(
74+
switchVariable,
75+
context.transformExpression(clause.expression),
76+
lua.SyntaxKind.EqualityOperator
77+
);
78+
}
79+
} else {
80+
if (ts.isDefaultClause(clause)) {
81+
// use the previous condition for the default clause
82+
} else {
83+
condition = lua.createBinaryExpression(
84+
condition,
85+
lua.createBinaryExpression(
86+
switchVariable,
87+
context.transformExpression(clause.expression),
88+
lua.SyntaxKind.EqualityOperator
89+
),
90+
lua.SyntaxKind.OrOperator
91+
);
92+
}
93+
}
94+
95+
if (clause.statements.length) {
96+
if (!ts.isDefaultClause(clause) && condition) {
97+
statements.push(
98+
lua.createAssignmentStatement(
99+
conditionVariable,
100+
lua.createBinaryExpression(conditionVariable, condition, lua.SyntaxKind.OrOperator)
101+
)
102+
);
103+
condition = undefined;
104+
}
61105

62-
// Starting from the back, concatenating ifs into one big if/elseif/[else] statement
63-
for (let i = statement.caseBlock.clauses.length - 1; i >= 0; i--) {
64-
const clause = statement.caseBlock.clauses[i];
106+
statements.push(
107+
lua.createIfStatement(
108+
conditionVariable,
109+
lua.createBlock(context.transformStatements(clause.statements))
110+
)
111+
);
112+
}
65113

66-
// Skip default clause to keep index aligned, handle in else block
67-
if (ts.isDefaultClause(clause)) continue;
114+
previousClause = clause;
115+
});
68116

69-
// If the clause condition holds, go to the correct label
70-
const condition = lua.createBinaryExpression(
71-
switchVariable,
72-
context.transformExpression(clause.expression),
73-
lua.SyntaxKind.EqualityOperator
74-
);
117+
// Amalgamate the default w/ fallthrough clauses and execute if nothing else executed above
118+
const start = clauses.findIndex(c => ts.isDefaultClause(c));
119+
if (start >= 0) {
120+
const end = statement.caseBlock.clauses
121+
.slice(start)
122+
.findIndex(clause => containsBreakOrReturn([...clause.statements]));
123+
const defaultStatements = statement.caseBlock.clauses
124+
.slice(start, end >= 0 ? end + start + 1 : undefined)
125+
.reduce<lua.Statement[]>(
126+
(statements, clause) => [...statements, ...context.transformStatements(clause.statements)],
127+
[]
128+
);
75129

76-
concatenatedIf = lua.createIfStatement(condition, lua.createBlock(caseBody[i]), previousCondition);
77-
previousCondition = concatenatedIf;
130+
if (defaultStatements.length) {
131+
statements.push(
132+
lua.createIfStatement(
133+
lua.createUnaryExpression(conditionVariable, lua.SyntaxKind.NotOperator),
134+
lua.createBlock(defaultStatements)
135+
)
136+
);
137+
}
78138
}
79-
if (concatenatedIf) statements.push(concatenatedIf);
80139
}
81140

141+
// Hoist the variable, function, and import statements to the top of the switch
82142
statements = performHoisting(context, statements);
83143
popScope(context);
84144

145+
// Add the switch expression after hoisting
85146
const expression = context.transformExpression(statement.expression);
147+
statements.unshift(lua.createVariableDeclarationStatement(conditionVariable, lua.createBooleanLiteral(false)));
86148
statements.unshift(lua.createVariableDeclarationStatement(switchVariable, expression));
87149

88-
return lua.createDoStatement(statements);
150+
// Wrap the statements in a repeat until true statement to facilitate dynamic break/returns
151+
return lua.createRepeatStatement(lua.createBlock(statements), lua.createBooleanLiteral(true));
89152
};

test/unit/__snapshots__/switch.spec.ts.snap

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,68 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`switch collapses empty case and minimizes conditions 1`] = `
4+
"require(\\"lualib_bundle\\");
5+
local ____exports = {}
6+
function ____exports.__main(self)
7+
local out = {}
8+
repeat
9+
local ____switch3 = 5
10+
local ____cond3 = false
11+
____cond3 = ____cond3 or (((____switch3 == 0) or (____switch3 == 1)) or (____switch3 == 2))
12+
if ____cond3 then
13+
__TS__ArrayPush(out, \\"0,1,2\\")
14+
end
15+
____cond3 = ____cond3 or (____switch3 == 3)
16+
if ____cond3 then
17+
__TS__ArrayPush(out, \\"3\\")
18+
break
19+
end
20+
if ____cond3 then
21+
__TS__ArrayPush(out, \\"default\\")
22+
end
23+
____cond3 = ____cond3 or (____switch3 == 4)
24+
if ____cond3 then
25+
__TS__ArrayPush(out, \\"4\\")
26+
end
27+
if not ____cond3 then
28+
__TS__ArrayPush(out, \\"default\\")
29+
__TS__ArrayPush(out, \\"4\\")
30+
end
31+
until true
32+
return out
33+
end
34+
return ____exports"
35+
`;
36+
337
exports[`switch uses elseif 1`] = `
438
"local ____exports = {}
539
function ____exports.__main(self)
640
local result = -1
7-
do
41+
repeat
842
local ____switch3 = 2
9-
if ____switch3 == 0 then
43+
local ____cond3 = false
44+
____cond3 = ____cond3 or (____switch3 == 0)
45+
if ____cond3 then
1046
do
1147
result = 200
48+
break
1249
end
13-
elseif ____switch3 == 1 then
50+
end
51+
____cond3 = ____cond3 or (____switch3 == 1)
52+
if ____cond3 then
1453
do
1554
result = 100
55+
break
1656
end
17-
elseif ____switch3 == 2 then
57+
end
58+
____cond3 = ____cond3 or (____switch3 == 2)
59+
if ____cond3 then
1860
do
1961
result = 1
62+
break
2063
end
2164
end
22-
end
65+
until true
2366
return result
2467
end
2568
return ____exports"

test/unit/switch.spec.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ test.each([0, 1, 2, 3])("switchWithBrackets (%p)", inp => {
207207
`.expectToMatchJsResult();
208208
});
209209

210-
test.each([0, 1, 2, 3])("switchWithBracketsBreakInConditional (%p)", inp => {
210+
test.each([0, 1, 2, 3, 4])("switchWithBracketsBreakInConditional (%p)", inp => {
211211
util.testFunction`
212212
let result: number = -1;
213213
@@ -223,6 +223,11 @@ test.each([0, 1, 2, 3])("switchWithBracketsBreakInConditional (%p)", inp => {
223223
}
224224
case 2: {
225225
result = 2;
226+
227+
if (result != 2) break;
228+
}
229+
case 3: {
230+
result = 3;
226231
break;
227232
}
228233
}
@@ -360,3 +365,61 @@ test("switch fallthrough stops after default", () => {
360365
return out;
361366
`.expectToMatchJsResult();
362367
});
368+
369+
test("switch does not pollute parent scope", () => {
370+
util.testFunction`
371+
let x: number = 0;
372+
let y = 1;
373+
switch (x) {
374+
case 0:
375+
let y = 2;
376+
}
377+
return y;
378+
`.expectToMatchJsResult();
379+
});
380+
381+
test("switch collapses empty case and minimizes conditions", () => {
382+
util.testFunction`
383+
const out = [];
384+
switch (5 as number) {
385+
case 0:
386+
case 1:
387+
case 2:
388+
out.push("0,1,2");
389+
case 3:
390+
out.push("3");
391+
break;
392+
default:
393+
out.push("default");
394+
case 4:
395+
out.push("4");
396+
}
397+
return out;
398+
`
399+
.expectLuaToMatchSnapshot()
400+
.expectToMatchJsResult();
401+
});
402+
403+
test("switch handles side-effects", () => {
404+
util.testFunction`
405+
const out = [];
406+
407+
let y = 0;
408+
function foo() {
409+
return y++;
410+
}
411+
412+
let x = 0;
413+
switch (x) {
414+
case foo():
415+
out.push(1);
416+
case foo():
417+
out.push(2);
418+
case foo():
419+
out.push(3);
420+
}
421+
422+
out.push(y);
423+
return out;
424+
`.expectToMatchJsResult();
425+
});

0 commit comments

Comments
 (0)