1import { assert } from './util/util.js';
2
3// The state of the preprocessor is a stack of States.
4type StateStack = { allowsFollowingElse: boolean; state: State }[];
5const 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
16abstract 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
33class 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
56class ElseIf extends If {
57  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
70class 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
83class 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 *     const shader = pp`
95 * ${pp._if(expr)}
96 *   const x: ${type} = ${value};
97 * ${pp._elif(expr)}
98 * ${pp.__if(expr)}
99 * ...
100 * ${pp.__else}
101 * ...
102 * ${pp.__endif}
103 * ${pp._endif}`;
104 *
105 * @param strings - The array of constant string chunks of the template string.
106 * @param ...values - The array of interpolated ${} values within the template string.
107 */
108export function pp(
109  strings: TemplateStringsArray,
110  ...values: ReadonlyArray<Directive | string | number>
111): string {
112  let result = '';
113  const stateStack: StateStack = [{ allowsFollowingElse: false, state: State.Passing }];
114
115  for (let i = 0; i < values.length; ++i) {
116    const passing = stateStack[stateStack.length - 1].state === State.Passing;
117    if (passing) {
118      result += strings[i];
119    }
120
121    const value = values[i];
122    if (value instanceof Directive) {
123      value.applyTo(stateStack);
124    } else {
125      if (passing) {
126        result += value;
127      }
128    }
129  }
130  assert(stateStack.length === 1, 'Unterminated preprocessor condition at end of file');
131  result += strings[values.length];
132
133  return result;
134}
135pp._if = (predicate: boolean) => new If(1, predicate);
136pp._elif = (predicate: boolean) => new ElseIf(1, predicate);
137pp._else = new Else(1);
138pp._endif = new EndIf(1);
139pp.__if = (predicate: boolean) => new If(2, predicate);
140pp.__elif = (predicate: boolean) => new ElseIf(2, predicate);
141pp.__else = new Else(2);
142pp.__endif = new EndIf(2);
143pp.___if = (predicate: boolean) => new If(3, predicate);
144pp.___elif = (predicate: boolean) => new ElseIf(3, predicate);
145pp.___else = new Else(3);
146pp.___endif = new EndIf(3);
147// Add more if needed.
148