preprocessor.ts (4119B)
1 import { assert } from './util.js'; 2 3 // The state of the preprocessor is a stack of States. 4 type StateStack = { allowsFollowingElse: boolean; state: State }[]; 5 const enum State { 6 Seeking, // Still looking for a passing condition 7 Passing, // Currently inside a passing condition (the root is always in this state) 8 Skipping, // Have already seen a passing condition; now skipping the rest 9 } 10 11 // The transitions in the state space are the following preprocessor directives: 12 // - Sibling elif 13 // - Sibling else 14 // - Sibling endif 15 // - Child if 16 abstract class Directive { 17 private readonly depth: number; 18 19 constructor(depth: number) { 20 this.depth = depth; 21 } 22 23 protected checkDepth(stack: StateStack): void { 24 assert( 25 stack.length === this.depth, 26 `Number of "$"s must match nesting depth, currently ${stack.length} (e.g. $if $$if $$endif $endif)` 27 ); 28 } 29 30 abstract applyTo(stack: StateStack): void; 31 } 32 33 class If extends Directive { 34 private readonly predicate: boolean; 35 36 constructor(depth: number, predicate: boolean) { 37 super(depth); 38 this.predicate = predicate; 39 } 40 41 applyTo(stack: StateStack) { 42 this.checkDepth(stack); 43 const parentState = stack[stack.length - 1].state; 44 stack.push({ 45 allowsFollowingElse: true, 46 state: 47 parentState !== State.Passing 48 ? State.Skipping 49 : this.predicate 50 ? State.Passing 51 : State.Seeking, 52 }); 53 } 54 } 55 56 class ElseIf extends If { 57 override applyTo(stack: StateStack) { 58 assert(stack.length >= 1); 59 const { allowsFollowingElse, state: siblingState } = stack.pop()!; 60 this.checkDepth(stack); 61 assert(allowsFollowingElse, 'pp.elif after pp.else'); 62 if (siblingState !== State.Seeking) { 63 stack.push({ allowsFollowingElse: true, state: State.Skipping }); 64 } else { 65 super.applyTo(stack); 66 } 67 } 68 } 69 70 class Else extends Directive { 71 applyTo(stack: StateStack) { 72 assert(stack.length >= 1); 73 const { allowsFollowingElse, state: siblingState } = stack.pop()!; 74 this.checkDepth(stack); 75 assert(allowsFollowingElse, 'pp.else after pp.else'); 76 stack.push({ 77 allowsFollowingElse: false, 78 state: siblingState === State.Seeking ? State.Passing : State.Skipping, 79 }); 80 } 81 } 82 83 class EndIf extends Directive { 84 applyTo(stack: StateStack) { 85 stack.pop(); 86 this.checkDepth(stack); 87 } 88 } 89 90 /** 91 * A simple template-based, non-line-based preprocessor implementing if/elif/else/endif. 92 * 93 * @example 94 * ``` 95 * const shader = pp` 96 * ${pp._if(expr)} 97 * const x: ${type} = ${value}; 98 * ${pp._elif(expr)} 99 * ${pp.__if(expr)} 100 * ... 101 * ${pp.__else} 102 * ... 103 * ${pp.__endif} 104 * ${pp._endif}`; 105 * ``` 106 * 107 * @param strings - The array of constant string chunks of the template string. 108 * @param ...values - The array of interpolated `${}` values within the template string. 109 */ 110 export function pp( 111 strings: TemplateStringsArray, 112 ...values: ReadonlyArray<Directive | string | number> 113 ): string { 114 let result = ''; 115 const stateStack: StateStack = [{ allowsFollowingElse: false, state: State.Passing }]; 116 117 for (let i = 0; i < values.length; ++i) { 118 const passing = stateStack[stateStack.length - 1].state === State.Passing; 119 if (passing) { 120 result += strings[i]; 121 } 122 123 const value = values[i]; 124 if (value instanceof Directive) { 125 value.applyTo(stateStack); 126 } else { 127 if (passing) { 128 result += value; 129 } 130 } 131 } 132 assert(stateStack.length === 1, 'Unterminated preprocessor condition at end of file'); 133 result += strings[values.length]; 134 135 return result; 136 } 137 pp._if = (predicate: boolean) => new If(1, predicate); 138 pp._elif = (predicate: boolean) => new ElseIf(1, predicate); 139 pp._else = new Else(1); 140 pp._endif = new EndIf(1); 141 pp.__if = (predicate: boolean) => new If(2, predicate); 142 pp.__elif = (predicate: boolean) => new ElseIf(2, predicate); 143 pp.__else = new Else(2); 144 pp.__endif = new EndIf(2); 145 pp.___if = (predicate: boolean) => new If(3, predicate); 146 pp.___elif = (predicate: boolean) => new ElseIf(3, predicate); 147 pp.___else = new Else(3); 148 pp.___endif = new EndIf(3); 149 // Add more if needed.