Skip to content

Commit a0b1e54

Browse files
authored
Merge pull request #1376 from ccnmtl/js-quote-unquote-compiler
Make a "JIT JS compiler" for math.js expressions.
2 parents 0e319ec + f8f4d15 commit a0b1e54

File tree

4 files changed

+191
-43
lines changed

4 files changed

+191
-43
lines changed

media/src/objects/Field.svelte

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<script>
66
import { onMount, onDestroy, untrack } from 'svelte';
77
import * as THREE from 'three';
8-
import { create, all, e } from 'mathjs';
8+
import { create, all, e, xgcd } from 'mathjs';
99
1010
import M from '../M.svelte';
1111
import ObjHeader from './ObjHeader.svelte';
@@ -17,6 +17,7 @@
1717
import InputChecker from '../form-components/InputChecker.svelte';
1818
import PlayButtons from '../form-components/PlayButtons.svelte';
1919
import FlowArrowMesh from './FlowArrowMesh';
20+
import { mathToJSFunction } from './mathutils';
2021
2122
const config = {};
2223
const math = create(all, config);
@@ -314,20 +315,20 @@
314315
render();
315316
};
316317
317-
let fieldF;
318-
319-
const updateField = function () {
318+
let fieldF = $derived.by(() => {
320319
const { p, q, r } = params;
321320
322-
const [P, Q, R] = [p, q, r].map((x) => math.parse(x).compile());
323-
324-
fieldF = (x, y, z, vec) => {
325-
const args = { x, y, z };
326-
vec.set(P.evaluate(args), Q.evaluate(args), R.evaluate(args));
321+
const [P, Q, R] = [p, q, r].map((x) =>
322+
mathToJSFunction(x, ['x', 'y', 'z']),
323+
);
327324
325+
return (x, y, z, vec) => {
326+
vec.set(P(x, y, z), Q(x, y, z), R(x, y, z));
328327
return vec;
329328
};
330-
};
329+
});
330+
331+
const updateField = function () {};
331332
332333
let maxLength = 2;
333334
flowArrows.name = uuid;

media/src/objects/Function.svelte

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
1111
import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js';
1212
13-
import { create, all, neutronMassDependencies } from 'mathjs';
13+
import { create, all } from 'mathjs';
1414
1515
import M from '../M.svelte';
1616
import ObjHeader from './ObjHeader.svelte';
@@ -37,6 +37,9 @@
3737
blockGeometry,
3838
checksum,
3939
} from '../utils.js';
40+
41+
import { mathToJSFunction } from './mathutils';
42+
4043
import { flashDance } from '../sceneUtils';
4144
4245
let {
@@ -209,10 +212,12 @@
209212
scene.add(point);
210213
211214
// Compile main function
212-
let func = $derived.by(() => {
213-
const z = math.parse(params.z).compile();
214-
return (x, y, t) => z.evaluate({ x, y, t });
215-
});
215+
// let func = $derived.by(() => {
216+
// const z = math.parse(params.z).compile();
217+
// return (x, y, t) => z.evaluate({ x, y, t });
218+
// });
219+
220+
let func = $derived(mathToJSFunction(params.z, ['x', 'y', 't']));
216221
217222
const tangentVectors = function () {
218223
// const arrowParams = {
@@ -721,8 +726,8 @@
721726
ymax: D,
722727
level: lev,
723728
zLevel: 0,
724-
nX: data.nX,
725-
nY: data.nX,
729+
nX: 200,
730+
nY: 200,
726731
});
727732
728733
if (points.length > 1) {

media/src/objects/Surface.svelte

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
checksum,
3535
ParametricGeometry,
3636
} from '../utils.js';
37+
38+
import { mathToJSFunction } from './mathutils';
39+
3740
import { flashDance } from '../sceneUtils';
3841
import InputChecker from '../form-components/InputChecker.svelte';
3942
import ColorBar from '../settings/ColorBar.svelte';
@@ -180,33 +183,34 @@
180183
}
181184
});
182185
183-
let xyz;
184-
let abcd;
185-
186-
// compile (in the math.js sense) each expression once they change
187-
// and thus have been cleared
188-
$effect(() => {
189-
// console.log('parm compile');
186+
let xyz = $derived.by(() => {
190187
const [x, y, z] = [params.x, params.y, params.z].map((f) =>
191-
math.parse(f).compile(),
188+
mathToJSFunction(f, ['u', 'v', 't']),
192189
);
193-
xyz = (u, v, vec, t = 0) =>
194-
vec.set(
195-
x.evaluate({ u, v, t }),
196-
y.evaluate({ u, v, t }),
197-
z.evaluate({ u, v, t }),
198-
);
199-
// });
200-
// $effect(() => {
190+
return (u, v, vec, t = 0) =>
191+
vec.set(x(u, v, t), y(u, v, t), z(u, v, t));
192+
});
193+
let abcd = $derived.by(() => {
201194
const [a, b] = [params.a, params.b].map((f) => math.evaluate(f));
202-
const [c, d] = [params.c, params.d].map((f) => math.parse(f).compile());
195+
const [c, d] = [params.c, params.d].map((f) =>
196+
mathToJSFunction(f, ['u']),
197+
);
203198
204-
// take uv on unit square to actual uv coords for parametricgeom
205-
abcd = (u, v) => [
199+
// take uv on unit square to actual uv coords for ParametricGeom
200+
return (u, v) => [
206201
a + u * (b - a),
207-
c.evaluate({ u: a + u * (b - a) }) * (1 - v) +
208-
d.evaluate({ u: a + u * (b - a) }) * v,
202+
c(a + u * (b - a)) * (1 - v) + d(a + u * (b - a)) * v,
209203
];
204+
});
205+
206+
// compile (in the math.js sense) each expression once they change
207+
// and thus have been cleared
208+
$effect(() => {
209+
// console.log('parm compile');
210+
211+
// React on changes here.
212+
const { a, b, c, d, x, y, z } = params;
213+
210214
untrack(updateSurface);
211215
});
212216
@@ -304,15 +308,15 @@
304308
});
305309
306310
const updateSurface = function () {
307-
const { t0, t1 } = params;
308-
const time = t0
309-
? math.evaluate(t0) + tau * (math.evaluate(t1) - math.evaluate(t0))
310-
: 0;
311+
// const { t0, t1 } = params;
312+
// const time = t0
313+
// ? math.evaluate(t0) + tau * (math.evaluate(t1) - math.evaluate(t0))
314+
// : 0;
311315
312316
const geometry = new ParametricGeometry(
313317
(u, v, vec) => {
314318
const [U, V] = abcd(u, v);
315-
xyz(U, V, vec, time);
319+
xyz(U, V, vec, tVal);
316320
},
317321
nX || 30,
318322
nX || 30,

media/src/objects/mathutils.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
parse,
3+
simplifyConstant,
4+
OperatorNode,
5+
FunctionNode,
6+
SymbolNode,
7+
ConditionalNode,
8+
} from 'mathjs';
9+
10+
const allowedMathFns = new Set([
11+
'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2',
12+
'sqrt', 'pow', 'exp', 'log', 'abs',
13+
'min', 'max', 'round', 'floor', 'ceil', 'sign', 'hypot',
14+
]);
15+
16+
const disallowedSymbols = new Set([
17+
'__proto__', 'constructor', 'prototype',
18+
'Function', 'window', 'document', 'globalThis',
19+
]);
20+
21+
const constantMap = {
22+
pi: 'Math.PI',
23+
e: 'Math.E',
24+
LOG2E: 'Math.LOG2E',
25+
LOG10E: 'Math.LOG10E',
26+
LN2: 'Math.LN2',
27+
LN10: 'Math.LN10',
28+
SQRT2: 'Math.SQRT2',
29+
SQRT1_2: 'Math.SQRT1_2',
30+
};
31+
32+
/**
33+
* Checks if a math.js AST contains a ConditionalNode
34+
* @param {*} node
35+
* @returns boolean
36+
*/
37+
function containsConditional(node) {
38+
let found = false;
39+
node.traverse((child) => {
40+
if (child.type === 'ConditionalNode') {
41+
found = true;
42+
}
43+
});
44+
return found;
45+
}
46+
47+
48+
// 🔁 Transform e^x → exp(x)
49+
function rewriteEToExp(ast) {
50+
return ast.transform((node) => {
51+
if (
52+
node.type === 'OperatorNode' &&
53+
node.fn === 'pow' &&
54+
node.args.length === 2 &&
55+
node.args[0].type === 'SymbolNode' &&
56+
node.args[0].name === 'e'
57+
) {
58+
return new FunctionNode('exp', [node.args[1]]);
59+
}
60+
return node;
61+
});
62+
}
63+
64+
function mathToJSFunction(expression, variableNames = null) {
65+
const rawAST = parse(expression);
66+
const rewrittenAST = rewriteEToExp(rawAST);
67+
const ast = containsConditional(rewrittenAST) ? rewrittenAST : simplifyConstant(rewrittenAST);
68+
69+
// Infer variables if not given
70+
const inferredVars = new Set();
71+
ast.traverse((node) => {
72+
if (node.isSymbolNode) {
73+
const name = node.name;
74+
if (disallowedSymbols.has(name)) {
75+
throw new Error(`Disallowed symbol: ${name}`);
76+
}
77+
if (!allowedMathFns.has(name) && !constantMap[name] && !/^[0-9.]+$/.test(name)) {
78+
inferredVars.add(name);
79+
}
80+
}
81+
});
82+
83+
const vars = variableNames ?? Array.from(inferredVars);
84+
85+
// Compile AST to JS expression string
86+
const jsExpr = ast.toString({
87+
handler: (node, options) => {
88+
switch (node.type) {
89+
case 'OperatorNode':
90+
if (node.fn === 'pow') {
91+
return `Math.pow(${node.args[0].toString(options)}, ${node.args[1].toString(options)})`;
92+
}
93+
if (node.args.length === 2) {
94+
return `(${node.args[0].toString(options)} ${node.op} ${node.args[1].toString(options)})`;
95+
} else if (node.args.length === 1) {
96+
return `(${node.op}${node.args[0].toString(options)})`;
97+
}
98+
break;
99+
100+
case 'FunctionNode':
101+
const fnName = node.fn.name;
102+
if (fnName === 'mod') {
103+
const [a, b] = node.args;
104+
return `(((${a.toString(options)} % ${b.toString(options)}) + ${b.toString(options)}) % ${b.toString(options)})`;
105+
}
106+
107+
if (!allowedMathFns.has(fnName)) {
108+
throw new Error(`Unsupported function: ${fnName}`);
109+
}
110+
const jsFn = fnName === 'atan2' ? 'Math.atan2' : `Math.${fnName}`;
111+
return `${jsFn}(${node.args.map(arg => arg.toString(options)).join(', ')})`;
112+
113+
case 'ConstantNode':
114+
return `${node.value}`;
115+
116+
case 'SymbolNode':
117+
return constantMap[node.name] || node.name;
118+
119+
case 'ParenthesisNode':
120+
return `(${node.content.toString(options)})`;
121+
122+
case 'ConditionalNode':
123+
return `(${node.condition.toString(options)} ? ${node.trueExpr.toString(options)} : ${node.falseExpr.toString(options)})`;
124+
125+
default:
126+
throw new Error(`Unsupported node type: ${node.type}`);
127+
}
128+
},
129+
parenthesis: 'keep',
130+
implicit: 'show',
131+
});
132+
133+
const argList = vars.join(', ');
134+
return new Function(argList, `return ${jsExpr};`);
135+
}
136+
137+
138+
export { mathToJSFunction };

0 commit comments

Comments
 (0)