Skip to content

Commit ec76f4b

Browse files
feat: switch statements for lua 5.1+ (universal) (#1098)
* feat: switch statements for lua 5.1 * refactor: cleanup & switch with only default * fix: lexical scoping create scope for switch and remove additional scope for each clause. * fix: test do not pollute parent scope * fix: scope switch in repeat-until, emit break * fix: non duplicating case body switch * chore: comments * refactor: handle side-effects plus test * fix: cleanup & feedback * chore: remove dead code * refactor: cleanup redundent break statement * refactor: cleanup for maintainability and review * chore: remove dead comment * fix: handle no initial statment fallthrough * test: async case * fix: containsBreakOrReturn & final default fallthrough * refactor: coalesceCondition function * fix: do not copy statements to check for break * refactor: cleanup clauses access * chore(nit): comment * fix: ensure final clause is evaluated * refactor: simplify and cleanup output * fix: remove redundant final default clause * fix: ensure we evaluate empty fallthrough clause
1 parent dd88722 commit ec76f4b

File tree

4 files changed

+391
-94
lines changed

4 files changed

+391
-94
lines changed

src/transformation/visitors/break-continue.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@ import { unsupportedForTarget } from "../utils/diagnostics";
66
import { findScope, ScopeType } from "../utils/scope";
77

88
export const transformBreakStatement: FunctionVisitor<ts.BreakStatement> = (breakStatement, context) => {
9-
const breakableScope = findScope(context, ScopeType.Loop | ScopeType.Switch);
10-
if (breakableScope?.type === ScopeType.Switch) {
11-
return lua.createGotoStatement(`____switch${breakableScope.id}_end`);
12-
} else {
13-
return lua.createBreakStatement(breakStatement);
14-
}
9+
void context;
10+
return lua.createBreakStatement(breakStatement);
1511
};
1612

1713
export const transformContinueStatement: FunctionVisitor<ts.ContinueStatement> = (statement, context) => {
Lines changed: 145 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,172 @@
11
import * as ts from "typescript";
2-
import { LuaTarget } from "../../CompilerOptions";
32
import * as lua from "../../LuaAST";
4-
import { FunctionVisitor } from "../context";
5-
import { unsupportedForTarget } from "../utils/diagnostics";
3+
import { FunctionVisitor, TransformationContext } from "../context";
64
import { performHoisting, popScope, pushScope, ScopeType } from "../utils/scope";
75

8-
export const transformSwitchStatement: FunctionVisitor<ts.SwitchStatement> = (statement, context) => {
9-
if (context.luaTarget === LuaTarget.Universal || context.luaTarget === LuaTarget.Lua51) {
10-
context.diagnostics.push(unsupportedForTarget(statement, "Switch statements", LuaTarget.Lua51));
6+
const containsBreakOrReturn = (nodes: Iterable<ts.Node>): boolean => {
7+
for (const s of nodes) {
8+
if (ts.isBreakStatement(s) || ts.isReturnStatement(s)) {
9+
return true;
10+
} else if (ts.isBlock(s) && containsBreakOrReturn(s.getChildren())) {
11+
return true;
12+
} else if (s.kind === ts.SyntaxKind.SyntaxList && containsBreakOrReturn(s.getChildren())) {
13+
return true;
14+
}
15+
}
16+
17+
return false;
18+
};
19+
20+
const coalesceCondition = (
21+
condition: lua.Expression | undefined,
22+
switchVariable: lua.Identifier,
23+
expression: ts.Expression,
24+
context: TransformationContext
25+
): lua.Expression => {
26+
// Coalesce skipped statements
27+
if (condition) {
28+
return lua.createBinaryExpression(
29+
condition,
30+
lua.createBinaryExpression(
31+
switchVariable,
32+
context.transformExpression(expression),
33+
lua.SyntaxKind.EqualityOperator
34+
),
35+
lua.SyntaxKind.OrOperator
36+
);
1137
}
1238

39+
// Next condition
40+
return lua.createBinaryExpression(
41+
switchVariable,
42+
context.transformExpression(expression),
43+
lua.SyntaxKind.EqualityOperator
44+
);
45+
};
46+
47+
export const transformSwitchStatement: FunctionVisitor<ts.SwitchStatement> = (statement, context) => {
1348
const scope = pushScope(context, ScopeType.Switch);
1449

15-
// Give the switch a unique name to prevent nested switches from acting up.
50+
// Give the switch and condition accumulator a unique name to prevent nested switches from acting up.
1651
const switchName = `____switch${scope.id}`;
52+
const conditionName = `____cond${scope.id}`;
1753
const switchVariable = lua.createIdentifier(switchName);
54+
const conditionVariable = lua.createIdentifier(conditionName);
1855

56+
// If the switch only has a default clause, wrap it in a single do.
57+
// Otherwise, we need to generate a set of if statements to emulate the switch.
1958
let statements: lua.Statement[] = [];
20-
21-
// Starting from the back, concatenating ifs into one big if/elseif statement
22-
const concatenatedIf = statement.caseBlock.clauses.reduceRight((previousCondition, clause, index) => {
23-
if (ts.isDefaultClause(clause)) {
24-
// Skip default clause here (needs to be included to ensure index lines up with index later)
25-
return previousCondition;
59+
const clauses = statement.caseBlock.clauses;
60+
if (clauses.length === 1 && ts.isDefaultClause(clauses[0])) {
61+
const defaultClause = clauses[0].statements;
62+
if (defaultClause.length) {
63+
statements.push(lua.createDoStatement(context.transformStatements(defaultClause)));
2664
}
65+
} else {
66+
// Build up the condition for each if statement
67+
let isInitialCondition = true;
68+
let condition: lua.Expression | undefined = undefined;
69+
for (let i = 0; i < clauses.length; i++) {
70+
const clause = clauses[i];
71+
const previousClause: ts.CaseOrDefaultClause | undefined = clauses[i - 1];
2772

28-
// If the clause condition holds, go to the correct label
29-
const condition = lua.createBinaryExpression(
30-
switchVariable,
31-
context.transformExpression(clause.expression),
32-
lua.SyntaxKind.EqualityOperator
33-
);
73+
// Skip redundant default clauses, will be handled in final default case
74+
if (i === 0 && ts.isDefaultClause(clause)) continue;
75+
if (ts.isDefaultClause(clause) && previousClause && containsBreakOrReturn(previousClause.statements)) {
76+
continue;
77+
}
3478

35-
const goto = lua.createGotoStatement(`${switchName}_case_${index}`);
36-
return lua.createIfStatement(condition, lua.createBlock([goto]), previousCondition);
37-
}, undefined as lua.IfStatement | undefined);
79+
// Compute the condition for the if statement
80+
if (!ts.isDefaultClause(clause)) {
81+
condition = coalesceCondition(condition, switchVariable, clause.expression, context);
3882

39-
if (concatenatedIf) {
40-
statements.push(concatenatedIf);
41-
}
83+
// Skip empty clauses unless final clause (i.e side-effects)
84+
if (i !== clauses.length - 1 && clause.statements.length === 0) continue;
4285

43-
const hasDefaultCase = statement.caseBlock.clauses.some(ts.isDefaultClause);
44-
statements.push(lua.createGotoStatement(`${switchName}_${hasDefaultCase ? "case_default" : "end"}`));
86+
// Declare or assign condition variable
87+
statements.push(
88+
isInitialCondition
89+
? lua.createVariableDeclarationStatement(conditionVariable, condition)
90+
: lua.createAssignmentStatement(
91+
conditionVariable,
92+
lua.createBinaryExpression(conditionVariable, condition, lua.SyntaxKind.OrOperator)
93+
)
94+
);
95+
isInitialCondition = false;
96+
} else {
97+
// If the default is proceeded by empty clauses and will be emitted we may need to initialize the condition
98+
if (isInitialCondition) {
99+
statements.push(
100+
lua.createVariableDeclarationStatement(
101+
conditionVariable,
102+
condition ?? lua.createBooleanLiteral(false)
103+
)
104+
);
45105

46-
for (const [index, clause] of statement.caseBlock.clauses.entries()) {
47-
const labelName = `${switchName}_case_${ts.isCaseClause(clause) ? index : "default"}`;
48-
statements.push(lua.createLabelStatement(labelName));
49-
statements.push(lua.createDoStatement(context.transformStatements(clause.statements)));
50-
}
106+
// Clear condition ot ensure it is not evaluated twice
107+
condition = undefined;
108+
isInitialCondition = false;
109+
}
110+
111+
// Allow default to fallthrough to final default clause
112+
if (i === clauses.length - 1) {
113+
// Evaluate the final condition that we may be skipping
114+
if (condition) {
115+
statements.push(
116+
lua.createAssignmentStatement(
117+
conditionVariable,
118+
lua.createBinaryExpression(conditionVariable, condition, lua.SyntaxKind.OrOperator)
119+
)
120+
);
121+
}
122+
continue;
123+
}
124+
}
51125

52-
statements.push(lua.createLabelStatement(`${switchName}_end`));
126+
// Transform the clause and append the final break statement if necessary
127+
const clauseStatements = context.transformStatements(clause.statements);
128+
if (i === clauses.length - 1 && !containsBreakOrReturn(clause.statements)) {
129+
clauseStatements.push(lua.createBreakStatement());
130+
}
131+
132+
// Push if statement for case
133+
statements.push(lua.createIfStatement(conditionVariable, lua.createBlock(clauseStatements)));
134+
135+
// Clear condition for next clause
136+
condition = undefined;
137+
}
138+
139+
// If no conditions above match, we need to create the final default case code-path,
140+
// as we only handle fallthrough into defaults in the previous if statement chain
141+
const start = clauses.findIndex(c => ts.isDefaultClause(c));
142+
if (start >= 0) {
143+
// Find the last clause that we can fallthrough to
144+
const end = clauses.findIndex(
145+
(clause, index) => index >= start && containsBreakOrReturn(clause.statements)
146+
);
147+
148+
// Combine the default and all fallthrough statements
149+
const defaultStatements: lua.Statement[] = [];
150+
clauses
151+
.slice(start, end >= 0 ? end + 1 : undefined)
152+
.forEach(c => defaultStatements.push(...context.transformStatements(c.statements)));
153+
154+
// Add the default clause if it has any statements
155+
// The switch will always break on the final clause and skip execution if valid to do so
156+
if (defaultStatements.length) {
157+
statements.push(lua.createDoStatement(defaultStatements));
158+
}
159+
}
160+
}
53161

162+
// Hoist the variable, function, and import statements to the top of the switch
54163
statements = performHoisting(context, statements);
55164
popScope(context);
56165

166+
// Add the switch expression after hoisting
57167
const expression = context.transformExpression(statement.expression);
58168
statements.unshift(lua.createVariableDeclarationStatement(switchVariable, expression));
59169

60-
return statements;
170+
// Wrap the statements in a repeat until true statement to facilitate dynamic break/returns
171+
return lua.createRepeatStatement(lua.createBlock(statements), lua.createBooleanLiteral(true));
61172
};
Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,80 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`switch not allowed in 5.1: code 1`] = `
4-
"local ____exports = {}
3+
exports[`switch empty fallthrough to default (0) 1`] = `
4+
"require(\\"lualib_bundle\\");
5+
local ____exports = {}
56
function ____exports.__main(self)
6-
local ____switch3 = \\"abc\\"
7-
goto ____switch3_end
8-
::____switch3_end::
7+
local out = {}
8+
repeat
9+
local ____switch3 = 0
10+
local ____cond3 = ____switch3 == 1
11+
do
12+
__TS__ArrayPush(out, \\"default\\")
13+
end
14+
until true
15+
return out
916
end
1017
return ____exports"
1118
`;
1219

13-
exports[`switch not allowed in 5.1: diagnostics 1`] = `"main.ts(2,9): error TSTL: Switch statements is/are not supported for target Lua 5.1."`;
14-
15-
exports[`switch uses elseif 1`] = `
16-
"local ____exports = {}
20+
exports[`switch empty fallthrough to default (1) 1`] = `
21+
"require(\\"lualib_bundle\\");
22+
local ____exports = {}
1723
function ____exports.__main(self)
18-
local result = -1
19-
local ____switch3 = 2
20-
if ____switch3 == 0 then
21-
goto ____switch3_case_0
22-
elseif ____switch3 == 1 then
23-
goto ____switch3_case_1
24-
elseif ____switch3 == 2 then
25-
goto ____switch3_case_2
26-
end
27-
goto ____switch3_end
28-
::____switch3_case_0::
29-
do
24+
local out = {}
25+
repeat
26+
local ____switch3 = 1
27+
local ____cond3 = ____switch3 == 1
3028
do
31-
result = 200
32-
goto ____switch3_end
29+
__TS__ArrayPush(out, \\"default\\")
3330
end
34-
end
35-
::____switch3_case_1::
36-
do
37-
do
38-
result = 100
39-
goto ____switch3_end
31+
until true
32+
return out
33+
end
34+
return ____exports"
35+
`;
36+
37+
exports[`switch produces optimal output 1`] = `
38+
"require(\\"lualib_bundle\\");
39+
local ____exports = {}
40+
function ____exports.__main(self)
41+
local x = 0
42+
local out = {}
43+
repeat
44+
local ____switch3 = 0
45+
local ____cond3 = ((____switch3 == 0) or (____switch3 == 1)) or (____switch3 == 2)
46+
if ____cond3 then
47+
__TS__ArrayPush(out, \\"0,1,2\\")
48+
break
49+
end
50+
____cond3 = ____cond3 or (____switch3 == 3)
51+
if ____cond3 then
52+
do
53+
__TS__ArrayPush(out, \\"3\\")
54+
break
55+
end
56+
end
57+
____cond3 = ____cond3 or (____switch3 == 4)
58+
if ____cond3 then
59+
break
4060
end
41-
end
42-
::____switch3_case_2::
43-
do
4461
do
45-
result = 1
46-
goto ____switch3_end
62+
x = x + 1
63+
__TS__ArrayPush(
64+
out,
65+
\\"default = \\" .. tostring(x)
66+
)
67+
do
68+
__TS__ArrayPush(out, \\"3\\")
69+
break
70+
end
4771
end
48-
end
49-
::____switch3_end::
50-
return result
72+
until true
73+
__TS__ArrayPush(
74+
out,
75+
tostring(x)
76+
)
77+
return out
5178
end
5279
return ____exports"
5380
`;

0 commit comments

Comments
 (0)