@@ -3,21 +3,14 @@ import * as lua from "../../LuaAST";
33import { FunctionVisitor } from "../context" ;
44import { 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 => {
2821export 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} ;
0 commit comments