Skip to content

Commit da0de8b

Browse files
feat: switch statements for lua 5.1
1 parent a6a89df commit da0de8b

File tree

4 files changed

+69
-77
lines changed

4 files changed

+69
-77
lines changed

src/transformation/visitors/break-continue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ 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 lua.createGotoStatement(`____switch${breakableScope.id}_end`);
11+
return undefined;
1212
} else {
1313
return lua.createBreakStatement(breakStatement);
1414
}

src/transformation/visitors/switch.ts

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,81 @@
11
import * as ts from "typescript";
2-
import { LuaTarget } from "../../CompilerOptions";
32
import * as lua from "../../LuaAST";
43
import { FunctionVisitor } from "../context";
5-
import { unsupportedForTarget } from "../utils/diagnostics";
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 containsBreakStatement = (statements: ts.Node[]): boolean => {
7+
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)) {
19+
return true;
20+
} else if (containsBreakStatement(s.getChildren())) {
21+
return true;
22+
}
1123
}
1224

25+
return false;
26+
};
27+
28+
export const transformSwitchStatement: FunctionVisitor<ts.SwitchStatement> = (statement, context) => {
1329
const scope = pushScope(context, ScopeType.Switch);
1430

1531
// Give the switch a unique name to prevent nested switches from acting up.
1632
const switchName = `____switch${scope.id}`;
1733
const switchVariable = lua.createIdentifier(switchName);
1834

19-
let statements: lua.Statement[] = [];
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) => [
45+
...statements,
46+
lua.createDoStatement(context.transformStatements(clause.statements)),
47+
],
48+
[]
49+
);
50+
}
2051

2152
// 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;
26-
}
27-
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-
);
53+
const defaultIndex = statement.caseBlock.clauses.findIndex(c => ts.isDefaultClause(c));
54+
const concatenatedIf = statement.caseBlock.clauses.reduceRight<lua.IfStatement | lua.Block | undefined>(
55+
(previousCondition, clause, index) => {
56+
if (ts.isDefaultClause(clause)) {
57+
// Skip default clause here (needs to be included to ensure index lines up with index later)
58+
return previousCondition;
59+
}
3460

35-
const goto = lua.createGotoStatement(`${switchName}_case_${index}`);
36-
return lua.createIfStatement(condition, lua.createBlock([goto]), previousCondition);
37-
}, undefined as lua.IfStatement | undefined);
61+
// If the clause condition holds, go to the correct label
62+
const condition = lua.createBinaryExpression(
63+
switchVariable,
64+
context.transformExpression(clause.expression),
65+
lua.SyntaxKind.EqualityOperator
66+
);
3867

39-
if (concatenatedIf) {
40-
statements.push(concatenatedIf);
41-
}
68+
return lua.createIfStatement(condition, lua.createBlock(caseBody[index]), previousCondition);
69+
},
70+
defaultIndex >= 0 ? lua.createBlock(caseBody[defaultIndex]) : undefined
71+
);
4272

43-
const hasDefaultCase = statement.caseBlock.clauses.some(ts.isDefaultClause);
44-
statements.push(lua.createGotoStatement(`${switchName}_${hasDefaultCase ? "case_default" : "end"}`));
73+
let statements: lua.Statement[] = [];
4574

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)));
75+
if (concatenatedIf) {
76+
statements.push(concatenatedIf as unknown as lua.IfStatement);
5077
}
5178

52-
statements.push(lua.createLabelStatement(`${switchName}_end`));
53-
5479
statements = performHoisting(context, statements);
5580
popScope(context);
5681

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

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,29 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`switch not allowed in 5.1: code 1`] = `
4-
"local ____exports = {}
5-
function ____exports.__main(self)
6-
local ____switch3 = \\"abc\\"
7-
goto ____switch3_end
8-
::____switch3_end::
9-
end
10-
return ____exports"
11-
`;
12-
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-
153
exports[`switch uses elseif 1`] = `
164
"local ____exports = {}
175
function ____exports.__main(self)
186
local result = -1
197
local ____switch3 = 2
208
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
309
do
31-
result = 200
32-
goto ____switch3_end
10+
do
11+
result = 200
12+
end
3313
end
34-
end
35-
::____switch3_case_1::
36-
do
14+
elseif ____switch3 == 1 then
3715
do
38-
result = 100
39-
goto ____switch3_end
16+
do
17+
result = 100
18+
end
4019
end
41-
end
42-
::____switch3_case_2::
43-
do
20+
elseif ____switch3 == 2 then
4421
do
45-
result = 1
46-
goto ____switch3_end
22+
do
23+
result = 1
24+
end
4725
end
4826
end
49-
::____switch3_end::
5027
return result
5128
end
5229
return ____exports"

test/unit/switch.spec.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as tstl from "../../src";
2-
import { unsupportedForTarget } from "../../src/transformation/utils/diagnostics";
31
import * as util from "../util";
42

53
test.each([0, 1, 2, 3])("switch (%p)", inp => {
@@ -288,14 +286,6 @@ test("switch uses elseif", () => {
288286
.expectToMatchJsResult();
289287
});
290288

291-
test("switch not allowed in 5.1", () => {
292-
util.testFunction`
293-
switch ("abc") {}
294-
`
295-
.setOptions({ luaTarget: tstl.LuaTarget.Lua51 })
296-
.expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]);
297-
});
298-
299289
// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/967
300290
test("switch default case not last - first", () => {
301291
util.testFunction`

0 commit comments

Comments
 (0)