-
-
Notifications
You must be signed in to change notification settings - Fork 185
Expand file tree
/
Copy pathswitch.ts
More file actions
269 lines (240 loc) · 11.8 KB
/
switch.ts
File metadata and controls
269 lines (240 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import * as ts from "typescript";
import * as lua from "../../LuaAST";
import { FunctionVisitor, TransformationContext } from "../context";
import { transformInPrecedingStatementScope, WithPrecedingStatements } from "../utils/preceding-statements";
import { ScopeType, separateHoistedStatements } from "../utils/scope";
import { createShortCircuitBinaryExpressionPrecedingStatements } from "./binary-expression";
const containsBreakOrReturn = (nodes: Iterable<ts.Node>): boolean => {
for (const s of nodes) {
if (ts.isBreakStatement(s) || ts.isReturnStatement(s)) {
return true;
} else if (ts.isBlock(s) && containsBreakOrReturn(s.statements)) {
return true;
} else if (s.kind === ts.SyntaxKind.SyntaxList) {
// We cannot use getChildren() because that breaks when using synthetic nodes from transformers
// So get children the long way
const children: ts.Node[] = [];
ts.forEachChild(s, c => children.push(c));
if (containsBreakOrReturn(children)) {
return true;
}
}
}
return false;
};
const createOrExpression = (
context: TransformationContext,
left: lua.Expression,
right: lua.Expression,
rightPrecedingStatements: lua.Statement[]
): WithPrecedingStatements<lua.Expression> => {
if (rightPrecedingStatements.length > 0) {
return createShortCircuitBinaryExpressionPrecedingStatements(
context,
left,
right,
rightPrecedingStatements,
ts.SyntaxKind.BarBarToken
);
} else {
return {
precedingStatements: rightPrecedingStatements,
result: lua.createBinaryExpression(left, right, lua.SyntaxKind.OrOperator),
};
}
};
const coalesceCondition = (
condition: lua.Expression | undefined,
conditionPrecedingStatements: lua.Statement[],
switchVariable: lua.Identifier,
expression: ts.Expression,
context: TransformationContext
): WithPrecedingStatements<lua.Expression> => {
const { precedingStatements, result: transformedExpression } = transformInPrecedingStatementScope(context, () =>
context.transformExpression(expression)
);
// Coalesce skipped statements
const comparison = lua.createBinaryExpression(
switchVariable,
transformedExpression,
lua.SyntaxKind.EqualityOperator
);
if (condition) {
return createOrExpression(context, condition, comparison, precedingStatements);
}
// Next condition
return { precedingStatements: [...conditionPrecedingStatements, ...precedingStatements], result: comparison };
};
export const transformSwitchStatement: FunctionVisitor<ts.SwitchStatement> = (statement, context) => {
const scope = context.pushScope(ScopeType.Switch, statement);
// Give the switch and condition accumulator a unique name to prevent nested switches from acting up.
const switchName = `____switch${scope.id}`;
const conditionName = `____cond${scope.id}`;
const switchVariable = lua.createIdentifier(switchName);
const conditionVariable = lua.createIdentifier(conditionName);
// If the switch only has a default clause, wrap it in a single do.
// Otherwise, we need to generate a set of if statements to emulate the switch.
const statements: lua.Statement[] = [];
const hoistedStatements: lua.Statement[] = [];
const hoistedIdentifiers: lua.Identifier[] = [];
const clauses = statement.caseBlock.clauses;
if (clauses.length === 1 && ts.isDefaultClause(clauses[0])) {
const defaultClause = clauses[0].statements;
if (defaultClause.length) {
const {
statements: defaultStatements,
hoistedStatements: defaultHoistedStatements,
hoistedIdentifiers: defaultHoistedIdentifiers,
} = separateHoistedStatements(context, context.transformStatements(defaultClause));
hoistedStatements.push(...defaultHoistedStatements);
hoistedIdentifiers.push(...defaultHoistedIdentifiers);
statements.push(lua.createDoStatement(defaultStatements));
}
} else {
// Build up the condition for each if statement
let defaultTransformed = false;
let isInitialCondition = true;
let condition: lua.Expression | undefined = undefined;
let conditionPrecedingStatements: lua.Statement[] = [];
for (let i = 0; i < clauses.length; i++) {
const clause = clauses[i];
const previousClause: ts.CaseOrDefaultClause | undefined = clauses[i - 1];
// Skip redundant default clauses, will be handled in final default case
if (i === 0 && ts.isDefaultClause(clause)) continue;
if (ts.isDefaultClause(clause) && previousClause && containsBreakOrReturn(previousClause.statements)) {
continue;
}
// Compute the condition for the if statement
if (!ts.isDefaultClause(clause)) {
const { precedingStatements, result } = coalesceCondition(
condition,
conditionPrecedingStatements,
switchVariable,
clause.expression,
context
);
conditionPrecedingStatements = precedingStatements;
condition = result;
// Skip empty clauses unless final clause (i.e side-effects)
if (i !== clauses.length - 1 && clause.statements.length === 0) continue;
// Declare or assign condition variable
if (isInitialCondition) {
statements.push(
...conditionPrecedingStatements,
lua.createVariableDeclarationStatement(conditionVariable, condition)
);
} else {
const { precedingStatements, result } = createOrExpression(
context,
conditionVariable,
condition,
conditionPrecedingStatements
);
conditionPrecedingStatements = precedingStatements;
condition = result;
statements.push(
...conditionPrecedingStatements,
lua.createAssignmentStatement(conditionVariable, condition)
);
}
isInitialCondition = false;
} else {
// If the default is proceeded by empty clauses and will be emitted we may need to initialize the condition
if (isInitialCondition) {
statements.push(
...conditionPrecedingStatements,
lua.createVariableDeclarationStatement(
conditionVariable,
condition ?? lua.createBooleanLiteral(false)
)
);
// Clear condition ot ensure it is not evaluated twice
condition = undefined;
conditionPrecedingStatements = [];
isInitialCondition = false;
}
// Allow default to fallthrough to final default clause
if (i === clauses.length - 1) {
// Evaluate the final condition that we may be skipping
if (condition) {
const { precedingStatements, result } = createOrExpression(
context,
conditionVariable,
condition,
conditionPrecedingStatements
);
conditionPrecedingStatements = precedingStatements;
condition = result;
statements.push(
...conditionPrecedingStatements,
lua.createAssignmentStatement(conditionVariable, condition)
);
}
continue;
}
}
// Transform the clause and append the final break statement if necessary
const {
statements: clauseStatements,
hoistedStatements: clauseHoistedStatements,
hoistedIdentifiers: clauseHoistedIdentifiers,
} = separateHoistedStatements(context, context.transformStatements(clause.statements));
if (i === clauses.length - 1 && !containsBreakOrReturn(clause.statements)) {
clauseStatements.push(lua.createBreakStatement());
}
hoistedStatements.push(...clauseHoistedStatements);
hoistedIdentifiers.push(...clauseHoistedIdentifiers);
// Remember that we transformed default clause so we don't duplicate hoisted statements later
if (ts.isDefaultClause(clause)) {
defaultTransformed = true;
}
// Push if statement for case
statements.push(lua.createIfStatement(conditionVariable, lua.createBlock(clauseStatements)));
// Clear condition for next clause
condition = undefined;
conditionPrecedingStatements = [];
}
// If no conditions above match, we need to create the final default case code-path,
// as we only handle fallthrough into defaults in the previous if statement chain
const start = clauses.findIndex(c => ts.isDefaultClause(c));
if (start >= 0) {
// Find the last clause that we can fallthrough to
const end = clauses.findIndex(
(clause, index) => index >= start && containsBreakOrReturn(clause.statements)
);
const {
statements: defaultStatements,
hoistedStatements: defaultHoistedStatements,
hoistedIdentifiers: defaultHoistedIdentifiers,
} = separateHoistedStatements(context, context.transformStatements(clauses[start].statements));
// Only push hoisted statements if this is the first time we're transforming the default clause
if (!defaultTransformed) {
hoistedStatements.push(...defaultHoistedStatements);
hoistedIdentifiers.push(...defaultHoistedIdentifiers);
}
// Combine the fallthrough statements
for (const clause of clauses.slice(start + 1, end >= 0 ? end + 1 : undefined)) {
let statements = context.transformStatements(clause.statements);
// Drop hoisted statements as they were already added when clauses were initially transformed above
({ statements } = separateHoistedStatements(context, statements));
defaultStatements.push(...statements);
}
// Add the default clause if it has any statements
// The switch will always break on the final clause and skip execution if valid to do so
if (defaultStatements.length) {
statements.push(lua.createDoStatement(defaultStatements));
}
}
}
// Hoist the variable, function, and import statements to the top of the switch
statements.unshift(...hoistedStatements);
if (hoistedIdentifiers.length > 0) {
statements.unshift(lua.createVariableDeclarationStatement(hoistedIdentifiers));
}
context.popScope();
// Add the switch expression after hoisting
const expression = context.transformExpression(statement.expression);
statements.unshift(lua.createVariableDeclarationStatement(switchVariable, expression));
// Wrap the statements in a repeat until true statement to facilitate dynamic break/returns
return lua.createRepeatStatement(lua.createBlock(statements), lua.createBooleanLiteral(true));
};