exprforge 0.6.1 → 0.7.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 +59 -3
- package/differentiate.js +345 -0
- package/index.js +4 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -32,8 +32,15 @@ dependencies.
|
|
|
32
32
|
|
|
33
33
|
**[▶ Try it live](https://theraccoonbear.github.io/exprforge/)** — write
|
|
34
34
|
a formula in the browser and watch it emitted across every target
|
|
35
|
-
language at once, no install required
|
|
36
|
-
|
|
35
|
+
language at once, no install required, or switch to the Differentiation
|
|
36
|
+
tab to get a formula's derivative and a numeric spot-check side by side.
|
|
37
|
+
Runs the real, current library (see `playground/`), not a frozen demo
|
|
38
|
+
build.
|
|
39
|
+
|
|
40
|
+
Every open pull request also gets its own live preview of the
|
|
41
|
+
playground, deployed automatically to
|
|
42
|
+
`https://theraccoonbear.github.io/exprforge/pr-<N>/` and linked in a
|
|
43
|
+
comment on the PR (see `.github/workflows/deploy-pr-preview.yml`).
|
|
37
44
|
|
|
38
45
|
## Motivation
|
|
39
46
|
|
|
@@ -68,7 +75,9 @@ Two shapes of real use this tends to fall into:
|
|
|
68
75
|
**What it does**: turns one small, pure-arithmetic AST into
|
|
69
76
|
identical-behavior source text for 16 real target languages, a native
|
|
70
77
|
evaluator, and its own readable printer — all from the same tree, walked
|
|
71
|
-
once per target.
|
|
78
|
+
once per target. Symbolic differentiation (`differentiate`) works over
|
|
79
|
+
that same AST too, so a derivative is just another tree, emittable and
|
|
80
|
+
evaluable exactly the same way.
|
|
72
81
|
|
|
73
82
|
**What it deliberately won't do** — not gaps waiting on a future
|
|
74
83
|
release, but a boundary held on purpose everywhere in this project:
|
|
@@ -325,6 +334,53 @@ validate against), a wrong argument *count* is a structurally malformed
|
|
|
325
334
|
call regardless of target, checked unconditionally at the same tier as
|
|
326
335
|
`checkUnboundVars` — see `primitives.js`.
|
|
327
336
|
|
|
337
|
+
## Symbolic differentiation (`differentiate`)
|
|
338
|
+
|
|
339
|
+
```js
|
|
340
|
+
const { fn, differentiate, emit, evaluate } = require("exprforge");
|
|
341
|
+
|
|
342
|
+
const f = fn`
|
|
343
|
+
f(x):
|
|
344
|
+
return x^2 * sin(x);
|
|
345
|
+
`;
|
|
346
|
+
const df = { name: "df_dx", params: f.params, body: differentiate(f.body, "x") };
|
|
347
|
+
|
|
348
|
+
console.log(emit(df, "python").source);
|
|
349
|
+
console.log(evaluate(df, [Math.PI])); // -π² ≈ -9.8696
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`differentiate(node, varName)` returns an ordinary AST node — the
|
|
353
|
+
symbolic derivative of `node` with respect to `varName` — in the exact
|
|
354
|
+
same representation as everything else, so it's emittable to all 18
|
|
355
|
+
targets via `emit()`/`emitMany()` and evaluable via `evaluate()`
|
|
356
|
+
unchanged. The input is never mutated.
|
|
357
|
+
|
|
358
|
+
- **Covers every differentiable primitive**: sum/difference/product/
|
|
359
|
+
quotient rule, plus the chain rule for `sqrt abs sin cos tan asin acos
|
|
360
|
+
atan log log2 log10 exp pow atan2 min max hypot` (`pow` picks power
|
|
361
|
+
rule, exponential rule, or the general product-and-chain-rule case,
|
|
362
|
+
depending on which side of `^` actually varies with respect to
|
|
363
|
+
`varName`).
|
|
364
|
+
- **`floor ceil round trunc sign` throw** at differentiation time, with a
|
|
365
|
+
clear error naming the offending call — these are piecewise-constant/
|
|
366
|
+
discontinuous primitives with no meaningful derivative, so this fails
|
|
367
|
+
loudly instead of silently producing a wrong AST.
|
|
368
|
+
- **Output is simplified, not the raw mechanical rules verbatim.** A
|
|
369
|
+
bottom-up pass folds constant subtrees (`num op num` → `num`) and
|
|
370
|
+
eliminates arithmetic identities (`x + 0`, `x * 1`, `x / 1`, `x^0`,
|
|
371
|
+
`x^1`, `0 - x`) to a fixpoint, so a real formula's derivative doesn't
|
|
372
|
+
come back buried in the `* 1`/`+ 0` swell every mechanical
|
|
373
|
+
product/chain rule application produces.
|
|
374
|
+
- **Verified numerically, not hand-checked algebraically** — every rule's
|
|
375
|
+
test asserts the symbolic result against a central-difference
|
|
376
|
+
approximation at several sample points (see `test/differentiate.test.js`),
|
|
377
|
+
the same "proof by running" approach this project already uses for
|
|
378
|
+
round-tripping expr syntax (see "Testing").
|
|
379
|
+
|
|
380
|
+
Try it interactively in the [live playground](https://theraccoonbear.github.io/exprforge/)'s
|
|
381
|
+
Differentiation tab — enter a formula, see the derivative and a numeric
|
|
382
|
+
spot-check side by side.
|
|
383
|
+
|
|
328
384
|
## Math utilities (`exprforge/math`)
|
|
329
385
|
|
|
330
386
|
A separate, additive export — `require("exprforge")` is unchanged — of
|
package/differentiate.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// exprforge/differentiate.js
|
|
2
|
+
//
|
|
3
|
+
// Symbolic differentiation: differentiate(node, varName) returns an AST
|
|
4
|
+
// node representing the derivative of `node` with respect to `varName`.
|
|
5
|
+
//
|
|
6
|
+
// The result is an ordinary AST in the exact same representation as
|
|
7
|
+
// everything else -- emittable to all 18 targets via emit()/emitMany()
|
|
8
|
+
// unchanged, and evaluable via evaluate() for the numerical verification
|
|
9
|
+
// the issue describes (central-difference proof).
|
|
10
|
+
//
|
|
11
|
+
// Scope: all differentiable primitives in the AST grammar.
|
|
12
|
+
// Non-differentiable operations (floor, ceil, round, trunc, sign) throw
|
|
13
|
+
// with a clear error at differentiation time rather than silently
|
|
14
|
+
// producing a wrong result.
|
|
15
|
+
//
|
|
16
|
+
// Design notes:
|
|
17
|
+
// - differentiateRaw() is purely mechanical -- product/quotient/chain
|
|
18
|
+
// rules produce expression swell (terms like `* 1`, `+ 0`) verbatim,
|
|
19
|
+
// which keeps every rule obviously correct by inspection. simplify()
|
|
20
|
+
// (below) is a separate bottom-up pass over that raw output --
|
|
21
|
+
// constant folding plus arithmetic-identity elimination -- run
|
|
22
|
+
// automatically by the exported differentiate(), so callers never
|
|
23
|
+
// see the raw swell. A standalone, general-purpose version of this
|
|
24
|
+
// (usable on any AST, not just differentiate()'s output) is issue #9.
|
|
25
|
+
// - Every rule is structural: it only looks at node.type and recurses.
|
|
26
|
+
// No alpha-renaming, no capture-avoiding substitution -- the output
|
|
27
|
+
// is a fresh tree built from the input's subterms, never mutating
|
|
28
|
+
// the input.
|
|
29
|
+
|
|
30
|
+
const { num, v, bin, call, add, mul, sub, div, neg, select, cmp } = require("./ast.js");
|
|
31
|
+
|
|
32
|
+
// The non-differentiable built-in primitives. These are piecewise-constant
|
|
33
|
+
// or discontinuous -- no meaningful derivative exists. Throwing here
|
|
34
|
+
// catches the mistake at differentiation time rather than silently
|
|
35
|
+
// producing a semantically wrong AST that happens to evaluate.
|
|
36
|
+
const NON_DIFFERENTIABLE = new Set(["floor", "ceil", "round", "trunc", "sign"]);
|
|
37
|
+
|
|
38
|
+
// differentiate(node, varName) -> Node
|
|
39
|
+
//
|
|
40
|
+
// Returns the symbolic derivative of `node` with respect to the variable
|
|
41
|
+
// named `varName`. The input is never mutated.
|
|
42
|
+
function differentiate(node, varName) {
|
|
43
|
+
return simplify(differentiateRaw(node, varName));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function differentiateRaw(node, varName) {
|
|
47
|
+
switch (node.type) {
|
|
48
|
+
// d/dx c = 0
|
|
49
|
+
case "num":
|
|
50
|
+
return num(0);
|
|
51
|
+
|
|
52
|
+
// d/dx x = 1, d/dx y = 0
|
|
53
|
+
case "var":
|
|
54
|
+
return node.name === varName ? num(1) : num(0);
|
|
55
|
+
|
|
56
|
+
// Binary arithmetic: product rule, quotient rule, sum/difference rule
|
|
57
|
+
case "bin":
|
|
58
|
+
return differentiateBin(node, varName);
|
|
59
|
+
|
|
60
|
+
// Function calls: chain rule + one derivative rule per intrinsic
|
|
61
|
+
case "call":
|
|
62
|
+
return differentiateCall(node, varName);
|
|
63
|
+
|
|
64
|
+
// select/cmp: differentiate both branches (both are always
|
|
65
|
+
// evaluated by design -- this is a value, not a branch). The
|
|
66
|
+
// condition's derivative is irrelevant (it selects, not computes).
|
|
67
|
+
case "select":
|
|
68
|
+
return select(
|
|
69
|
+
node.cond,
|
|
70
|
+
differentiateRaw(node.then, varName),
|
|
71
|
+
differentiateRaw(node.else, varName),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(
|
|
76
|
+
`differentiate(): unexpected node type "${node.type}" -- ` +
|
|
77
|
+
`"let"/"outputs"/"field" must already be resolved by expandMacros/collectLets before differentiation`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function differentiateBin(node, varName) {
|
|
83
|
+
const { op, left, right } = node;
|
|
84
|
+
const dl = differentiateRaw(left, varName);
|
|
85
|
+
const dr = differentiateRaw(right, varName);
|
|
86
|
+
|
|
87
|
+
switch (op) {
|
|
88
|
+
// d/dx (f + g) = f' + g'
|
|
89
|
+
case "+":
|
|
90
|
+
return add(dl, dr);
|
|
91
|
+
|
|
92
|
+
// d/dx (f - g) = f' - g'
|
|
93
|
+
case "-":
|
|
94
|
+
return sub(dl, dr);
|
|
95
|
+
|
|
96
|
+
// Product rule: d/dx (f * g) = f' * g + f * g'
|
|
97
|
+
case "*":
|
|
98
|
+
return add(mul(dl, right), mul(left, dr));
|
|
99
|
+
|
|
100
|
+
// Quotient rule: d/dx (f / g) = (f' * g - f * g') / g²
|
|
101
|
+
case "/":
|
|
102
|
+
return div(
|
|
103
|
+
sub(mul(dl, right), mul(left, dr)),
|
|
104
|
+
mul(right, right),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
throw new Error(`differentiateBin(): unknown op "${op}"`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function differentiateCall(node, varName) {
|
|
113
|
+
const { name, args } = node;
|
|
114
|
+
|
|
115
|
+
if (NON_DIFFERENTIABLE.has(name)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`differentiate(): "${name}" is not differentiable (piecewise-constant/discontinuous)`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Unary primitives: chain rule is d/dx f(u) = f'(u) * u'
|
|
122
|
+
// where u = args[0] and u' = differentiate(args[0], varName).
|
|
123
|
+
//
|
|
124
|
+
// Binary primitives: chain rule is d/dx f(u, v) = (∂f/∂u * u' + ∂f/∂v * v')
|
|
125
|
+
// where partial derivatives are computed treating the other arg as constant.
|
|
126
|
+
|
|
127
|
+
const du = differentiateRaw(args[0], varName);
|
|
128
|
+
|
|
129
|
+
switch (name) {
|
|
130
|
+
// d/dx sqrt(u) = u' / (2 * sqrt(u))
|
|
131
|
+
case "sqrt":
|
|
132
|
+
return div(du, mul(num(2), call("sqrt", args[0])));
|
|
133
|
+
|
|
134
|
+
// d/dx abs(u) = u' * sign(u)
|
|
135
|
+
case "abs":
|
|
136
|
+
return mul(du, call("sign", args[0]));
|
|
137
|
+
|
|
138
|
+
// d/dx sin(u) = u' * cos(u)
|
|
139
|
+
case "sin":
|
|
140
|
+
return mul(du, call("cos", args[0]));
|
|
141
|
+
|
|
142
|
+
// d/dx cos(u) = -u' * sin(u)
|
|
143
|
+
case "cos":
|
|
144
|
+
return mul(neg(du), call("sin", args[0]));
|
|
145
|
+
|
|
146
|
+
// d/dx tan(u) = u' / cos²(u) = u' * (1 + tan²(u))
|
|
147
|
+
// Using 1/cos² form via sec² identity avoids needing a sec builtin.
|
|
148
|
+
case "tan":
|
|
149
|
+
return mul(du, add(num(1), mul(call("tan", args[0]), call("tan", args[0]))));
|
|
150
|
+
|
|
151
|
+
// d/dx asin(u) = u' / sqrt(1 - u²)
|
|
152
|
+
case "asin":
|
|
153
|
+
return div(du, call("sqrt", sub(num(1), mul(args[0], args[0]))));
|
|
154
|
+
|
|
155
|
+
// d/dx acos(u) = -u' / sqrt(1 - u²)
|
|
156
|
+
case "acos":
|
|
157
|
+
return div(neg(du), call("sqrt", sub(num(1), mul(args[0], args[0]))));
|
|
158
|
+
|
|
159
|
+
// d/dx atan(u) = u' / (1 + u²)
|
|
160
|
+
case "atan":
|
|
161
|
+
return div(du, add(num(1), mul(args[0], args[0])));
|
|
162
|
+
|
|
163
|
+
// d/dx log(u) = u' / u
|
|
164
|
+
case "log":
|
|
165
|
+
return div(du, args[0]);
|
|
166
|
+
|
|
167
|
+
// d/dx log2(u) = u' / (u * ln(2))
|
|
168
|
+
case "log2":
|
|
169
|
+
return div(du, mul(args[0], num(Math.LN2)));
|
|
170
|
+
|
|
171
|
+
// d/dx log10(u) = u' / (u * ln(10))
|
|
172
|
+
case "log10":
|
|
173
|
+
return div(du, mul(args[0], num(Math.LN10)));
|
|
174
|
+
|
|
175
|
+
// d/dx exp(u) = u' * exp(u)
|
|
176
|
+
case "exp":
|
|
177
|
+
return mul(du, call("exp", args[0]));
|
|
178
|
+
|
|
179
|
+
// d/dx pow(u, v) -- three cases:
|
|
180
|
+
// 1. v is constant: d/dx u^v = v * u^(v-1) * u' (power rule)
|
|
181
|
+
// 2. u is constant: d/dx c^v = c^v * ln(c) * v' (exponential rule)
|
|
182
|
+
// 3. Both vary: d/dx u^v = u^v * (v' * ln(u) + v * u'/u)
|
|
183
|
+
case "pow": {
|
|
184
|
+
const dv = differentiateRaw(args[1], varName);
|
|
185
|
+
const uIsConst = isConstant(args[0], varName);
|
|
186
|
+
const vIsConst = isConstant(args[1], varName);
|
|
187
|
+
|
|
188
|
+
if (vIsConst) {
|
|
189
|
+
// Power rule: v * u^(v-1) * u'
|
|
190
|
+
return mul(
|
|
191
|
+
mul(args[1], call("pow", args[0], sub(args[1], num(1)))),
|
|
192
|
+
du,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (uIsConst) {
|
|
196
|
+
// Exponential rule: c^v * ln(c) * v'
|
|
197
|
+
return mul(
|
|
198
|
+
mul(node, call("log", args[0])),
|
|
199
|
+
dv,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
// General: u^v * (v' * ln(u) + v * u'/u)
|
|
203
|
+
return mul(
|
|
204
|
+
node,
|
|
205
|
+
add(
|
|
206
|
+
mul(dv, call("log", args[0])),
|
|
207
|
+
mul(args[1], div(du, args[0])),
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// d/dx atan2(u, v) = (u' * v - u * v') / (u² + v²)
|
|
213
|
+
// Partial w.r.t. first arg (u): v / (u² + v²)
|
|
214
|
+
// Partial w.r.t. second arg (v): -u / (u² + v²)
|
|
215
|
+
case "atan2": {
|
|
216
|
+
const dv = differentiateRaw(args[1], varName);
|
|
217
|
+
const denom = add(mul(args[0], args[0]), mul(args[1], args[1]));
|
|
218
|
+
return div(
|
|
219
|
+
sub(mul(du, args[1]), mul(args[0], dv)),
|
|
220
|
+
denom,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// d/dx min(u, v):
|
|
225
|
+
// If u < v: derivative is du (min is u)
|
|
226
|
+
// If v < u: derivative is dv (min is v)
|
|
227
|
+
// If equal: undefined, but both branches evaluated anyway
|
|
228
|
+
case "min": {
|
|
229
|
+
const dv = differentiateRaw(args[1], varName);
|
|
230
|
+
return select(cmp(args[0], "<", args[1]), du, dv);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// d/dx max(u, v):
|
|
234
|
+
// If u > v: derivative is du (max is u)
|
|
235
|
+
// If v > u: derivative is dv (max is v)
|
|
236
|
+
case "max": {
|
|
237
|
+
const dv = differentiateRaw(args[1], varName);
|
|
238
|
+
return select(cmp(args[0], ">", args[1]), du, dv);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// d/dx hypot(u, v) = (u * u' + v * v') / hypot(u, v)
|
|
242
|
+
case "hypot": {
|
|
243
|
+
const dv = differentiateRaw(args[1], varName);
|
|
244
|
+
return div(
|
|
245
|
+
add(mul(args[0], du), mul(args[1], dv)),
|
|
246
|
+
call("hypot", args[0], args[1]),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
default:
|
|
251
|
+
throw new Error(`differentiate(): unknown call "${name}"`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check whether a node is a constant with respect to varName -- a num
|
|
256
|
+
// literal, or a var reference to anything OTHER than the differentiation
|
|
257
|
+
// variable. Doesn't recurse into subtrees -- if the node is a call/bin,
|
|
258
|
+
// it's not constant (even if all its leaves are). This is deliberately
|
|
259
|
+
// conservative: we only need to distinguish "definitely constant" (num,
|
|
260
|
+
// unrelated var) from "might not be" (everything else) for the pow()
|
|
261
|
+
// special cases.
|
|
262
|
+
function isConstant(node, varName) {
|
|
263
|
+
if (node.type === "num") return true;
|
|
264
|
+
if (node.type === "var") return node.name !== varName;
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Bottom-up algebraic simplification. Handles the expression swell that
|
|
269
|
+
// differentiation rules inevitably produce (terms like `* 0`, `+ 0`,
|
|
270
|
+
// `* 1`, `/ 1`, `^ 0`, `^ 1`). Runs to fixpoint — a single pass can
|
|
271
|
+
// create new simplifiable patterns (e.g. `0 * (x + 0)` → `0 * x` → `0`).
|
|
272
|
+
function simplify(node) {
|
|
273
|
+
if (!node || typeof node !== "object") return node;
|
|
274
|
+
|
|
275
|
+
// Recurse bottom-up first.
|
|
276
|
+
if (node.type === "bin") {
|
|
277
|
+
node = { ...node, left: simplify(node.left), right: simplify(node.right) };
|
|
278
|
+
} else if (node.type === "call") {
|
|
279
|
+
node = { ...node, args: node.args.map(simplify) };
|
|
280
|
+
} else if (node.type === "select") {
|
|
281
|
+
node = {
|
|
282
|
+
...node,
|
|
283
|
+
then: simplify(node.then),
|
|
284
|
+
else: simplify(node.else),
|
|
285
|
+
cond: { ...node.cond, left: simplify(node.cond.left), right: simplify(node.cond.right) },
|
|
286
|
+
};
|
|
287
|
+
} else {
|
|
288
|
+
return node; // num, var — nothing to simplify
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- bin node simplifications ---
|
|
292
|
+
if (node.type === "bin") {
|
|
293
|
+
const { op, left, right } = node;
|
|
294
|
+
|
|
295
|
+
// Constant folding: if both operands are num literals, evaluate.
|
|
296
|
+
if (left.type === "num" && right.type === "num") {
|
|
297
|
+
switch (op) {
|
|
298
|
+
case "+": return num(left.value + right.value);
|
|
299
|
+
case "-": return num(left.value - right.value);
|
|
300
|
+
case "*": return num(left.value * right.value);
|
|
301
|
+
case "/": return num(left.value / right.value);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (op === "+") {
|
|
306
|
+
if (left.type === "num" && left.value === 0) return right;
|
|
307
|
+
if (right.type === "num" && right.value === 0) return left;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (op === "-") {
|
|
311
|
+
if (right.type === "num" && right.value === 0) return left;
|
|
312
|
+
if (left.type === "num" && left.value === 0) {
|
|
313
|
+
// 0 - x → -(x): if x is a num, fold to negated literal
|
|
314
|
+
if (right.type === "num") return num(-right.value);
|
|
315
|
+
return mul(num(-1), right);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (op === "*") {
|
|
320
|
+
if (left.type === "num" && left.value === 0) return num(0);
|
|
321
|
+
if (right.type === "num" && right.value === 0) return num(0);
|
|
322
|
+
if (left.type === "num" && left.value === 1) return right;
|
|
323
|
+
if (right.type === "num" && right.value === 1) return left;
|
|
324
|
+
// -1 * x → negated
|
|
325
|
+
if (left.type === "num" && left.value === -1) return mul(num(-1), right);
|
|
326
|
+
if (right.type === "num" && right.value === -1) return mul(num(-1), left);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (op === "/") {
|
|
330
|
+
if (left.type === "num" && left.value === 0) return num(0);
|
|
331
|
+
if (right.type === "num" && right.value === 1) return left;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- call node simplifications ---
|
|
336
|
+
if (node.type === "call" && node.name === "pow" && node.args.length === 2) {
|
|
337
|
+
const [base, exp] = node.args;
|
|
338
|
+
if (exp.type === "num" && exp.value === 0) return num(1);
|
|
339
|
+
if (exp.type === "num" && exp.value === 1) return base;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return node;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = { differentiate };
|
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const { forComponents } = require("./util.js");
|
|
|
4
4
|
const { expr } = require("./expr.js");
|
|
5
5
|
const { fn } = require("./fn.js");
|
|
6
6
|
const { evaluate } = require("./evaluate.js");
|
|
7
|
+
const { differentiate } = require("./differentiate.js");
|
|
7
8
|
const { loadMacro, loadExtern, expandMacros, createRegistry } = require("./macros.js");
|
|
8
9
|
const { loadExpr, loadExprSource } = require("./load-expr.js");
|
|
9
10
|
const emitters = require("./emitters/registry.js");
|
|
@@ -132,6 +133,9 @@ module.exports = {
|
|
|
132
133
|
// A native interpreter over the AST -- evaluate(fn, args) computes a
|
|
133
134
|
// result directly in JS, no codegen/compile step. See evaluate.js.
|
|
134
135
|
evaluate,
|
|
136
|
+
// Symbolic differentiation: differentiate(node, varName) returns an
|
|
137
|
+
// AST node for the derivative. See differentiate.js.
|
|
138
|
+
differentiate,
|
|
135
139
|
// Register a macro: a name usable inside fn`...`/expr`...` text
|
|
136
140
|
// beyond the built-in primitives, inline-expanded at build time,
|
|
137
141
|
// never emitted as a real call. See macros.js's own header comment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exprforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Author a math expression once as an AST (or readable infix text via expr/fn), emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme, and COBOL, plus a native evaluator and its own readable syntax printer.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"expr.js",
|
|
17
17
|
"fn.js",
|
|
18
18
|
"evaluate.js",
|
|
19
|
+
"differentiate.js",
|
|
19
20
|
"macros.js",
|
|
20
21
|
"primitives.js",
|
|
21
22
|
"load-expr.js",
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"math/"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|
|
29
|
+
"dev": "npm run dev --prefix playground",
|
|
28
30
|
"build": "node build.js",
|
|
29
31
|
"test": "node --test",
|
|
30
32
|
"test:coverage": "node --test --experimental-test-coverage --test-coverage-exclude=\"test/**\"",
|
|
@@ -36,7 +38,9 @@
|
|
|
36
38
|
"math",
|
|
37
39
|
"cross-language",
|
|
38
40
|
"transpiler",
|
|
39
|
-
"qb64"
|
|
41
|
+
"qb64",
|
|
42
|
+
"differentiation",
|
|
43
|
+
"calculus"
|
|
40
44
|
],
|
|
41
45
|
"license": "MIT",
|
|
42
46
|
"author": {
|