exprforge 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # ExprForge šŸ”¢šŸ”Ø
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/exprforge.svg)](https://www.npmjs.com/package/exprforge)
3
4
  [![TypeScript](https://github.com/theraccoonbear/exprforge/actions/workflows/test-typescript.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-typescript.yml)
4
5
  [![Python](https://github.com/theraccoonbear/exprforge/actions/workflows/test-python.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-python.yml)
5
6
  [![C#](https://github.com/theraccoonbear/exprforge/actions/workflows/test-csharp.yml/badge.svg)](https://github.com/theraccoonbear/exprforge/actions/workflows/test-csharp.yml)
@@ -69,6 +70,9 @@ package individually, or together as `samples`):
69
70
  purely as a conformance-test fixture. It's what caught Go's and Rust's
70
71
  `sign()` disagreeing with everyone else at exactly zero (see below) —
71
72
  the other samples between them only ever exercised 5 of the 22.
73
+ - `samples/math-demo.js` — also not a worked example: a conformance-test
74
+ fixture exercising every `exprforge/math` helper (see below) in one
75
+ suite.
72
76
 
73
77
  `npm run build` emits all of them, for every target language, into `out/`.
74
78
 
@@ -80,6 +84,46 @@ floor ceil round trunc sign min max hypot`
80
84
  Add more by extending a target's `calls` table in `emitters/<lang>.js`.
81
85
  Requesting an unmapped function throws at build time, not silently.
82
86
 
87
+ ## Math utilities (`exprforge/math`)
88
+
89
+ A separate, additive export — `require("exprforge")` is unchanged — of
90
+ pre-built compositions of the core AST builders for common 3-D math
91
+ patterns, so consumers stop re-implementing the same safe-math and vector
92
+ code in every project (`samples/spline-frame.js` had local, hand-rolled
93
+ versions of most of these before this module existed).
94
+
95
+ ```js
96
+ const { num, v } = require("exprforge");
97
+ const { safeDiv, dot3, len3, cross3, normalize3, clamp, EPS } = require("exprforge/math");
98
+
99
+ // Safe-normalize x component, falling back to 0 near zero length.
100
+ safeDiv(v("x"), len3(v("x"), v("y"), v("z")), num(0));
101
+ ```
102
+
103
+ - `safeDiv(numerator, denominatorExpr, fallback)` — `numerator /
104
+ denominatorExpr` when `|denominatorExpr| > EPS`, else `fallback`. Clamps
105
+ the denominator before dividing rather than guarding the division
106
+ directly, since `select()` always evaluates both branches (see below).
107
+ - `dot3(ax, ay, az, bx, by, bz)` — `ax*bx + ay*by + az*bz`.
108
+ - `len3(x, y, z)` — `sqrt(x² + y² + z²)`.
109
+ - `cross3(ax, ay, az, bx, by, bz)` — 3-D cross product. Returns a plain JS
110
+ object `{ x, y, z }` of AST nodes (not a Node itself), for destructuring
111
+ into your own `letIn` chain.
112
+ - `normalize3(x, y, z, fx?, fy?, fz?)` — safe-normalize; same `{ x, y, z }`
113
+ shape as `cross3`. Falls back to `(fx, fy, fz)` (default `(0, 1, 0)`)
114
+ below `EPS` length. Computes the length once and shares it across all
115
+ three divisions.
116
+ - `clamp(val, lo, hi)` — clamps to `[lo, hi]` via nested `select`/`cmp`; no
117
+ runtime intrinsic.
118
+ - `EPS` — `num(0.000001)`, the epsilon every guard above uses; exported for
119
+ callers who want the same threshold in their own `cmp()` calls.
120
+
121
+ What deliberately stays out (project-specific conventions, not general
122
+ math): a "near-vertical" world-up check, baked-in-PI degree/radian
123
+ conversion, Rodrigues rotation, and a full Gram-Schmidt frame — see
124
+ `samples/spline-frame.js` for those, and
125
+ `docs/v0.2.0-math-utilities.md` for the full design rationale.
126
+
83
127
  ## Adding a language
84
128
 
85
129
  Write `emitters/<lang>.js` exporting an `Emitter` instance (see any
@@ -97,6 +141,33 @@ expression model without introducing control flow:
97
141
  components by it). Every `let` in a function gets lifted into an ordered
98
142
  list of local declarations ahead of the return statement/expression, in
99
143
  every target.
144
+
145
+ Chaining several is normally hand-nested `letIn` calls, one inside the
146
+ next, closing parens piling up at the end with no real hierarchy behind
147
+ them — just bookkeeping to get everything hoisted before it's used.
148
+ **`letChain(bindings, body)`** is that same nesting, built for you from a
149
+ flat, ordered list instead:
150
+
151
+ ```js
152
+ const { v, num, mul, add, letChain, outputs } = require("exprforge");
153
+
154
+ letChain(
155
+ [
156
+ ["t2", mul(v("t"), v("t"))],
157
+ ["t3", mul(v("t2"), v("t"))],
158
+ ],
159
+ outputs({ t2: v("t2"), t3: v("t3") }),
160
+ );
161
+ // same tree as letIn("t2", ..., letIn("t3", ..., outputs({...})))
162
+ ```
163
+
164
+ `bindings` is an ordered array of `[name, valueNode]` pairs, not a
165
+ `{name: valueNode}` object like `outputs()` takes — order is
166
+ load-bearing here (a later binding's value can reference an earlier
167
+ one's name), and that's clearer as an explicit sequence than resting on
168
+ an object's key order. Pure authoring sugar: builds the identical `let`
169
+ node structure `letIn` would, so it needs no emitter changes and
170
+ round-trips through `collectLets` the same way.
100
171
  - **`select(cond, then, else)` + `cmp(left, op, right)`** — conditional
101
172
  *value* selection. Every target has a genuinely different way to spell
102
173
  this: a native ternary where one exists (C, Java, C#), `if`-as-expression
package/ast.js CHANGED
@@ -62,6 +62,27 @@ function letIn(name, value, body) {
62
62
  return { type: "let", name, value, body };
63
63
  }
64
64
 
65
+ // Chains N letIn bindings without hand-nesting them (and hand-balancing the
66
+ // resulting N closing parens — the nesting depth reflects no real
67
+ // hierarchy, only that each binding must be lifted ahead of anything using
68
+ // it). Builds the exact same nested `let` structure letIn() would if
69
+ // written out by hand: pure authoring sugar, not a new node type, so
70
+ // collectLets and every emitter already understand the result unchanged.
71
+ //
72
+ // `bindings` is an ORDERED array of [name, valueNode] pairs, not a
73
+ // {name: valueNode} object like outputs() takes — order is load-bearing
74
+ // here (a later binding's value can reference an earlier one's name via
75
+ // v(name)), and a plain object's key order isn't reliably that: a binding
76
+ // named e.g. "0" would silently sort ahead of everything else. An array
77
+ // keeps "this is a strict sequence" explicit instead of resting on that.
78
+ //
79
+ // Doesn't check for duplicate names itself — collectLets already does,
80
+ // with the whole function body in view (see its doc comment); duplicating
81
+ // that check here would only see this one chain, not the whole picture.
82
+ function letChain(bindings, body) {
83
+ return bindings.reduceRight((acc, [name, value]) => letIn(name, value, acc), body);
84
+ }
85
+
65
86
  // Comparison predicate — only valid as the `cond` of a select(); not a
66
87
  // general boolean expression, and shouldn't appear anywhere else in a tree.
67
88
  function cmp(left, op, right) {
@@ -139,4 +160,4 @@ function collectLets(node) {
139
160
  return { bindings, body };
140
161
  }
141
162
 
142
- module.exports = { num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets };
163
+ module.exports = { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets };
package/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // exprforge/index.js
2
- const { num, v, bin, call, add, mul, sub, div, neg, letIn, cmp, select, outputs, collectLets } = require("./ast.js");
2
+ const { num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets } = require("./ast.js");
3
3
  const { forComponents } = require("./util.js");
4
4
  const emitters = require("./emitters/registry.js");
5
5
  const { catmullRomAst } = require("./samples/catmull-rom.js");
6
6
  const { fibonacciAst } = require("./samples/fibonacci.js");
7
7
  const { splineFrameAsts } = require("./samples/spline-frame.js");
8
8
  const { kitchenSinkAst } = require("./samples/kitchen-sink.js");
9
+ const { mathDemoAst } = require("./samples/math-demo.js");
9
10
 
10
11
  /**
11
12
  * Run every registered emitter against one AST function definition.
@@ -21,7 +22,7 @@ function emitAll(fn) {
21
22
 
22
23
  module.exports = {
23
24
  // 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
+ num, v, bin, call, add, mul, sub, div, neg, letIn, letChain, cmp, select, outputs, collectLets,
25
26
  // Authoring convenience — not an AST primitive, see util.js.
26
27
  forComponents,
27
28
  // Built-in example formulas — see samples/ for the source.
@@ -31,11 +32,15 @@ module.exports = {
31
32
  // Not a worked example -- a conformance-test fixture that calls every
32
33
  // supported Math function once. See samples/kitchen-sink.js.
33
34
  kitchenSinkAst,
35
+ // Also not a worked example -- a conformance-test fixture for
36
+ // require("exprforge/math"). See samples/math-demo.js.
37
+ mathDemoAst,
34
38
  samples: {
35
39
  catmullRom: catmullRomAst,
36
40
  fibonacci: fibonacciAst,
37
41
  splineFrame: splineFrameAsts,
38
42
  kitchenSink: kitchenSinkAst,
43
+ mathDemo: mathDemoAst,
39
44
  },
40
45
  // Per-language emitter instances, keyed by name (js, qb64, c, java, go, rust).
41
46
  emitters,
package/math/index.js ADDED
@@ -0,0 +1,105 @@
1
+ // exprforge/math/index.js
2
+ // Standard math utilities — see docs/v0.2.0-math-utilities.md for the
3
+ // design doc this implements. This module is level 1: pure compositions of
4
+ // the level-0 AST primitives (ast.js), built to save every consumer from
5
+ // re-deriving the same safe-math/vector patterns samples/spline-frame.js
6
+ // used to hand-roll locally (dot3/len3/safeDiv/EPS there predate this file
7
+ // and motivated it). No new emitter logic — emitAll handles these
8
+ // transparently, same as any other AST the caller builds by hand.
9
+ //
10
+ // require("exprforge/math") is a separate export path from require("exprforge")
11
+ // itself (see package.json's "exports" map) — additive, not merged into the
12
+ // core barrel.
13
+ const { num, v, call, add, mul, sub, div, letIn, cmp, select } = require("../ast.js");
14
+
15
+ // Shared epsilon for all near-zero guards below. Exposed so callers can
16
+ // reuse it in their own cmp() calls for consistency with safeDiv/normalize3,
17
+ // same convention as samples/spline-frame.js's local EPS.
18
+ const EPS = num(0.000001);
19
+
20
+ // Guard against division by zero: numerator/denominatorExpr when
21
+ // |denominatorExpr| > EPS, else fallback. Per select()'s doc comment in
22
+ // ast.js, both branches of a select are always evaluated on every target —
23
+ // so this does NOT guard the division directly (div(numerator,
24
+ // denominatorExpr) would still be reached with a near-zero denominator on
25
+ // any target that can't short-circuit, e.g. QB64). Instead the denominator
26
+ // is clamped to a safe, always-nonzero value by its own select first,
27
+ // mirroring the local safeDiv in samples/spline-frame.js.
28
+ //
29
+ // denominatorExpr is referenced twice in the resulting tree (once for the
30
+ // |.| > EPS check, once in the clamped-denominator select) — cheap if it's
31
+ // a var reference or a simple expression, but if it's an expensive
32
+ // subexpression (e.g. a len3() call), pass an already-letIn-bound v(name)
33
+ // instead of the raw expression to avoid computing it twice per emitted
34
+ // target. normalize3() below does exactly that internally.
35
+ function safeDiv(numerator, denominatorExpr, fallback) {
36
+ const isSafe = cmp(call("abs", denominatorExpr), ">", EPS);
37
+ const safeDenom = select(isSafe, denominatorExpr, num(1));
38
+ return select(isSafe, div(numerator, safeDenom), fallback);
39
+ }
40
+
41
+ // 3-D dot product: ax*bx + ay*by + az*bz.
42
+ function dot3(ax, ay, az, bx, by, bz) {
43
+ return add(mul(ax, bx), mul(ay, by), mul(az, bz));
44
+ }
45
+
46
+ // 3-D Euclidean length: sqrt(x² + y² + z²). Emits a sqrt intrinsic (see
47
+ // README's "Supported Math functions").
48
+ function len3(x, y, z) {
49
+ return call("sqrt", dot3(x, y, z, x, y, z));
50
+ }
51
+
52
+ // 3-D cross product. Returns a plain JS object { x, y, z } of AST nodes,
53
+ // NOT an AST node itself — a deliberate ergonomic choice so callers can
54
+ // destructure and name each component in their own letIn chain, rather
55
+ // than exprforge picking the names for them.
56
+ function cross3(ax, ay, az, bx, by, bz) {
57
+ return {
58
+ x: sub(mul(ay, bz), mul(az, by)),
59
+ y: sub(mul(az, bx), mul(ax, bz)),
60
+ z: sub(mul(ax, by), mul(ay, bx)),
61
+ };
62
+ }
63
+
64
+ // Monotonic counter behind normalize3's internal let-binding names — see
65
+ // the comment inside normalize3 for why it needs one at all. Global (not
66
+ // per-call-site) is deliberately overkill: it only has to avoid colliding
67
+ // with another normalize3() binding inside the same function body, and a
68
+ // process-wide counter trivially guarantees that regardless of how many
69
+ // times normalize3 is called across however many functions.
70
+ let normalizeGensymCounter = 0;
71
+
72
+ // Safe-normalize a 3-D vector. Returns { x, y, z } (same shape as cross3).
73
+ // Falls back to (fx, fy, fz) — default (0, 1, 0) — when the vector's length
74
+ // is at or below EPS.
75
+ //
76
+ // Per the spec's recommendation, this computes len3(x, y, z) ONCE and
77
+ // shares it across all three divisions (one EPS check, one sqrt), instead
78
+ // of calling safeDiv three times against three independent len3() calls.
79
+ // The mechanism: the length is let-bound inside the `x` field's own tree,
80
+ // and `y`/`z` just reference that bound name bare. collectLets (ast.js)
81
+ // hoists a let found anywhere in a function body to one flat, ordered list
82
+ // regardless of which sibling subtree it was found in — see
83
+ // test/ast.test.js's "collectLets hoists a let nested inside one output
84
+ // field's own value" for the exact behavior this relies on. The gensym'd
85
+ // name avoids a "duplicate let binding name" throw if normalize3 is called
86
+ // more than once inside one function (e.g. normalizing two vectors).
87
+ function normalize3(x, y, z, fx = num(0), fy = num(1), fz = num(0)) {
88
+ const lenName = `__exprforgeMathNrmLen${normalizeGensymCounter++}`;
89
+ return {
90
+ x: letIn(lenName, len3(x, y, z), safeDiv(x, v(lenName), fx)),
91
+ y: safeDiv(y, v(lenName), fy),
92
+ z: safeDiv(z, v(lenName), fz),
93
+ };
94
+ }
95
+
96
+ // Clamps val to [lo, hi]. Expressed as nested select/cmp — no runtime
97
+ // intrinsic required, matching cmp/select's existing usage elsewhere (see
98
+ // samples/spline-frame.js). val is referenced three times in the resulting
99
+ // tree; pass a var reference (or an already-let-bound one) if it's not
100
+ // already cheap to re-evaluate.
101
+ function clamp(val, lo, hi) {
102
+ return select(cmp(val, "<", lo), lo, select(cmp(val, ">", hi), hi, val));
103
+ }
104
+
105
+ module.exports = { EPS, safeDiv, dot3, len3, cross3, normalize3, clamp };
package/package.json CHANGED
@@ -1,16 +1,22 @@
1
1
  {
2
2
  "name": "exprforge",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
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
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./math": "./math/index.js",
10
+ "./package.json": "./package.json"
11
+ },
7
12
  "files": [
8
13
  "index.js",
9
14
  "ast.js",
10
15
  "util.js",
11
16
  "build.js",
12
17
  "emitters/",
13
- "samples/"
18
+ "samples/",
19
+ "math/"
14
20
  ],
15
21
  "scripts": {
16
22
  "build": "node build.js",
@@ -0,0 +1,44 @@
1
+ // exprforge/samples/math-demo.js
2
+ // Not a worked example -- a conformance-test fixture for math/index.js,
3
+ // same role samples/kitchen-sink.js plays for the core Math intrinsics:
4
+ // exercises every exprforge/math helper in one suite, purely so
5
+ // test/conformance.test.js can prove the generated code agrees across
6
+ // every target. Calls normalize3() twice (once per input vector) in the
7
+ // same function specifically to prove its internal gensym'd let-binding
8
+ // doesn't collide with itself -- see the comment on normalizeGensymCounter
9
+ // in math/index.js.
10
+ const { v, num, outputs } = require("../ast.js");
11
+ const { safeDiv, dot3, len3, cross3, normalize3, clamp } = require("../math/index.js");
12
+
13
+ const MATH_DEMO_PARAMS = ["ax", "ay", "az", "bx", "by", "bz", "t", "lo", "hi"];
14
+
15
+ const cross = cross3(v("ax"), v("ay"), v("az"), v("bx"), v("by"), v("bz"));
16
+ const normA = normalize3(v("ax"), v("ay"), v("az"));
17
+ const normB = normalize3(v("bx"), v("by"), v("bz"));
18
+
19
+ const MathDemo = {
20
+ name: "MathDemo",
21
+ params: MATH_DEMO_PARAMS,
22
+ body: outputs({
23
+ dot: dot3(v("ax"), v("ay"), v("az"), v("bx"), v("by"), v("bz")),
24
+ // "mag", not "len": LEN is a reserved QB64 builtin (string/array
25
+ // length) -- see test/conformance.test.js's normalizeXAst comment.
26
+ mag: len3(v("ax"), v("ay"), v("az")),
27
+ crossX: cross.x,
28
+ crossY: cross.y,
29
+ crossZ: cross.z,
30
+ normAX: normA.x,
31
+ normAY: normA.y,
32
+ normAZ: normA.z,
33
+ normBX: normB.x,
34
+ normBY: normB.y,
35
+ normBZ: normB.z,
36
+ clamped: clamp(v("t"), v("lo"), v("hi")),
37
+ // ax/bx as numerator/denominator: exercises both the safe path
38
+ // (bx away from zero) and the fallback (bx == 0, see
39
+ // test/conformance.test.js's math-demo input rows).
40
+ safeDivResult: safeDiv(v("ax"), v("bx"), num(-1)),
41
+ }),
42
+ };
43
+
44
+ module.exports = { mathDemoAst: MathDemo };
@@ -15,7 +15,12 @@
15
15
  // IMPORTANT for QB64: function/SUB names must be unique across the entire
16
16
  // QB64 compilation unit. The SpEf prefix (SplineExprforge) exists to avoid
17
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");
18
+ //
19
+ // mfLetChain/apLetChain/rfLetChain/SpEfCrWeights use letChain() (ast.js)
20
+ // instead of hand-nested letIn calls -- apLetChain was 14 levels deep
21
+ // before, all hand-balanced closing parens with no real hierarchy behind
22
+ // the nesting, exactly the kind of thing that's easy to miscount editing.
23
+ const { num, v, call, add, mul, sub, div, neg, letChain, select, cmp, outputs } = require("../ast.js");
19
24
 
20
25
  // ── Helpers ──────────────────────────────────────────────────────────────
21
26
  const PI = num(3.141592653589793);
@@ -66,17 +71,20 @@ const MF_PARAMS = ["tx", "ty", "tz"];
66
71
  const nearVert = cmp(call("abs", v("ty")), ">", num(0.98));
67
72
 
68
73
  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
- )))))))));
74
+ return letChain(
75
+ [
76
+ ["wy", select(nearVert, num(0), num(1))],
77
+ ["wz", select(nearVert, num(1), num(0))],
78
+ ["crossX", sub(mul(v("ty"), v("wz")), mul(v("tz"), v("wy")))],
79
+ ["crossY", neg(mul(v("tx"), v("wz")))],
80
+ ["crossZ", mul(v("tx"), v("wy"))],
81
+ ["rLen", len3(v("crossX"), v("crossY"), v("crossZ"))],
82
+ ["rxN", safeDiv(v("crossX"), "rLen", num(0))],
83
+ ["ryN", safeDiv(v("crossY"), "rLen", num(0))],
84
+ ["rzN", safeDiv(v("crossZ"), "rLen", num(1))],
85
+ ],
86
+ body,
87
+ );
80
88
  }
81
89
 
82
90
  // U = R Ɨ T (using normalized R) — a cyclic permutation, not one formula,
@@ -101,23 +109,26 @@ const SpEfMkFrame = {
101
109
  const AP_PARAMS = ["wx", "wy_wire", "wz_wire", "tx", "ty", "tz", "prDeg", "so"];
102
110
 
103
111
  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
- )))))))))))))));
112
+ return letChain(
113
+ [
114
+ ["wy", select(nearVert, num(0), num(1))],
115
+ ["wz", select(nearVert, num(1), num(0))],
116
+ ["crossX", sub(mul(v("ty"), v("wz")), mul(v("tz"), v("wy")))],
117
+ ["crossY", neg(mul(v("tx"), v("wz")))],
118
+ ["crossZ", mul(v("tx"), v("wy"))],
119
+ ["rLen", len3(v("crossX"), v("crossY"), v("crossZ"))],
120
+ ["rxN", safeDiv(v("crossX"), "rLen", num(0))],
121
+ ["ryN", safeDiv(v("crossY"), "rLen", num(0))],
122
+ ["rzN", safeDiv(v("crossZ"), "rLen", num(1))],
123
+ ["ux", sub(mul(v("ryN"), v("tz")), mul(v("rzN"), v("ty")))],
124
+ ["uy", sub(mul(v("rzN"), v("tx")), mul(v("rxN"), v("tz")))],
125
+ ["uz", sub(mul(v("rxN"), v("ty")), mul(v("ryN"), v("tx")))],
126
+ ["rad", degToRad(v("prDeg"))],
127
+ ["c", call("cos", v("rad"))],
128
+ ["s", call("sin", v("rad"))],
129
+ ],
130
+ body,
131
+ );
121
132
  }
122
133
 
123
134
  const SpEfActualPos = {
@@ -139,10 +150,14 @@ const SpEfActualPos = {
139
150
  const RF_PARAMS = ["ux", "uy", "uz", "rx", "ry", "rz", "crDeg"];
140
151
 
141
152
  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)));
153
+ return letChain(
154
+ [
155
+ ["rad", degToRad(v("crDeg"))],
156
+ ["c", call("cos", v("rad"))],
157
+ ["s", call("sin", v("rad"))],
158
+ ],
159
+ body,
160
+ );
146
161
  }
147
162
 
148
163
  const SpEfRollFrame = {
@@ -167,15 +182,18 @@ const CRW_PARAMS = ["t"];
167
182
  const SpEfCrWeights = {
168
183
  name: "SpEfCrWeights",
169
184
  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
- )),
185
+ body: letChain(
186
+ [
187
+ ["t2", mul(v("t"), v("t"))],
188
+ ["t3", mul(v("t2"), v("t"))],
189
+ ],
190
+ outputs({
191
+ w0: mul(num(0.5), add(neg(v("t3")), mul(num(2), v("t2")), neg(v("t")))),
192
+ w1: mul(num(0.5), add(mul(num(3), v("t3")), mul(num(-5), v("t2")), num(2))),
193
+ w2: mul(num(0.5), add(mul(num(-3), v("t3")), mul(num(4), v("t2")), v("t"))),
194
+ w3: mul(num(0.5), add(v("t3"), neg(v("t2")))),
195
+ }),
196
+ ),
179
197
  };
180
198
 
181
199
  // ── Exports ──────────────────────────────────────────────────────────────