Skip to content

Commit 4ec743d

Browse files
authored
TSL Transpiler: Add Support for Switch Statements (#31272)
* init branch * work on switch transpiler * work * alternate approach * remove logs, add default case * rework switch to use parseBody and getGroupDelta * fix read bug * remove comments, add continue and break * add import and improve code style * skip break statements in switch cases * add multiples case conditions support ---------
1 parent 1f603c3 commit 4ec743d

File tree

3 files changed

+267
-5
lines changed

3 files changed

+267
-5
lines changed

examples/jsm/transpiler/AST.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,26 @@ export class Discard {
214214

215215
}
216216

217+
export class Continue {
218+
219+
constructor() {
220+
221+
this.isContinue = true;
222+
223+
}
224+
225+
}
226+
227+
export class Break {
228+
229+
constructor() {
230+
231+
this.isBreak = true;
232+
233+
}
234+
235+
}
236+
217237
export class Accessor {
218238

219239
constructor( property ) {
@@ -278,3 +298,37 @@ export class For {
278298
}
279299

280300
}
301+
302+
export class Switch {
303+
304+
constructor( discriminant ) {
305+
306+
this.body = [];
307+
308+
this.discriminant = discriminant;
309+
this.case = null;
310+
this.isSwitch = true;
311+
312+
}
313+
314+
}
315+
316+
export class SwitchCase {
317+
318+
constructor( caseCondition ) {
319+
320+
// Condition for the case body to execute
321+
this.caseCondition = caseCondition;
322+
323+
// Body of the case statement
324+
this.body = [];
325+
326+
// Next case to fall to if current case fails
327+
this.nextCase = null;
328+
329+
this.isDefault = caseCondition === null ? true : false;
330+
this.isSwitchCase = true;
331+
332+
}
333+
334+
}

examples/jsm/transpiler/GLSLDecoder.js

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Program, FunctionDeclaration, For, AccessorElements, Ternary, Varying, DynamicElement, StaticElement, FunctionParameter, Unary, Conditional, VariableDeclaration, Operator, Number, String, FunctionCall, Return, Accessor, Uniform, Discard } from './AST.js';
1+
import { Program, FunctionDeclaration, Switch, For, AccessorElements, Ternary, Varying, DynamicElement, StaticElement, FunctionParameter, Unary, Conditional, VariableDeclaration, Operator, Number, String, FunctionCall, Return, Accessor, Uniform, Discard, SwitchCase, Continue, Break } from './AST.js';
22

33
const unaryOperators = [
44
'+', '-', '~', '!', '++', '--'
@@ -63,7 +63,7 @@ function getFunctionName( str ) {
6363
function getGroupDelta( str ) {
6464

6565
if ( str === '(' || str === '[' || str === '{' ) return 1;
66-
if ( str === ')' || str === ']' || str === '}' ) return - 1;
66+
if ( str === ')' || str === ']' || str === '}' || str === 'case' || str === 'default' ) return - 1;
6767

6868
return 0;
6969

@@ -492,6 +492,14 @@ class GLSLDecoder {
492492

493493
return new Discard();
494494

495+
} else if ( firstToken.str === 'continue' ) {
496+
497+
return new Continue();
498+
499+
} else if ( firstToken.str === 'break' ) {
500+
501+
return new Break();
502+
495503
}
496504

497505
const secondToken = tokens[ 1 ];
@@ -801,6 +809,110 @@ class GLSLDecoder {
801809

802810
}
803811

812+
parseSwitch() {
813+
814+
const parseSwitchExpression = () => {
815+
816+
this.readToken(); // Skip 'switch'
817+
818+
const switchDeterminantTokens = this.readTokensUntil( ')' );
819+
820+
// Parse expresison between parentheses. Index 1: char after '('. Index -1: char before ')'
821+
return this.parseExpressionFromTokens( switchDeterminantTokens.slice( 1, - 1 ) );
822+
823+
824+
};
825+
826+
const parseSwitchBlock = ( switchStatement ) => {
827+
828+
// Validate curly braces
829+
if ( this.getToken().str === '{' ) {
830+
831+
this.readToken(); // Skip '{'
832+
833+
} else {
834+
835+
throw new Error( 'Expected \'{\' after switch(...) ' );
836+
837+
}
838+
839+
if ( this.getToken() && ( this.getToken().str === 'case' || this.getToken().str === 'default' ) ) {
840+
841+
switchStatement.case = this.parseSwitchCase();
842+
843+
} else {
844+
845+
this.parseBlock( switchStatement );
846+
847+
}
848+
849+
850+
};
851+
852+
const switchStatement = new Switch( parseSwitchExpression() );
853+
854+
parseSwitchBlock( switchStatement );
855+
856+
return switchStatement;
857+
858+
859+
}
860+
861+
parseSwitchCase() {
862+
863+
const parseCaseExpression = ( token ) => {
864+
865+
const caseTypeToken = token ? token : this.readToken(); // Skip 'case' or 'default
866+
867+
const caseTokens = this.readTokensUntil( ':' );
868+
869+
// No case condition on default
870+
if ( caseTypeToken.str === 'default' ) {
871+
872+
return null;
873+
874+
}
875+
876+
return this.parseExpressionFromTokens( caseTokens.slice( 0, - 1 ) );
877+
878+
};
879+
880+
let lastReadToken = null;
881+
882+
// No '{' so use different approach
883+
const parseCaseBlock = ( caseStatement ) => {
884+
885+
lastReadToken = this.parseBlock( caseStatement );
886+
887+
};
888+
889+
// Parse case condition
890+
const caseCondition = parseCaseExpression();
891+
const switchCase = new SwitchCase( caseCondition );
892+
893+
// Get case body
894+
parseCaseBlock( switchCase );
895+
896+
let currentCase = switchCase;
897+
898+
// If block ended with case, then continue chaining cases, otherwise, ended with '}' and no more case blocks to parse
899+
while ( lastReadToken.str === 'case' || lastReadToken.str === 'default' ) {
900+
901+
const previousCase = currentCase;
902+
903+
// case and default already skipped at block end, so need to pass it in as last read token
904+
currentCase = new SwitchCase( parseCaseExpression( lastReadToken ) );
905+
906+
previousCase.nextCase = currentCase;
907+
908+
parseCaseBlock( currentCase );
909+
910+
}
911+
912+
return switchCase;
913+
914+
}
915+
804916
parseIf() {
805917

806918
const parseIfExpression = () => {
@@ -841,10 +953,13 @@ class GLSLDecoder {
841953

842954
this.readToken(); // skip 'else'
843955

956+
// Assign the current if/else statement as the previous within the chain of conditionals
844957
const previous = current;
845958

959+
// If an 'else if' statement, parse the conditional within the if
846960
if ( this.getToken().str === 'if' ) {
847961

962+
// Current conditional now equal to next conditional in the chain
848963
current = new Conditional( parseIfExpression() );
849964

850965
} else {
@@ -853,8 +968,10 @@ class GLSLDecoder {
853968

854969
}
855970

971+
// n - 1 conditional's else statement assigned to new if/else statement
856972
previous.elseConditional = current;
857973

974+
// Parse conditional of latest if statement
858975
parseIfBlock( current );
859976

860977
}
@@ -885,9 +1002,10 @@ class GLSLDecoder {
8851002

8861003
if ( groupIndex < 0 ) {
8871004

888-
this.readToken(); // skip '}'
1005+
this.readToken(); // skip '}', ']', 'case', or other block ending tokens'
8891006

890-
break;
1007+
// Return skipped token
1008+
return token;
8911009

8921010
}
8931011

@@ -931,6 +1049,10 @@ class GLSLDecoder {
9311049

9321050
statement = this.parseFor();
9331051

1052+
} else if ( token.str === 'switch' ) {
1053+
1054+
statement = this.parseSwitch();
1055+
9341056
} else {
9351057

9361058
statement = this.parseExpression();

examples/jsm/transpiler/TSLEncoder.js

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ class TSLEncoder {
6666
this._currentProperties = {};
6767
this._lastStatement = null;
6868

69+
this.block = null;
70+
6971
}
7072

7173
addImport( name ) {
@@ -253,6 +255,17 @@ class TSLEncoder {
253255

254256
code = 'Discard()';
255257

258+
} else if ( node.isBreak ) {
259+
260+
this.addImport( 'Break' );
261+
262+
code = 'Break()';
263+
264+
} else if ( node.isContinue ) {
265+
266+
this.addImport( 'Continue' );
267+
code = 'Continue()';
268+
256269
} else if ( node.isAccessorElements ) {
257270

258271
code = this.emitExpression( node.object );
@@ -293,6 +306,10 @@ class TSLEncoder {
293306

294307
code = this.emitFor( node );
295308

309+
} else if ( node.isSwitch ) {
310+
311+
code = this.emitSwitch( node );
312+
296313
} else if ( node.isVariableDeclaration ) {
297314

298315
code = this.emitVariables( node );
@@ -378,6 +395,12 @@ class TSLEncoder {
378395

379396
for ( const statement of body ) {
380397

398+
if ( this.block && this.block.isSwitchCase ) {
399+
400+
if ( statement.isBreak ) continue; // skip break statements in switch cases
401+
402+
}
403+
381404
code += this.emitExtraLine( statement );
382405
code += this.tab + this.emitExpression( statement );
383406

@@ -507,6 +530,69 @@ ${ this.tab }} )`;
507530

508531
}
509532

533+
534+
emitSwitch( switchNode ) {
535+
536+
const discriminantString = this.emitExpression( switchNode.discriminant );
537+
538+
this.tab += '\t';
539+
540+
let switchString = `Switch( ${ discriminantString } )\n${ this.tab }`;
541+
542+
let caseNode = switchNode.case;
543+
544+
const previousBlock = this.block;
545+
546+
while ( caseNode !== null ) {
547+
548+
this.block = caseNode;
549+
550+
let caseBodyString;
551+
552+
if ( ! caseNode.isDefault ) {
553+
554+
const caseConditions = [ this.emitExpression( caseNode.caseCondition ) ];
555+
556+
while ( caseNode.body.length === 0 && caseNode.nextCase !== null && caseNode.nextCase.isDefault !== true ) {
557+
558+
caseNode = caseNode.nextCase;
559+
560+
caseConditions.push( this.emitExpression( caseNode.caseCondition ) );
561+
562+
}
563+
564+
caseBodyString = this.emitBody( caseNode.body );
565+
566+
switchString += `.Case( ${ caseConditions.join( ', ' ) }, `;
567+
568+
} else {
569+
570+
caseBodyString = this.emitBody( caseNode.body );
571+
572+
switchString += '.Default( ';
573+
574+
}
575+
576+
switchString += `() => {
577+
578+
${ caseBodyString }
579+
580+
${ this.tab }} )`;
581+
582+
caseNode = caseNode.nextCase;
583+
584+
}
585+
586+
this.block = previousBlock;
587+
588+
this.tab = this.tab.slice( 0, - 1 );
589+
590+
this.imports.add( 'Switch' );
591+
592+
return switchString;
593+
594+
}
595+
510596
emitFor( node ) {
511597

512598
const { initialization, condition, afterthought } = node;
@@ -754,7 +840,7 @@ ${ this.tab }} )`;
754840

755841
if ( statement.isReturn ) return '\n';
756842

757-
const isExpression = ( st ) => st.isFunctionDeclaration !== true && st.isFor !== true && st.isConditional !== true;
843+
const isExpression = ( st ) => st.isFunctionDeclaration !== true && st.isFor !== true && st.isConditional !== true && st.isSwitch !== true;
758844
const lastExp = isExpression( last );
759845
const currExp = isExpression( statement );
760846

0 commit comments

Comments
 (0)