2 * A small arithmetic expression language for an instrument file's derived
3 * quantities: grid sizes, the timestep, mode counts, dispatch sizes.
4 *
5 * An instrument file must be able to say how its discretization follows
6 * from its parameters (n1 from a stability bound, dt from the CFL
7 * condition) without shipping executable JavaScript, so the host evaluates
8 * these expressions itself over a plain map of numbers. The language is
9 * deliberately tiny: numbers, named values, + - * / %, parentheses, unary
10 * minus, and a fixed set of functions. Everything is IEEE double; integer
11 * results come from floor/ceil/round where the author wants them.
12 */
14const FUNCTIONS: Record<string, (...a: number[]) => number> = {
15 floor: Math.floor,
16 ceil: Math.ceil,
17 round: Math.round,
18 sqrt: Math.sqrt,
19 abs: Math.abs,
20 exp: Math.exp,
21 log: Math.log,
22 min: Math.min,
23 max: Math.max,
24 pow: Math.pow,
25 clamp: (v, lo, hi) => Math.min(hi, Math.max(lo, v)),
26};
28const CONSTANTS: Record<string, number> = { pi: Math.PI };
30type Token =
31 | { kind: 'num'; value: number }
32 | { kind: 'name'; value: string }
33 | { kind: 'op'; value: string };
35function tokenize(src: string): Token[] {
36 const out: Token[] = [];
37 let i = 0;
38 while (i < src.length) {
39 const ch = src[i];
40 if (/\s/.test(ch)) {
41 i++;
42 } else if (/[0-9.]/.test(ch)) {
43 const m = /^[0-9]*\.?[0-9]+(?:[eE][+-]?[0-9]+)?/.exec(src.slice(i))!;
44 out.push({ kind: 'num', value: Number(m[0]) });
45 i += m[0].length;
46 } else if (/[A-Za-z_]/.test(ch)) {
47 const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(src.slice(i))!;
48 out.push({ kind: 'name', value: m[0] });
49 i += m[0].length;
50 } else if ('+-*/%(),'.includes(ch)) {
51 out.push({ kind: 'op', value: ch });
52 i++;
53 } else {
54 throw new Error(`unexpected character '${ch}' in expression '${src}'`);
55 }
56 }
57 return out;
58}
60/** Evaluate one expression against a map of named values. */
61export function evalExpr(src: string, scope: Record<string, number>): number {
62 const toks = tokenize(src);
63 let pos = 0;
64 const peek = (): Token | undefined => toks[pos];
65 const isOp = (v: string): boolean => {
66 const t = peek();
67 return t !== undefined && t.kind === 'op' && t.value === v;
68 };
69 const expect = (v: string): void => {
70 if (!isOp(v)) throw new Error(`expected '${v}' in expression '${src}'`);
71 pos++;
72 };
74 const primary = (): number => {
75 const t = peek();
76 if (t === undefined) throw new Error(`expression '${src}' ends early`);
77 if (t.kind === 'num') {
78 pos++;
79 return t.value;
80 }
81 if (t.kind === 'name') {
82 pos++;
83 if (isOp('(')) {
84 const fn = FUNCTIONS[t.value];
85 if (!fn) throw new Error(`unknown function '${t.value}' in '${src}'`);
86 pos++;
87 const args: number[] = [];
88 if (!isOp(')')) {
89 args.push(addSub());
90 while (isOp(',')) {
91 pos++;
92 args.push(addSub());
93 }
94 }
95 expect(')');
96 return fn(...args);
97 }
98 if (t.value in scope) return scope[t.value];
99 if (t.value in CONSTANTS) return CONSTANTS[t.value];
100 throw new Error(`unknown name '${t.value}' in expression '${src}'`);
101 }
102 if (t.value === '(') {
103 pos++;
104 const v = addSub();
105 expect(')');
106 return v;
107 }
108 if (t.value === '-') {
109 pos++;
110 return -primary();
111 }
112 throw new Error(`unexpected '${t.value}' in expression '${src}'`);
113 };
115 const mulDiv = (): number => {
116 let v = primary();
117 while (isOp('*') || isOp('/') || isOp('%')) {
118 const op = (peek() as Token & { kind: 'op' }).value;
119 pos++;
120 const r = primary();
121 v = op === '*' ? v * r : op === '/' ? v / r : v % r;
122 }
123 return v;
124 };
126 const addSub = (): number => {
127 let v = mulDiv();
128 while (isOp('+') || isOp('-')) {
129 const op = (peek() as Token & { kind: 'op' }).value;
130 pos++;
131 const r = mulDiv();
132 v = op === '+' ? v + r : v - r;
133 }
134 return v;
135 };
137 const v = addSub();
138 if (pos !== toks.length) throw new Error(`trailing input in expression '${src}'`);
139 if (!Number.isFinite(v)) throw new Error(`expression '${src}' is not finite`);
140 return v;
141}
143/** Evaluate an ordered map of derived expressions; each may use the scope
144 * and every earlier result. Returns the extended scope. */
145export function evalDerived(
146 derived: Record<string, string>,
147 scope: Record<string, number>,
148): Record<string, number> {
149 const out = { ...scope };
150 for (const [name, src] of Object.entries(derived)) {
151 out[name] = evalExpr(src, out);
152 }
153 return out;
154}