/** * A small arithmetic expression language for an instrument file's derived * quantities: grid sizes, the timestep, mode counts, dispatch sizes. * * An instrument file must be able to say how its discretization follows * from its parameters (n1 from a stability bound, dt from the CFL * condition) without shipping executable JavaScript, so the host evaluates * these expressions itself over a plain map of numbers. The language is * deliberately tiny: numbers, named values, + - * / %, parentheses, unary * minus, and a fixed set of functions. Everything is IEEE double; integer * results come from floor/ceil/round where the author wants them. */ const FUNCTIONS: Record number> = { floor: Math.floor, ceil: Math.ceil, round: Math.round, sqrt: Math.sqrt, abs: Math.abs, exp: Math.exp, log: Math.log, min: Math.min, max: Math.max, pow: Math.pow, clamp: (v, lo, hi) => Math.min(hi, Math.max(lo, v)), }; const CONSTANTS: Record = { pi: Math.PI }; type Token = | { kind: 'num'; value: number } | { kind: 'name'; value: string } | { kind: 'op'; value: string }; function tokenize(src: string): Token[] { const out: Token[] = []; let i = 0; while (i < src.length) { const ch = src[i]; if (/\s/.test(ch)) { i++; } else if (/[0-9.]/.test(ch)) { const m = /^[0-9]*\.?[0-9]+(?:[eE][+-]?[0-9]+)?/.exec(src.slice(i))!; out.push({ kind: 'num', value: Number(m[0]) }); i += m[0].length; } else if (/[A-Za-z_]/.test(ch)) { const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(src.slice(i))!; out.push({ kind: 'name', value: m[0] }); i += m[0].length; } else if ('+-*/%(),'.includes(ch)) { out.push({ kind: 'op', value: ch }); i++; } else { throw new Error(`unexpected character '${ch}' in expression '${src}'`); } } return out; } /** Evaluate one expression against a map of named values. */ export function evalExpr(src: string, scope: Record): number { const toks = tokenize(src); let pos = 0; const peek = (): Token | undefined => toks[pos]; const isOp = (v: string): boolean => { const t = peek(); return t !== undefined && t.kind === 'op' && t.value === v; }; const expect = (v: string): void => { if (!isOp(v)) throw new Error(`expected '${v}' in expression '${src}'`); pos++; }; const primary = (): number => { const t = peek(); if (t === undefined) throw new Error(`expression '${src}' ends early`); if (t.kind === 'num') { pos++; return t.value; } if (t.kind === 'name') { pos++; if (isOp('(')) { const fn = FUNCTIONS[t.value]; if (!fn) throw new Error(`unknown function '${t.value}' in '${src}'`); pos++; const args: number[] = []; if (!isOp(')')) { args.push(addSub()); while (isOp(',')) { pos++; args.push(addSub()); } } expect(')'); return fn(...args); } if (t.value in scope) return scope[t.value]; if (t.value in CONSTANTS) return CONSTANTS[t.value]; throw new Error(`unknown name '${t.value}' in expression '${src}'`); } if (t.value === '(') { pos++; const v = addSub(); expect(')'); return v; } if (t.value === '-') { pos++; return -primary(); } throw new Error(`unexpected '${t.value}' in expression '${src}'`); }; const mulDiv = (): number => { let v = primary(); while (isOp('*') || isOp('/') || isOp('%')) { const op = (peek() as Token & { kind: 'op' }).value; pos++; const r = primary(); v = op === '*' ? v * r : op === '/' ? v / r : v % r; } return v; }; const addSub = (): number => { let v = mulDiv(); while (isOp('+') || isOp('-')) { const op = (peek() as Token & { kind: 'op' }).value; pos++; const r = mulDiv(); v = op === '+' ? v + r : v - r; } return v; }; const v = addSub(); if (pos !== toks.length) throw new Error(`trailing input in expression '${src}'`); if (!Number.isFinite(v)) throw new Error(`expression '${src}' is not finite`); return v; } /** Evaluate an ordered map of derived expressions; each may use the scope * and every earlier result. Returns the extended scope. */ export function evalDerived( derived: Record, scope: Record, ): Record { const out = { ...scope }; for (const [name, src] of Object.entries(derived)) { out[name] = evalExpr(src, out); } return out; }