|
1 | 1 | import * as ts from "typescript"; |
2 | | -import { LuaTarget } from "../../CompilerOptions"; |
3 | 2 | import * as lua from "../../LuaAST"; |
4 | | -import { FunctionVisitor } from "../context"; |
5 | | -import { unsupportedForTarget } from "../utils/diagnostics"; |
| 3 | +import { FunctionVisitor, TransformationContext } from "../context"; |
6 | 4 | import { performHoisting, popScope, pushScope, ScopeType } from "../utils/scope"; |
7 | 5 |
|
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 | + ); |
11 | 37 | } |
12 | 38 |
|
| 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) => { |
13 | 48 | const scope = pushScope(context, ScopeType.Switch); |
14 | 49 |
|
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. |
16 | 51 | const switchName = `____switch${scope.id}`; |
| 52 | + const conditionName = `____cond${scope.id}`; |
17 | 53 | const switchVariable = lua.createIdentifier(switchName); |
| 54 | + const conditionVariable = lua.createIdentifier(conditionName); |
18 | 55 |
|
| 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. |
19 | 58 | 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))); |
26 | 64 | } |
| 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]; |
27 | 72 |
|
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 | + } |
34 | 78 |
|
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); |
38 | 82 |
|
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; |
42 | 85 |
|
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 | + ); |
45 | 105 |
|
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 | + } |
51 | 125 |
|
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 | + } |
53 | 161 |
|
| 162 | + // Hoist the variable, function, and import statements to the top of the switch |
54 | 163 | statements = performHoisting(context, statements); |
55 | 164 | popScope(context); |
56 | 165 |
|
| 166 | + // Add the switch expression after hoisting |
57 | 167 | const expression = context.transformExpression(statement.expression); |
58 | 168 | statements.unshift(lua.createVariableDeclarationStatement(switchVariable, expression)); |
59 | 169 |
|
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)); |
61 | 172 | }; |
0 commit comments