exprforge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/ast.js +142 -0
- package/build.js +35 -0
- package/emitters/base.js +113 -0
- package/emitters/c.js +64 -0
- package/emitters/csharp.js +86 -0
- package/emitters/go.js +95 -0
- package/emitters/java.js +76 -0
- package/emitters/js.js +49 -0
- package/emitters/lua.js +103 -0
- package/emitters/python.js +85 -0
- package/emitters/qb64.js +127 -0
- package/emitters/registry.js +15 -0
- package/emitters/rust.js +90 -0
- package/emitters/typescript.js +63 -0
- package/index.js +44 -0
- package/package.json +44 -0
- package/samples/catmull-rom.js +26 -0
- package/samples/fibonacci.js +24 -0
- package/samples/kitchen-sink.js +56 -0
- package/samples/spline-frame.js +184 -0
- package/util.js +15 -0
package/emitters/rust.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// exprforge/emitters/rust.js
|
|
2
|
+
//
|
|
3
|
+
// Rust's f64 math is method-call syntax (x.sqrt()), not free functions
|
|
4
|
+
// (sqrt(x)) like every other target here. The base Emitter's `calls` table
|
|
5
|
+
// just holds string templates, so this needs no special-casing in base.js —
|
|
6
|
+
// it's the reason `calls` was designed as arbitrary templates instead of a
|
|
7
|
+
// plain "language function name" lookup.
|
|
8
|
+
const Emitter = require("./base.js");
|
|
9
|
+
|
|
10
|
+
function method0(name) {
|
|
11
|
+
return ([x]) => `(${x}).${name}()`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function method1(name) {
|
|
15
|
+
return ([recv, arg]) => `(${recv}).${name}(${arg})`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const emitter = new Emitter({
|
|
19
|
+
ext: "rs",
|
|
20
|
+
// Explicit f64 suffix: a bare literal like `5.0` is only usable as a
|
|
21
|
+
// method-call receiver (`5.0.sqrt()`) once its type is unambiguous, and
|
|
22
|
+
// rustc won't always infer it from context (E0689).
|
|
23
|
+
formatNumber: (v) => (Number.isInteger(v) ? `${v}.0f64` : `${v}f64`),
|
|
24
|
+
calls: {
|
|
25
|
+
sqrt: method0("sqrt"), abs: method0("abs"), sin: method0("sin"), cos: method0("cos"),
|
|
26
|
+
tan: method0("tan"), asin: method0("asin"), acos: method0("acos"), atan: method0("atan"),
|
|
27
|
+
ln: method0("ln"), log2: method0("log2"), log10: method0("log10"), exp: method0("exp"),
|
|
28
|
+
floor: method0("floor"), ceil: method0("ceil"), round: method0("round"), trunc: method0("trunc"),
|
|
29
|
+
log: method0("ln"), // Math.log is natural log; Rust spells it ln()
|
|
30
|
+
// NOT .signum(): Rust's docs specify 1.0 at positive zero (it only
|
|
31
|
+
// reads the sign bit), unlike JS's Math.sign(0) === 0 (and C's/
|
|
32
|
+
// Java's sign, which both special-case zero). Found by the
|
|
33
|
+
// kitchen-sink conformance test at exactly x - y == 0.
|
|
34
|
+
sign: ([x]) => `(if ${x} > 0.0 { 1.0f64 } else if ${x} < 0.0 { -1.0f64 } else { 0.0f64 })`,
|
|
35
|
+
pow: method1("powf"), atan2: method1("atan2"), min: method1("min"),
|
|
36
|
+
max: method1("max"), hypot: method1("hypot"),
|
|
37
|
+
},
|
|
38
|
+
// Rust has no C-style ?: ternary; `if` is itself an expression instead
|
|
39
|
+
// (`if cond { a } else { b }`), and it's just as short-circuiting.
|
|
40
|
+
emitSelect: function (condNode, thenStr, elseStr) {
|
|
41
|
+
const L = this.emitExpr(condNode.left);
|
|
42
|
+
const R = this.emitExpr(condNode.right);
|
|
43
|
+
return `(if ${L} ${condNode.op} ${R} { ${thenStr} } else { ${elseStr} })`;
|
|
44
|
+
},
|
|
45
|
+
formatFunction: (fn, body, letBindings = []) => {
|
|
46
|
+
const params = fn.params.map((p) => `${p}: f64`).join(", ");
|
|
47
|
+
const lets = letBindings.map(({ name, valueStr }) => ` let ${name}: f64 = ${valueStr};`).join("\n");
|
|
48
|
+
const letsBlock = lets ? lets + "\n" : "";
|
|
49
|
+
// Param/function names come from the AST author (may not be snake_case,
|
|
50
|
+
// e.g. "P0"), every bin node keeps explicit parens by design (see
|
|
51
|
+
// header), and a shared let-chain can bind more names than one
|
|
52
|
+
// function's body reads (e.g. ux/uy/uz computed for three sibling
|
|
53
|
+
// single-component functions) — all trip default rustc lints
|
|
54
|
+
// without being bugs, so silence them rather than let a
|
|
55
|
+
// `-D warnings` build choke on them.
|
|
56
|
+
return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
57
|
+
`#[allow(non_snake_case, unused_parens, unused_variables)]\n` +
|
|
58
|
+
`pub fn ${fn.name}(${params}) -> f64 {\n` +
|
|
59
|
+
letsBlock +
|
|
60
|
+
` ${body}\n` +
|
|
61
|
+
`}\n`;
|
|
62
|
+
},
|
|
63
|
+
// Multiple named outputs from one call: Rust has no native multi-return
|
|
64
|
+
// with names (tuples are positional and easy to mix up for 6 fields),
|
|
65
|
+
// so this emits a small struct alongside the function and constructs it
|
|
66
|
+
// directly — the idiomatic Rust shape for "several named values out."
|
|
67
|
+
formatSuite: (fn, outputStrs, letBindings = []) => {
|
|
68
|
+
const params = fn.params.map((p) => `${p}: f64`).join(", ");
|
|
69
|
+
const lets = letBindings.map(({ name, valueStr }) => ` let ${name}: f64 = ${valueStr};`).join("\n");
|
|
70
|
+
const letsBlock = lets ? lets + "\n" : "";
|
|
71
|
+
const outputNames = Object.keys(outputStrs);
|
|
72
|
+
const structName = `${capitalize(fn.name)}Result`;
|
|
73
|
+
const structFields = outputNames.map((n) => ` pub ${n}: f64,`).join("\n");
|
|
74
|
+
const initFields = outputNames.map((n) => `${n}: ${outputStrs[n]}`).join(", ");
|
|
75
|
+
return `// AUTO-GENERATED by ExprForge -- do not hand-edit.\n` +
|
|
76
|
+
`#[allow(non_snake_case)]\n` +
|
|
77
|
+
`pub struct ${structName} {\n${structFields}\n}\n\n` +
|
|
78
|
+
`#[allow(non_snake_case, unused_parens, unused_variables)]\n` +
|
|
79
|
+
`pub fn ${fn.name}(${params}) -> ${structName} {\n` +
|
|
80
|
+
letsBlock +
|
|
81
|
+
` ${structName} { ${initFields} }\n` +
|
|
82
|
+
`}\n`;
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
function capitalize(s) {
|
|
87
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = emitter;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// exprforge/emitters/typescript.js
|
|
2
|
+
//
|
|
3
|
+
// Almost identical to js.js at the expression level -- same Math.* calls,
|
|
4
|
+
// same number literal syntax, native ternary for select() (no override
|
|
5
|
+
// needed) -- since TS numbers are JS numbers. What's different is purely
|
|
6
|
+
// surface: typed signatures, ESM `export` instead of CommonJS
|
|
7
|
+
// `module.exports` (this targets a frontend build tool, not Node), and a
|
|
8
|
+
// named `interface` for multi-output suites instead of an untyped object.
|
|
9
|
+
const Emitter = require("./base.js");
|
|
10
|
+
|
|
11
|
+
function fn1(name) {
|
|
12
|
+
return ([x]) => `Math.${name}(${x})`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fn2(name) {
|
|
16
|
+
return ([a, b]) => `Math.${name}(${a}, ${b})`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const emitter = new Emitter({
|
|
20
|
+
ext: "ts",
|
|
21
|
+
formatNumber: (v) => String(v),
|
|
22
|
+
calls: {
|
|
23
|
+
sqrt: fn1("sqrt"), abs: fn1("abs"), sin: fn1("sin"), cos: fn1("cos"), tan: fn1("tan"),
|
|
24
|
+
asin: fn1("asin"), acos: fn1("acos"), atan: fn1("atan"), log: fn1("log"),
|
|
25
|
+
log2: fn1("log2"), log10: fn1("log10"), exp: fn1("exp"), floor: fn1("floor"),
|
|
26
|
+
ceil: fn1("ceil"), round: fn1("round"), trunc: fn1("trunc"), sign: fn1("sign"),
|
|
27
|
+
pow: fn2("pow"), atan2: fn2("atan2"), min: fn2("min"), max: fn2("max"), hypot: fn2("hypot"),
|
|
28
|
+
},
|
|
29
|
+
formatFunction: (fn, body, letBindings = []) => {
|
|
30
|
+
const params = fn.params.map((p) => `${p}: number`).join(", ");
|
|
31
|
+
const lets = letBindings.map(({ name, valueStr }) => ` const ${name} = ${valueStr};`).join("\n");
|
|
32
|
+
const letsBlock = lets ? lets + "\n" : "";
|
|
33
|
+
return `// AUTO-GENERATED by ExprForge — do not hand-edit.\n` +
|
|
34
|
+
`export function ${fn.name}(${params}): number {\n` +
|
|
35
|
+
letsBlock +
|
|
36
|
+
` return ${body};\n` +
|
|
37
|
+
`}\n`;
|
|
38
|
+
},
|
|
39
|
+
// Multiple named outputs from one call: a named interface, returned as
|
|
40
|
+
// an object literal — TS's structural typing means this is checked at
|
|
41
|
+
// every call site, unlike JS's untyped equivalent.
|
|
42
|
+
formatSuite: (fn, outputStrs, letBindings = []) => {
|
|
43
|
+
const params = fn.params.map((p) => `${p}: number`).join(", ");
|
|
44
|
+
const lets = letBindings.map(({ name, valueStr }) => ` const ${name} = ${valueStr};`).join("\n");
|
|
45
|
+
const letsBlock = lets ? lets + "\n" : "";
|
|
46
|
+
const outputNames = Object.keys(outputStrs);
|
|
47
|
+
const interfaceName = `${capitalize(fn.name)}Result`;
|
|
48
|
+
const interfaceFields = outputNames.map((n) => ` ${n}: number;`).join("\n");
|
|
49
|
+
const objectFields = outputNames.map((n) => ` ${n}: ${outputStrs[n]},`).join("\n");
|
|
50
|
+
return `// AUTO-GENERATED by ExprForge — do not hand-edit.\n` +
|
|
51
|
+
`export interface ${interfaceName} {\n${interfaceFields}\n}\n\n` +
|
|
52
|
+
`export function ${fn.name}(${params}): ${interfaceName} {\n` +
|
|
53
|
+
letsBlock +
|
|
54
|
+
` return {\n${objectFields}\n };\n` +
|
|
55
|
+
`}\n`;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function capitalize(s) {
|
|
60
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = emitter;
|
package/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// exprforge/index.js
|
|
2
|
+
const { num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets } = require("./ast.js");
|
|
3
|
+
const { forComponents } = require("./util.js");
|
|
4
|
+
const emitters = require("./emitters/registry.js");
|
|
5
|
+
const { catmullRomAst } = require("./samples/catmull-rom.js");
|
|
6
|
+
const { fibonacciAst } = require("./samples/fibonacci.js");
|
|
7
|
+
const { splineFrameAsts } = require("./samples/spline-frame.js");
|
|
8
|
+
const { kitchenSinkAst } = require("./samples/kitchen-sink.js");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run every registered emitter against one AST function definition.
|
|
12
|
+
* Returns { [lang]: { ext, source } }.
|
|
13
|
+
*/
|
|
14
|
+
function emitAll(fn) {
|
|
15
|
+
const result = {};
|
|
16
|
+
for (const [lang, emitter] of Object.entries(emitters)) {
|
|
17
|
+
result[lang] = { ext: emitter.ext, source: emitter.emitFunction(fn) };
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
// AST builders — use these to define your own formulas.
|
|
24
|
+
num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets,
|
|
25
|
+
// Authoring convenience — not an AST primitive, see util.js.
|
|
26
|
+
forComponents,
|
|
27
|
+
// Built-in example formulas — see samples/ for the source.
|
|
28
|
+
catmullRomAst,
|
|
29
|
+
fibonacciAst,
|
|
30
|
+
splineFrameAsts,
|
|
31
|
+
// Not a worked example -- a conformance-test fixture that calls every
|
|
32
|
+
// supported Math function once. See samples/kitchen-sink.js.
|
|
33
|
+
kitchenSinkAst,
|
|
34
|
+
samples: {
|
|
35
|
+
catmullRom: catmullRomAst,
|
|
36
|
+
fibonacci: fibonacciAst,
|
|
37
|
+
splineFrame: splineFrameAsts,
|
|
38
|
+
kitchenSink: kitchenSinkAst,
|
|
39
|
+
},
|
|
40
|
+
// Per-language emitter instances, keyed by name (js, qb64, c, java, go, rust).
|
|
41
|
+
emitters,
|
|
42
|
+
// Convenience: run every emitter at once.
|
|
43
|
+
emitAll,
|
|
44
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "exprforge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Author a math expression once as an AST, emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, and Rust.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.js",
|
|
9
|
+
"ast.js",
|
|
10
|
+
"util.js",
|
|
11
|
+
"build.js",
|
|
12
|
+
"emitters/",
|
|
13
|
+
"samples/"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "node build.js",
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"prepublishOnly": "npm test"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"codegen",
|
|
22
|
+
"ast",
|
|
23
|
+
"math",
|
|
24
|
+
"cross-language",
|
|
25
|
+
"transpiler",
|
|
26
|
+
"qb64"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"author": {
|
|
30
|
+
"name": "Don Smith",
|
|
31
|
+
"url": "https://github.com/theraccoonbear"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/theraccoonbear/exprforge.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/theraccoonbear/exprforge#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/theraccoonbear/exprforge/issues"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// exprforge/samples/catmull-rom.js
|
|
2
|
+
// Uniform Catmull-Rom spline interpolation, one scalar component at a time
|
|
3
|
+
// (call once per x/y/z with P0..P3 = that axis' control values).
|
|
4
|
+
const { num, v, mul, add, sub } = require("../ast.js");
|
|
5
|
+
|
|
6
|
+
// P(t) = 0.5 * ( 2*P1 + (-P0+P2)*t + (2*P0-5*P1+4*P2-P3)*t^2 + (-P0+3*P1-3*P2+P3)*t^3 )
|
|
7
|
+
const P0 = v("P0");
|
|
8
|
+
const P1 = v("P1");
|
|
9
|
+
const P2 = v("P2");
|
|
10
|
+
const P3 = v("P3");
|
|
11
|
+
const t = v("t");
|
|
12
|
+
const t2 = mul(t, t);
|
|
13
|
+
const t3 = mul(t, t, t);
|
|
14
|
+
|
|
15
|
+
const term0 = mul(num(2), P1);
|
|
16
|
+
const term1 = mul(sub(P2, P0), t);
|
|
17
|
+
const term2 = mul(add(mul(num(2), P0), mul(num(-5), P1), mul(num(4), P2), mul(num(-1), P3)), t2);
|
|
18
|
+
const term3 = mul(add(mul(num(-1), P0), mul(num(3), P1), mul(num(-3), P2), P3), t3);
|
|
19
|
+
|
|
20
|
+
const catmullRomAst = {
|
|
21
|
+
name: "catmullRom1D",
|
|
22
|
+
params: ["P0", "P1", "P2", "P3", "t"],
|
|
23
|
+
body: mul(num(0.5), add(term0, term1, term2, term3)),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
module.exports = { catmullRomAst };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// exprforge/samples/fibonacci.js
|
|
2
|
+
// nth Fibonacci number via the closed form (Binet's formula), not recursion
|
|
3
|
+
// or a loop — exprforge has no control flow (see README), so this is the
|
|
4
|
+
// shape a "fibonacci" example has to take here:
|
|
5
|
+
//
|
|
6
|
+
// F(n) = (phi^n - psi^n) / sqrt(5)
|
|
7
|
+
// phi = (1 + sqrt(5)) / 2 (golden ratio)
|
|
8
|
+
// psi = (1 - sqrt(5)) / 2
|
|
9
|
+
//
|
|
10
|
+
// float64 throughout, so exact only up to about n=70 before precision drifts.
|
|
11
|
+
const { num, v, call, sub, add, div } = require("../ast.js");
|
|
12
|
+
|
|
13
|
+
const sqrt5 = call("sqrt", num(5));
|
|
14
|
+
const phi = div(add(num(1), sqrt5), num(2));
|
|
15
|
+
const psi = div(sub(num(1), sqrt5), num(2));
|
|
16
|
+
const n = v("n");
|
|
17
|
+
|
|
18
|
+
const fibonacciAst = {
|
|
19
|
+
name: "fibonacci",
|
|
20
|
+
params: ["n"],
|
|
21
|
+
body: div(sub(call("pow", phi, n), call("pow", psi, n)), sqrt5),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
module.exports = { fibonacciAst };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// exprforge/samples/kitchen-sink.js
|
|
2
|
+
// Not a worked example like the other samples/ — a synthetic function with
|
|
3
|
+
// no real-world meaning, built to call every supported Math function
|
|
4
|
+
// (see README's "Supported Math functions") at least once in a single
|
|
5
|
+
// expression. Exists purely as a conformance-test fixture: the other
|
|
6
|
+
// samples only exercise 5 of the 22 functions between them, so most of
|
|
7
|
+
// the `calls` tables in emitters/*.js — several with hand-rolled,
|
|
8
|
+
// easy-to-get-wrong expansions (QB64's asin/acos as ATN+SQR identities,
|
|
9
|
+
// C's ternary-based sign, Java's log2/round/trunc, Go's Copysign-based
|
|
10
|
+
// sign) — had never actually been run through a compiler.
|
|
11
|
+
//
|
|
12
|
+
// x is kept in (0, 1) by every caller (see test/conformance.test.js's
|
|
13
|
+
// inputs for this sample) so sqrt/log/log2/log10/asin/acos all stay in
|
|
14
|
+
// their valid domain simultaneously. d = x - y is free to land anywhere
|
|
15
|
+
// -- positive, negative, or exactly zero -- and carries the functions
|
|
16
|
+
// that care about sign: floor/ceil/round/trunc/sign. (Test inputs
|
|
17
|
+
// deliberately avoid exact .5 fractional values for d: JS/Java round
|
|
18
|
+
// half-up while C/Go/Rust round half-away-from-zero, which disagree at
|
|
19
|
+
// exact .5 for negative numbers -- a real cross-language difference in
|
|
20
|
+
// round() itself, not an emitter bug, and out of scope to fix here.)
|
|
21
|
+
const { v, call, add, sub } = require("../ast.js");
|
|
22
|
+
|
|
23
|
+
const x = v("x");
|
|
24
|
+
const y = v("y");
|
|
25
|
+
const d = sub(x, y);
|
|
26
|
+
|
|
27
|
+
const kitchenSinkAst = {
|
|
28
|
+
name: "kitchenSink",
|
|
29
|
+
params: ["x", "y"],
|
|
30
|
+
body: add(
|
|
31
|
+
call("sqrt", x),
|
|
32
|
+
call("abs", d),
|
|
33
|
+
call("pow", x, y),
|
|
34
|
+
call("sin", x),
|
|
35
|
+
call("cos", x),
|
|
36
|
+
call("tan", x),
|
|
37
|
+
call("asin", x),
|
|
38
|
+
call("acos", x),
|
|
39
|
+
call("atan", x),
|
|
40
|
+
call("atan2", y, x),
|
|
41
|
+
call("log", x),
|
|
42
|
+
call("log2", x),
|
|
43
|
+
call("log10", x),
|
|
44
|
+
call("exp", x),
|
|
45
|
+
call("floor", d),
|
|
46
|
+
call("ceil", d),
|
|
47
|
+
call("round", d),
|
|
48
|
+
call("trunc", d),
|
|
49
|
+
call("sign", d),
|
|
50
|
+
call("min", x, y),
|
|
51
|
+
call("max", x, y),
|
|
52
|
+
call("hypot", x, y),
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
module.exports = { kitchenSinkAst };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// exprforge/samples/spline-frame.js
|
|
2
|
+
// Gram-Schmidt frame construction and related ops for Catmull-Rom spline
|
|
3
|
+
// paths — the motivating use case for the let/cmp/select AST additions,
|
|
4
|
+
// and now for outputs() (see ast.js). Mirrors math from a private game
|
|
5
|
+
// project's spline_path.bi / spline.ts.
|
|
6
|
+
//
|
|
7
|
+
// Each group below shares one let-chain across several *related* outputs
|
|
8
|
+
// (e.g. SpMakeFrame's R and U vectors) and is emitted as ONE suite —
|
|
9
|
+
// computed once — instead of one function per output. That used to be six
|
|
10
|
+
// separate functions here, each independently re-deriving the same
|
|
11
|
+
// 9-step chain (including a sqrt) from scratch; a caller wanting the
|
|
12
|
+
// whole frame paid for that chain six times over for one tangent vector.
|
|
13
|
+
// See docs/planned-additions.md and the outputs() doc comment in ast.js.
|
|
14
|
+
//
|
|
15
|
+
// IMPORTANT for QB64: function/SUB names must be unique across the entire
|
|
16
|
+
// QB64 compilation unit. The SpEf prefix (SplineExprforge) exists to avoid
|
|
17
|
+
// collisions with hand-written code elsewhere in that project.
|
|
18
|
+
const { num, v, call, add, mul, sub, div, neg, letIn, select, cmp, outputs } = require("../ast.js");
|
|
19
|
+
|
|
20
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
21
|
+
const PI = num(3.141592653589793);
|
|
22
|
+
const degToRad = (degVar) => mul(degVar, div(PI, num(180)));
|
|
23
|
+
const dot3 = (ax, ay, az, bx, by, bz) => add(mul(ax, bx), mul(ay, by), mul(az, bz));
|
|
24
|
+
const len3 = (x, y, z) => call("sqrt", dot3(x, y, z, x, y, z));
|
|
25
|
+
|
|
26
|
+
// Epsilon guard: normalize when len is big enough to trust, else fallback.
|
|
27
|
+
//
|
|
28
|
+
// select() always evaluates both branches (see ast.js), so this does NOT
|
|
29
|
+
// divide by lenVar directly — that would be undefined right when the guard
|
|
30
|
+
// is supposed to matter. Instead the denominator is clamped to a safe,
|
|
31
|
+
// always-nonzero value by its own select first, so the div() itself never
|
|
32
|
+
// sees anything near zero on any target, no matter which logical branch
|
|
33
|
+
// "wins".
|
|
34
|
+
const EPS = num(0.000001);
|
|
35
|
+
function safeDiv(component, lenVar, fallback) {
|
|
36
|
+
const isSafe = cmp(v(lenVar), ">", EPS);
|
|
37
|
+
const safeLen = select(isSafe, v(lenVar), num(1));
|
|
38
|
+
return select(isSafe, div(component, safeLen), fallback);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── SpMakeFrame ──────────────────────────────────────────────────────────
|
|
42
|
+
// Gram-Schmidt frame from normalized tangent (tx, ty, tz).
|
|
43
|
+
// worldUp = (0,0,1) when |ty|>0.98, else (0,1,0). worldUp.x is always 0.
|
|
44
|
+
// R = normalize(T × worldUp) U = R × T
|
|
45
|
+
//
|
|
46
|
+
// One shared let chain, six named outputs (rx,ry,rz,ux,uy,uz), computed
|
|
47
|
+
// once per call instead of once per output. The pre-normalization cross
|
|
48
|
+
// product is named crossX/Y/Z, deliberately NOT rx/ry/rz, even though
|
|
49
|
+
// that's its usual name here: it would collide with the "rx"/"ry"/"rz"
|
|
50
|
+
// *output* field names below in any language whose multi-return mechanism
|
|
51
|
+
// shares a namespace with local variables (this collided for real in Go's
|
|
52
|
+
// named-return-values form before that was changed to avoid relying on it —
|
|
53
|
+
// kept renamed anyway, both for clarity and because QB64's output-param
|
|
54
|
+
// SUBs are structurally the same risk and can't be verified here).
|
|
55
|
+
// wy = select(|ty|>0.98, 0, 1)
|
|
56
|
+
// wz = select(|ty|>0.98, 1, 0)
|
|
57
|
+
// crossX = ty*wz - tz*wy
|
|
58
|
+
// crossY = -tx*wz (worldUp.x=0 collapses two terms)
|
|
59
|
+
// crossZ = tx*wy
|
|
60
|
+
// rLen = sqrt(crossX²+crossY²+crossZ²)
|
|
61
|
+
// rxN = safeDiv(crossX, rLen, 0) — fallback 0
|
|
62
|
+
// ryN = safeDiv(crossY, rLen, 0)
|
|
63
|
+
// rzN = safeDiv(crossZ, rLen, 1) — fallback z=1 keeps a valid frame
|
|
64
|
+
|
|
65
|
+
const MF_PARAMS = ["tx", "ty", "tz"];
|
|
66
|
+
const nearVert = cmp(call("abs", v("ty")), ">", num(0.98));
|
|
67
|
+
|
|
68
|
+
function mfLetChain(body) {
|
|
69
|
+
return letIn("wy", select(nearVert, num(0), num(1)),
|
|
70
|
+
letIn("wz", select(nearVert, num(1), num(0)),
|
|
71
|
+
letIn("crossX", sub(mul(v("ty"), v("wz")), mul(v("tz"), v("wy"))),
|
|
72
|
+
letIn("crossY", neg(mul(v("tx"), v("wz"))),
|
|
73
|
+
letIn("crossZ", mul(v("tx"), v("wy")),
|
|
74
|
+
letIn("rLen", len3(v("crossX"), v("crossY"), v("crossZ")),
|
|
75
|
+
letIn("rxN", safeDiv(v("crossX"), "rLen", num(0)),
|
|
76
|
+
letIn("ryN", safeDiv(v("crossY"), "rLen", num(0)),
|
|
77
|
+
letIn("rzN", safeDiv(v("crossZ"), "rLen", num(1)),
|
|
78
|
+
body
|
|
79
|
+
)))))))));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// U = R × T (using normalized R) — a cyclic permutation, not one formula,
|
|
83
|
+
// so it's tabulated by axis rather than derived generically.
|
|
84
|
+
const uX = sub(mul(v("ryN"), v("tz")), mul(v("rzN"), v("ty")));
|
|
85
|
+
const uY = sub(mul(v("rzN"), v("tx")), mul(v("rxN"), v("tz")));
|
|
86
|
+
const uZ = sub(mul(v("rxN"), v("ty")), mul(v("ryN"), v("tx")));
|
|
87
|
+
|
|
88
|
+
const SpEfMkFrame = {
|
|
89
|
+
name: "SpEfMkFrame",
|
|
90
|
+
params: MF_PARAMS,
|
|
91
|
+
body: mfLetChain(outputs({
|
|
92
|
+
rx: v("rxN"), ry: v("ryN"), rz: v("rzN"),
|
|
93
|
+
ux: uX, uy: uY, uz: uZ,
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// ── SpActualPos ──────────────────────────────────────────────────────────
|
|
98
|
+
// actual = wire + standoff*(cos(pathRoll)*U + sin(pathRoll)*R)
|
|
99
|
+
// Params: wx, wy_wire, wz_wire, tx, ty, tz, prDeg, so
|
|
100
|
+
// (wy_wire to avoid colliding with the 'wy' let-binding name inside the chain)
|
|
101
|
+
const AP_PARAMS = ["wx", "wy_wire", "wz_wire", "tx", "ty", "tz", "prDeg", "so"];
|
|
102
|
+
|
|
103
|
+
function apLetChain(body) {
|
|
104
|
+
return letIn("wy", select(nearVert, num(0), num(1)),
|
|
105
|
+
letIn("wz", select(nearVert, num(1), num(0)),
|
|
106
|
+
letIn("crossX", sub(mul(v("ty"), v("wz")), mul(v("tz"), v("wy"))),
|
|
107
|
+
letIn("crossY", neg(mul(v("tx"), v("wz"))),
|
|
108
|
+
letIn("crossZ", mul(v("tx"), v("wy")),
|
|
109
|
+
letIn("rLen", len3(v("crossX"), v("crossY"), v("crossZ")),
|
|
110
|
+
letIn("rxN", safeDiv(v("crossX"), "rLen", num(0)),
|
|
111
|
+
letIn("ryN", safeDiv(v("crossY"), "rLen", num(0)),
|
|
112
|
+
letIn("rzN", safeDiv(v("crossZ"), "rLen", num(1)),
|
|
113
|
+
letIn("ux", sub(mul(v("ryN"), v("tz")), mul(v("rzN"), v("ty"))),
|
|
114
|
+
letIn("uy", sub(mul(v("rzN"), v("tx")), mul(v("rxN"), v("tz"))),
|
|
115
|
+
letIn("uz", sub(mul(v("rxN"), v("ty")), mul(v("ryN"), v("tx"))),
|
|
116
|
+
letIn("rad", degToRad(v("prDeg")),
|
|
117
|
+
letIn("c", call("cos", v("rad")),
|
|
118
|
+
letIn("s", call("sin", v("rad")),
|
|
119
|
+
body
|
|
120
|
+
)))))))))))))));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const SpEfActualPos = {
|
|
124
|
+
name: "SpEfActualPos",
|
|
125
|
+
params: AP_PARAMS,
|
|
126
|
+
body: apLetChain(outputs({
|
|
127
|
+
x: add(v("wx"), mul(v("so"), add(mul(v("c"), v("ux")), mul(v("s"), v("rxN"))))),
|
|
128
|
+
y: add(v("wy_wire"), mul(v("so"), add(mul(v("c"), v("uy")), mul(v("s"), v("ryN"))))),
|
|
129
|
+
z: add(v("wz_wire"), mul(v("so"), add(mul(v("c"), v("uz")), mul(v("s"), v("rzN"))))),
|
|
130
|
+
})),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ── SpRollFrame ──────────────────────────────────────────────────────────
|
|
134
|
+
// rolledU = cos(rad)*U - sin(rad)*R
|
|
135
|
+
// rolledR = sin(rad)*U + cos(rad)*R
|
|
136
|
+
// Params: ux, uy, uz, rx, ry, rz, crDeg
|
|
137
|
+
// Output field names are prefixed (rolledUx, not ux) since ux/uy/uz/rx/ry/rz
|
|
138
|
+
// are already taken by the *input* params.
|
|
139
|
+
const RF_PARAMS = ["ux", "uy", "uz", "rx", "ry", "rz", "crDeg"];
|
|
140
|
+
|
|
141
|
+
function rfLetChain(body) {
|
|
142
|
+
return letIn("rad", degToRad(v("crDeg")),
|
|
143
|
+
letIn("c", call("cos", v("rad")),
|
|
144
|
+
letIn("s", call("sin", v("rad")),
|
|
145
|
+
body)));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const SpEfRollFrame = {
|
|
149
|
+
name: "SpEfRollFrame",
|
|
150
|
+
params: RF_PARAMS,
|
|
151
|
+
body: rfLetChain(outputs({
|
|
152
|
+
rolledUx: sub(mul(v("c"), v("ux")), mul(v("s"), v("rx"))),
|
|
153
|
+
rolledUy: sub(mul(v("c"), v("uy")), mul(v("s"), v("ry"))),
|
|
154
|
+
rolledUz: sub(mul(v("c"), v("uz")), mul(v("s"), v("rz"))),
|
|
155
|
+
rolledRx: add(mul(v("s"), v("ux")), mul(v("c"), v("rx"))),
|
|
156
|
+
rolledRy: add(mul(v("s"), v("uy")), mul(v("c"), v("ry"))),
|
|
157
|
+
rolledRz: add(mul(v("s"), v("uz")), mul(v("c"), v("rz"))),
|
|
158
|
+
})),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// ── CR basis weights ────────────────────────────────────────────────────
|
|
162
|
+
// Already expressible without let/select, but kept here so all spline
|
|
163
|
+
// math lives in one sample. Shares t2/t3 across all four weights.
|
|
164
|
+
// Params: t
|
|
165
|
+
const CRW_PARAMS = ["t"];
|
|
166
|
+
|
|
167
|
+
const SpEfCrWeights = {
|
|
168
|
+
name: "SpEfCrWeights",
|
|
169
|
+
params: CRW_PARAMS,
|
|
170
|
+
body: letIn("t2", mul(v("t"), v("t")),
|
|
171
|
+
letIn("t3", mul(v("t2"), v("t")),
|
|
172
|
+
outputs({
|
|
173
|
+
w0: mul(num(0.5), add(neg(v("t3")), mul(num(2), v("t2")), neg(v("t")))),
|
|
174
|
+
w1: mul(num(0.5), add(mul(num(3), v("t3")), mul(num(-5), v("t2")), num(2))),
|
|
175
|
+
w2: mul(num(0.5), add(mul(num(-3), v("t3")), mul(num(4), v("t2")), v("t"))),
|
|
176
|
+
w3: mul(num(0.5), add(v("t3"), neg(v("t2")))),
|
|
177
|
+
})
|
|
178
|
+
)),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ── Exports ──────────────────────────────────────────────────────────────
|
|
182
|
+
const splineFrameAsts = [SpEfMkFrame, SpEfActualPos, SpEfRollFrame, SpEfCrWeights];
|
|
183
|
+
|
|
184
|
+
module.exports = { splineFrameAsts };
|
package/util.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// exprforge/util.js
|
|
2
|
+
// Authoring conveniences — NOT AST primitives (see ast.js for those). These
|
|
3
|
+
// don't return Nodes and the emitters never see them; they just help you
|
|
4
|
+
// write the { name, params, body } function definitions exprforge already
|
|
5
|
+
// understands, without hand-duplicating near-identical ones.
|
|
6
|
+
|
|
7
|
+
// Expands a template into one function definition per axis. Purely
|
|
8
|
+
// `axes.map(templateFn)` under a name that signals intent: this is a vector
|
|
9
|
+
// operation expanded component-wise, not an arbitrary loop. See
|
|
10
|
+
// samples/spline-frame.js for a real call site (SpMakeFrame's R/U vectors).
|
|
11
|
+
function forComponents(axes, templateFn) {
|
|
12
|
+
return axes.map(templateFn);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { forComponents };
|