@johnhenry/math 0.0.3 → 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/dist/Symbolic.js CHANGED
@@ -8,6 +8,7 @@ import { FUNC_DERIVATIVE_RULES } from "./differentiation-rules.js";
8
8
  import { MatrixMath } from "./MatrixMath.js";
9
9
  import { Numerical } from "./Numerical.js";
10
10
  import { Rational } from "./Rational.js";
11
+ import { compileShader } from "./Shader.js";
11
12
  import { SpecialFunctions } from "./SpecialFunctions.js";
12
13
  const FUNCS = [
13
14
  "sin",
@@ -323,13 +324,16 @@ export class InfiniteSolutionsError extends Error {
323
324
  }
324
325
  }
325
326
  export class Symbolic {
326
- /** Parse an expression string such as `"sin(x^2) + 3*x"` into an AST. */
327
- static parse(input) {
328
- return new Parser(input).parse();
327
+ /**
328
+ * Parse an expression string such as `"sin(x^2) + 3*x"` into an AST.
329
+ * Pass `{ implicitMultiplication: true }` to also read `2x`, `x y` and `2(x+1)`; see {@link ParseOptions}.
330
+ */
331
+ static parse(input, options = {}) {
332
+ return new Parser(input, options ?? {}).parse();
329
333
  }
330
334
  static differentiate(expr, variable = "x") {
331
335
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
332
- return Symbolic.simplify(diff(e, variable));
336
+ return simplifyCore(diff(e, variable));
333
337
  }
334
338
  /**
335
339
  * Like {@link differentiate}, but also returns a bottom-up trace of every
@@ -342,17 +346,20 @@ export class Symbolic {
342
346
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
343
347
  const steps = [];
344
348
  const raw = diffTraced(e, variable, steps);
345
- return { steps, result: Symbolic.simplify(raw) };
349
+ return { steps, result: simplifyCore(raw) };
346
350
  }
351
+ /**
352
+ * Algebraic simplification: constant folding, like-term collection, and, as a last
353
+ * step, expanding the expression as a rational function and cancelling common
354
+ * factors -- `(x+1)^2 - x^2 - 2*x` gives `1` and `(x^2-1)/(x-1)` gives `x+1`.
355
+ * The expanded/cancelled form is used only when it is strictly smaller than the
356
+ * collected form, so a product such as `(x+1)*2` keeps its shape. Non-polynomial
357
+ * subexpressions (`sin(x)`, `x^y`) are treated as opaque symbols. Like any CAS
358
+ * cancellation, `(x^2-1)/(x-1)` is `x+1` as an expression, ignoring that the
359
+ * original is undefined at `x = 1`.
360
+ */
347
361
  static simplify(expr) {
348
- let e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
349
- for (let i = 0; i < 30; i++) {
350
- const next = collectLikeTerms(simplifyOnce(e));
351
- if (equal(next, e))
352
- return next;
353
- e = next;
354
- }
355
- return e;
362
+ return normalizeRational(simplifyCore(expr));
356
363
  }
357
364
  /**
358
365
  * Simplifies `expr` with additional domain knowledge about specific
@@ -371,14 +378,14 @@ export class Symbolic {
371
378
  * assumptions (e.g. "x+y is positive" isn't expressible).
372
379
  */
373
380
  static simplifyAssuming(expr, assumptions) {
374
- const simplified = Symbolic.simplify(expr);
381
+ const simplified = simplifyCore(expr);
375
382
  const rewritten = applyAssumptions(simplified, assumptions);
376
- return Symbolic.simplify(rewritten);
383
+ return simplifyCore(rewritten);
377
384
  }
378
385
  /** Symbolic anti-derivative (elementary rules; throws {@link NotIntegrableError} otherwise). */
379
386
  static integrate(expr, variable = "x") {
380
387
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
381
- return Symbolic.simplify(integ(e, variable));
388
+ return simplifyCore(integ(e, variable));
382
389
  }
383
390
  /**
384
391
  * Definite integral of `expr` over `[lower, upper]` with respect to
@@ -410,13 +417,13 @@ export class Symbolic {
410
417
  * @throws {IntegrationSingularityError} if an interior singularity is detected.
411
418
  */
412
419
  static integrateDefinite(expr, lower, upper, variable = "x", env = {}) {
413
- const e = Symbolic.simplify(typeof expr === "string" ? Symbolic.parse(expr) : expr);
420
+ const e = simplifyCore(typeof expr === "string" ? Symbolic.parse(expr) : expr);
414
421
  const probe = compileExpr(e);
415
422
  if (hasInteriorSingularity((val) => probe({ ...env, [variable]: val }), lower, upper)) {
416
423
  throw new IntegrationSingularityError();
417
424
  }
418
425
  try {
419
- const F = Symbolic.simplify(integ(e, variable));
426
+ const F = simplifyCore(integ(e, variable));
420
427
  const at = (val) => evalExpr(F, { ...env, [variable]: val });
421
428
  return at(upper) - at(lower);
422
429
  }
@@ -471,7 +478,7 @@ export class Symbolic {
471
478
  * @throws {NoClosedFormError} if a method matches but its required integral(s) aren't elementary.
472
479
  */
473
480
  static solveOdeClosedForm(dyDxExpr, x0, y0, xVar = "x", yVar = "y") {
474
- const e = Symbolic.simplify(typeof dyDxExpr === "string" ? Symbolic.parse(dyDxExpr) : dyDxExpr);
481
+ const e = simplifyCore(typeof dyDxExpr === "string" ? Symbolic.parse(dyDxExpr) : dyDxExpr);
475
482
  const sep = matchSeparable(e, xVar, yVar);
476
483
  if (sep)
477
484
  return solveSeparableOde(sep, xVar, yVar, x0, y0);
@@ -580,20 +587,20 @@ export class Symbolic {
580
587
  }
581
588
  term = diff(term, variable);
582
589
  }
583
- return Symbolic.simplify(result);
590
+ return simplifyCore(result);
584
591
  }
585
592
  /** Replace every occurrence of a variable with a sub-expression (e.g. for changing variables or plugging in a solved value). */
586
593
  static substitute(expr, variable, replacement) {
587
594
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
588
595
  const r = typeof replacement === "string" ? Symbolic.parse(replacement) : replacement;
589
- return Symbolic.simplify(subst(e, variable, r));
596
+ return simplifyCore(subst(e, variable, r));
590
597
  }
591
598
  /** Distribute multiplication over addition/subtraction and expand small integer powers of sums, e.g. `(x+1)^2 -> x^2+2*x+1`. */
592
599
  static expand(expr) {
593
600
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
594
601
  let cur = e;
595
602
  for (let i = 0; i < 30; i++) {
596
- const next = Symbolic.simplify(expandOnce(cur));
603
+ const next = simplifyCore(expandOnce(cur));
597
604
  if (equal(next, cur))
598
605
  return next;
599
606
  cur = next;
@@ -680,6 +687,21 @@ export class Symbolic {
680
687
  static toString(expr) {
681
688
  return render(typeof expr === "string" ? Symbolic.parse(expr) : expr, 0);
682
689
  }
690
+ /**
691
+ * Compile `expr` to a GLSL ES 3.0 expression over `float` (default) or, with
692
+ * `{ complex: true }`, over `vec2` complex numbers (the helpers it needs, such as
693
+ * `mx_cmul`, are returned in `helpers` and emitted once). Output is deterministic.
694
+ * Refuses nodes with no shader form (`sum`, `product`, `erf`, `gcd`, `lcm`, and anything
695
+ * not closed under the complex numbers in complex mode) with a `ShaderUnsupportedError`.
696
+ * Shader `pow` of a negative base with a non-integer exponent is undefined, unlike JS `NaN`.
697
+ */
698
+ static toGLSL(expr, options = {}) {
699
+ return compileShader(typeof expr === "string" ? Symbolic.parse(expr) : expr, "glsl", options);
700
+ }
701
+ /** As {@link toGLSL}, for WGSL (`f32` / `vec2<f32>`; `select` replaces the ternary). */
702
+ static toWGSL(expr, options = {}) {
703
+ return compileShader(typeof expr === "string" ? Symbolic.parse(expr) : expr, "wgsl", options);
704
+ }
683
705
  /** Render `expr` as a LaTeX string, e.g. `"x/2"` -> `"\\frac{x}{2}"`. */
684
706
  static toLatex(expr) {
685
707
  return toLatexRec(typeof expr === "string" ? Symbolic.parse(expr) : expr, 0);
@@ -694,12 +716,16 @@ export class Symbolic {
694
716
  * commands (`\sin`, `\arcsin`, `\sinh`, `\operatorname{sech}`, ...), and the
695
717
  * two-argument `BinaryFuncName` commands (`\min`, `\max`, `\gcd`,
696
718
  * `\operatorname{atan2}`, `\operatorname{hypot}`, `\operatorname{lcm}`).
697
- * Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored.
719
+ * Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored. Single-token arguments as TeX
720
+ * reads them (`\frac12`, `\frac1x`, `\sqrt2`) and a power on a function command
721
+ * (`\sin^{2}(x)` is `sin(x)^2`) are accepted. An exponent of `-1` on a function command (`\sin^{-1}`) is
722
+ * refused as ambiguous (inverse function or reciprocal): write `\arcsin(x)` or `(\sin x)^{-1}`. For what people type into a math field
723
+ * (`2x`, `\sin 2x`, `(x+1)(x-1)`, `x^23`) pass `{ implicitMultiplication: true }`; see {@link ParseOptions}.
698
724
  *
699
725
  * @throws on LaTeX constructs with no `Expr` equivalent, e.g. `\int` or `\sum`.
700
726
  */
701
- static fromLatex(latex) {
702
- return Symbolic.parse(latexToInfix(latex));
727
+ static fromLatex(latex, options = {}) {
728
+ return Symbolic.parse(latexToInfix(latex, options), options);
703
729
  }
704
730
  /**
705
731
  * Solve `expr = 0` for `variable`, returning every *real* root @johnhenry/math
@@ -719,11 +745,11 @@ export class Symbolic {
719
745
  */
720
746
  static solve(expr, variable = "x") {
721
747
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
722
- const coeffs = polynomialCoeffs(Symbolic.simplify(e), variable, 6);
748
+ const coeffs = polynomialCoeffs(simplifyCore(e), variable, 6);
723
749
  if (!coeffs) {
724
750
  throw new Error(`solve: expression is not a polynomial in "${variable}" of degree ≤ 6.`);
725
751
  }
726
- return solvePolynomial(coeffs).map((r) => Symbolic.simplify(r));
752
+ return solvePolynomial(coeffs).map((r) => simplifyCore(r));
727
753
  }
728
754
  /**
729
755
  * Solve a system of linear equations, each written in the same "expr
@@ -740,7 +766,7 @@ export class Symbolic {
740
766
  * @throws {SingularSystemError} if the coefficient matrix is singular/rank-deficient.
741
767
  */
742
768
  static solveSystem(equations, variables) {
743
- const exprs = equations.map((eq) => Symbolic.simplify(typeof eq === "string" ? Symbolic.parse(eq) : eq));
769
+ const exprs = equations.map((eq) => simplifyCore(typeof eq === "string" ? Symbolic.parse(eq) : eq));
744
770
  if (exprs.length !== variables.length) {
745
771
  throw new Error(`solveSystem: expected ${variables.length} equations for ${variables.length} variables, got ${exprs.length}.`);
746
772
  }
@@ -948,7 +974,7 @@ export class Symbolic {
948
974
  */
949
975
  static factor(expr, variable = "x") {
950
976
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
951
- const simplified = Symbolic.simplify(e);
977
+ const simplified = simplifyCore(e);
952
978
  const coeffs = polynomialCoeffs(simplified, variable, 6);
953
979
  if (!coeffs)
954
980
  return simplified;
@@ -1132,6 +1158,17 @@ function diff(e, x) {
1132
1158
  }
1133
1159
  }
1134
1160
  // -- simplification ---------------------------------------------------------
1161
+ /** Constant folding and like-term collection to a fixed point -- no expansion or cancellation. */
1162
+ function simplifyCore(expr) {
1163
+ let e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
1164
+ for (let i = 0; i < 30; i++) {
1165
+ const next = collectLikeTerms(simplifyOnce(e));
1166
+ if (equal(next, e))
1167
+ return next;
1168
+ e = next;
1169
+ }
1170
+ return e;
1171
+ }
1135
1172
  function simplifyOnce(e) {
1136
1173
  if (e.type === "const" || e.type === "var")
1137
1174
  return e;
@@ -1876,7 +1913,7 @@ function freshVariableName(e, x) {
1876
1913
  * legitimately hit a domain error/NaN on either side).
1877
1914
  */
1878
1915
  function verifyByDifferentiation(candidate, e, x) {
1879
- const check = Symbolic.simplify(diff(candidate, x));
1916
+ const check = simplifyCore(diff(candidate, x));
1880
1917
  let agreements = 0;
1881
1918
  for (const p of [0.7, 1.3, -0.9]) {
1882
1919
  const expected = evalExpr(e, { [x]: p });
@@ -1902,10 +1939,10 @@ function verifyByDifferentiation(candidate, e, x) {
1902
1939
  */
1903
1940
  function tryUSubstitution(e, x) {
1904
1941
  for (const g of collectSubstitutionCandidates(e, x)) {
1905
- const gPrime = Symbolic.simplify(diff(g, x));
1942
+ const gPrime = simplifyCore(diff(g, x));
1906
1943
  if (isConst(gPrime, 0))
1907
1944
  continue;
1908
- const quotient = Symbolic.simplify(divideOutFactors(e, gPrime) ?? div(e, gPrime));
1945
+ const quotient = simplifyCore(divideOutFactors(e, gPrime) ?? div(e, gPrime));
1909
1946
  if (!containsExpr(quotient, g))
1910
1947
  continue;
1911
1948
  const uName = freshVariableName(quotient, x);
@@ -1914,7 +1951,7 @@ function tryUSubstitution(e, x) {
1914
1951
  continue;
1915
1952
  try {
1916
1953
  const antiderivativeInU = integRules(substituted, uName);
1917
- const candidate = Symbolic.simplify(substExpr(antiderivativeInU, v(uName), g));
1954
+ const candidate = simplifyCore(substExpr(antiderivativeInU, v(uName), g));
1918
1955
  if (verifyByDifferentiation(candidate, e, x))
1919
1956
  return candidate;
1920
1957
  }
@@ -2044,13 +2081,13 @@ function matchSeparable(e, xVar, yVar) {
2044
2081
  function matchLinearOde(e, xVar, yVar) {
2045
2082
  if (!containsVar(e, yVar))
2046
2083
  return null; // handled by matchSeparable's h(y)=1 case instead
2047
- const negP = Symbolic.simplify(diff(e, yVar));
2084
+ const negP = simplifyCore(diff(e, yVar));
2048
2085
  if (containsVar(negP, yVar))
2049
2086
  return null; // not affine in y
2050
- const q = Symbolic.simplify(sub(e, mul(negP, v(yVar))));
2087
+ const q = simplifyCore(sub(e, mul(negP, v(yVar))));
2051
2088
  if (containsVar(q, yVar))
2052
2089
  return null; // sanity check
2053
- const p = Symbolic.simplify(neg(negP));
2090
+ const p = simplifyCore(neg(negP));
2054
2091
  // Conservative, matching matchSeparable's philosophy: p/q must depend on
2055
2092
  // no free variable other than xVar (an extra symbolic parameter, or a
2056
2093
  // stray reference to yVar that survived simplification, both bail out).
@@ -2105,12 +2142,12 @@ function tryInvertForY(H, yVar, rhs, y0) {
2105
2142
  return null;
2106
2143
  }
2107
2144
  function solveSeparableOde(sep, xVar, yVar, x0, y0) {
2108
- const invH = Symbolic.simplify(div(num(1), sep.h));
2145
+ const invH = simplifyCore(div(num(1), sep.h));
2109
2146
  let H;
2110
2147
  let G;
2111
2148
  try {
2112
- H = Symbolic.simplify(integ(invH, yVar));
2113
- G = Symbolic.simplify(integ(sep.g, xVar));
2149
+ H = simplifyCore(integ(invH, yVar));
2150
+ G = simplifyCore(integ(sep.g, xVar));
2114
2151
  }
2115
2152
  catch (err) {
2116
2153
  if (err instanceof NotIntegrableError)
@@ -2118,20 +2155,20 @@ function solveSeparableOde(sep, xVar, yVar, x0, y0) {
2118
2155
  throw err;
2119
2156
  }
2120
2157
  const C = evalExpr(H, { [yVar]: y0 }) - evalExpr(G, { [xVar]: x0 });
2121
- const rhs = Symbolic.simplify(add(G, num(C)));
2158
+ const rhs = simplifyCore(add(G, num(C)));
2122
2159
  const explicitY = tryInvertForY(H, yVar, rhs, y0);
2123
2160
  if (explicitY)
2124
- return { explicit: true, y: Symbolic.simplify(explicitY) };
2125
- return { explicit: false, implicitRelation: Symbolic.simplify(sub(H, rhs)) };
2161
+ return { explicit: true, y: simplifyCore(explicitY) };
2162
+ return { explicit: false, implicitRelation: simplifyCore(sub(H, rhs)) };
2126
2163
  }
2127
2164
  function solveLinearOde(lin, xVar, _yVar, x0, y0) {
2128
2165
  let integralP;
2129
2166
  let mu;
2130
2167
  let muQIntegral;
2131
2168
  try {
2132
- integralP = Symbolic.simplify(integ(lin.p, xVar));
2133
- mu = Symbolic.simplify(fn("exp", integralP));
2134
- muQIntegral = Symbolic.simplify(integ(Symbolic.simplify(mul(mu, lin.q)), xVar));
2169
+ integralP = simplifyCore(integ(lin.p, xVar));
2170
+ mu = simplifyCore(fn("exp", integralP));
2171
+ muQIntegral = simplifyCore(integ(simplifyCore(mul(mu, lin.q)), xVar));
2135
2172
  }
2136
2173
  catch (err) {
2137
2174
  if (err instanceof NotIntegrableError)
@@ -2142,7 +2179,7 @@ function solveLinearOde(lin, xVar, _yVar, x0, y0) {
2142
2179
  const muAtX0 = evalExpr(mu, { [xVar]: x0 });
2143
2180
  const muQIntegralAtX0 = evalExpr(muQIntegral, { [xVar]: x0 });
2144
2181
  const C = y0 * muAtX0 - muQIntegralAtX0;
2145
- const y = Symbolic.simplify(div(add(muQIntegral, num(C)), mu));
2182
+ const y = simplifyCore(div(add(muQIntegral, num(C)), mu));
2146
2183
  return { explicit: true, y };
2147
2184
  }
2148
2185
  /** A variable name distinct from every name in `avoid` and not free in `e`, for a temporary substitution variable (like {@link freshVariableName}, but avoiding a whole list of names rather than just one). */
@@ -2198,7 +2235,7 @@ function matchHomogeneous(e, xVar, yVar) {
2198
2235
  }
2199
2236
  if (attempts === 0 || agreements < 4)
2200
2237
  return null; // not enough usable probes to trust the match
2201
- const F = Symbolic.simplify(subst(substituted, xVar, num(1)));
2238
+ const F = simplifyCore(subst(substituted, xVar, num(1)));
2202
2239
  return { vName, F };
2203
2240
  }
2204
2241
  /**
@@ -2223,15 +2260,15 @@ function matchHomogeneous(e, xVar, yVar) {
2223
2260
  function solveHomogeneousOde(hom, xVar, yVar, x0, y0) {
2224
2261
  const { vName, F } = hom;
2225
2262
  const v0 = y0 / x0;
2226
- const FMinusV = Symbolic.simplify(sub(F, v(vName)));
2263
+ const FMinusV = simplifyCore(sub(F, v(vName)));
2227
2264
  if (FMinusV.type === "const" && FMinusV.value === 0) {
2228
- return { explicit: true, y: Symbolic.simplify(mul(num(v0), v(xVar))) };
2265
+ return { explicit: true, y: simplifyCore(mul(num(v0), v(xVar))) };
2229
2266
  }
2230
2267
  const sepResult = solveSeparableOde({ g: div(num(1), v(xVar)), h: FMinusV }, xVar, vName, x0, v0);
2231
2268
  if (sepResult.explicit && sepResult.y) {
2232
- return { explicit: true, y: Symbolic.simplify(mul(sepResult.y, v(xVar))) };
2269
+ return { explicit: true, y: simplifyCore(mul(sepResult.y, v(xVar))) };
2233
2270
  }
2234
- const implicitRelation = Symbolic.simplify(subst(sepResult.implicitRelation, vName, div(v(yVar), v(xVar))));
2271
+ const implicitRelation = simplifyCore(subst(sepResult.implicitRelation, vName, div(v(yVar), v(xVar))));
2235
2272
  return { explicit: false, implicitRelation };
2236
2273
  }
2237
2274
  /**
@@ -2324,8 +2361,8 @@ function matchBernoulli(e, xVar, yVar) {
2324
2361
  }
2325
2362
  if (powMatch.n === 0)
2326
2363
  return null; // y^0=1: really just linear, matchLinearOde's job
2327
- const q = powSign === 1 ? powMatch.coef : Symbolic.simplify(neg(powMatch.coef));
2328
- const p = linSign === 1 ? Symbolic.simplify(neg(linMatch.coef)) : linMatch.coef;
2364
+ const q = powSign === 1 ? powMatch.coef : simplifyCore(neg(powMatch.coef));
2365
+ const p = linSign === 1 ? simplifyCore(neg(linMatch.coef)) : linMatch.coef;
2329
2366
  return { p, q, n: powMatch.n };
2330
2367
  }
2331
2368
  /**
@@ -2345,10 +2382,10 @@ function matchBernoulli(e, xVar, yVar) {
2345
2382
  function solveBernoulliOde(bern, xVar, yVar, x0, y0) {
2346
2383
  const oneMinusN = 1 - bern.n;
2347
2384
  const w0 = y0 ** oneMinusN;
2348
- const transformedP = Symbolic.simplify(mul(num(oneMinusN), bern.p));
2349
- const transformedQ = Symbolic.simplify(mul(num(oneMinusN), bern.q));
2385
+ const transformedP = simplifyCore(mul(num(oneMinusN), bern.p));
2386
+ const transformedQ = simplifyCore(mul(num(oneMinusN), bern.q));
2350
2387
  const wResult = solveLinearOde({ p: transformedP, q: transformedQ }, xVar, yVar, x0, w0);
2351
- const y = Symbolic.simplify(pow(wResult.y, num(1 / oneMinusN)));
2388
+ const y = simplifyCore(pow(wResult.y, num(1 / oneMinusN)));
2352
2389
  return { explicit: true, y };
2353
2390
  }
2354
2391
  /**
@@ -2371,7 +2408,7 @@ function matchExact(e, xVar, yVar) {
2371
2408
  if (![...vars].every((n) => n === xVar || n === yVar))
2372
2409
  return null;
2373
2410
  const M = e.left;
2374
- const N = Symbolic.simplify(neg(e.right));
2411
+ const N = simplifyCore(neg(e.right));
2375
2412
  const dMdy = diff(M, yVar);
2376
2413
  const dNdx = diff(N, xVar);
2377
2414
  let agreements = 0;
@@ -2421,8 +2458,8 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
2421
2458
  let g;
2422
2459
  let gPrime;
2423
2460
  try {
2424
- F0 = Symbolic.simplify(integ(M, xVar));
2425
- gPrime = Symbolic.simplify(sub(N, diff(F0, yVar)));
2461
+ F0 = simplifyCore(integ(M, xVar));
2462
+ gPrime = simplifyCore(sub(N, diff(F0, yVar)));
2426
2463
  if (containsVar(gPrime, xVar)) {
2427
2464
  // Symbolic cancellation fell short -- verify x-independence
2428
2465
  // numerically, then pin x to a probe value that evaluates finitely.
@@ -2435,21 +2472,21 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
2435
2472
  if (Math.abs(atProbe - atOther) > 1e-6 * Math.max(1, Math.abs(atProbe))) {
2436
2473
  throw new NoClosedFormError();
2437
2474
  }
2438
- pinned = Symbolic.simplify(subst(gPrime, xVar, num(xp)));
2475
+ pinned = simplifyCore(subst(gPrime, xVar, num(xp)));
2439
2476
  break;
2440
2477
  }
2441
2478
  if (!pinned)
2442
2479
  throw new NoClosedFormError();
2443
2480
  gPrime = pinned;
2444
2481
  }
2445
- g = Symbolic.simplify(integ(gPrime, yVar));
2482
+ g = simplifyCore(integ(gPrime, yVar));
2446
2483
  }
2447
2484
  catch (err) {
2448
2485
  if (err instanceof NotIntegrableError)
2449
2486
  throw new NoClosedFormError();
2450
2487
  throw err;
2451
2488
  }
2452
- const F = Symbolic.simplify(add(F0, g));
2489
+ const F = simplifyCore(add(F0, g));
2453
2490
  const C = evalExpr(F, { [xVar]: x0, [yVar]: y0 });
2454
2491
  if (!Number.isFinite(C))
2455
2492
  throw new NoClosedFormError();
@@ -2467,13 +2504,13 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
2467
2504
  continue;
2468
2505
  if (F.type === "sub" && yPart === b)
2469
2506
  continue; // F = G(x) - H(y): inverting -H is out of tryInvertForY's shapes
2470
- const rhs = Symbolic.simplify(sub(num(C), xPartSigned));
2507
+ const rhs = simplifyCore(sub(num(C), xPartSigned));
2471
2508
  const explicitY = tryInvertForY(yPart, yVar, rhs, y0);
2472
2509
  if (explicitY)
2473
- return { explicit: true, y: Symbolic.simplify(explicitY) };
2510
+ return { explicit: true, y: simplifyCore(explicitY) };
2474
2511
  }
2475
2512
  }
2476
- return { explicit: false, implicitRelation: Symbolic.simplify(sub(F, num(C))) };
2513
+ return { explicit: false, implicitRelation: simplifyCore(sub(F, num(C))) };
2477
2514
  }
2478
2515
  /** If `e` is `a·x + b` (a, b constant), return `{ a, b }`, else `null`. */
2479
2516
  function linearCoeffs(e, x) {
@@ -3345,11 +3382,54 @@ const CMP_LATEX_CMDS = {
3345
3382
  "\\ne": "!=",
3346
3383
  };
3347
3384
  /** Convert a LaTeX math string into the plain-infix syntax `Symbolic.parse`'s `Parser` understands. */
3348
- function latexToInfix(latex) {
3349
- const cleaned = latex.replace(/\\left|\\right/g, "").replace(/\\(?:,|;|!|quad|qquad| )/g, "");
3350
- return transformLatex(cleaned);
3385
+ function latexToInfix(latex, opts = {}) {
3386
+ const cleaned = latex.replace(/\\left|\\right/g, "").replace(/(?<!\\)\\(?:,|;|!|quad|qquad| )/g, "");
3387
+ return transformLatex(cleaned, opts);
3351
3388
  }
3352
- function transformLatex(s) {
3389
+ /**
3390
+ * Where the unbracketed argument of a function command ends under implicit multiplication: the longest
3391
+ * product of numbers, letters, `\\pi`, powers, subscripts and bracket groups starting at `start`
3392
+ * (`\\sin 2x` takes `2x`; `\\sin x\\cos x` takes just `x`). Returns `start` when there is none.
3393
+ */
3394
+ function bareArgumentEnd(s, start) {
3395
+ let j = start;
3396
+ const group = (at) => {
3397
+ const open = s[at];
3398
+ return findGroupEnd(s, at, open, open === "{" ? "}" : ")") + 1;
3399
+ };
3400
+ while (j < s.length) {
3401
+ const c = s[j];
3402
+ if (/[A-Za-z0-9.]/.test(c)) {
3403
+ j++;
3404
+ }
3405
+ else if (c === "(" || c === "{") {
3406
+ j = group(j);
3407
+ }
3408
+ else if (c === "_" && s[j + 1] === "{") {
3409
+ j = group(j + 1);
3410
+ }
3411
+ else if (c === "^") {
3412
+ if (s[j + 1] === "{")
3413
+ j = group(j + 1);
3414
+ else if (s[j + 1] === "\\")
3415
+ j += 1 + (/^\\[a-zA-Z]+/.exec(s.slice(j + 1))?.[0].length ?? 1);
3416
+ else
3417
+ j += 2;
3418
+ }
3419
+ else if (/^\\pi(?![a-zA-Z])/.test(s.slice(j))) {
3420
+ j += 3;
3421
+ }
3422
+ else if (c === " " && /^ +(?:[A-Za-z0-9.({]|\\pi(?![a-zA-Z]))/.test(s.slice(j))) {
3423
+ j += s.slice(j).length - s.slice(j).trimStart().length;
3424
+ }
3425
+ else {
3426
+ break;
3427
+ }
3428
+ }
3429
+ return j;
3430
+ }
3431
+ function transformLatex(s, opts = {}) {
3432
+ const implicit = opts.implicitMultiplication === true;
3353
3433
  let out = "";
3354
3434
  let i = 0;
3355
3435
  const readGroup = () => {
@@ -3360,11 +3440,21 @@ function transformLatex(s) {
3360
3440
  return content;
3361
3441
  };
3362
3442
  const readGroupOrParenOrToken = () => {
3443
+ while (s[i] === " ")
3444
+ i++;
3363
3445
  if (s[i] === "{" || s[i] === "(") {
3364
3446
  const { content, next } = extractGroup(s, i);
3365
3447
  i = next;
3366
3448
  return content;
3367
3449
  }
3450
+ if (implicit) {
3451
+ const end = bareArgumentEnd(s, i);
3452
+ if (end > i) {
3453
+ const run = s.slice(i, end).trim();
3454
+ i = end;
3455
+ return run;
3456
+ }
3457
+ }
3368
3458
  const m = /^[a-zA-Z_]\w*|^\d+\.?\d*(?:[eE][+-]?\d+)?/.exec(s.slice(i));
3369
3459
  if (m) {
3370
3460
  i += m[0].length;
@@ -3372,6 +3462,32 @@ function transformLatex(s) {
3372
3462
  }
3373
3463
  throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
3374
3464
  };
3465
+ /** A `{...}` group, or the one token TeX takes without braces: a digit, a letter or a `\\command`. */
3466
+ const readGroupOrToken = () => {
3467
+ while (s[i] === " ")
3468
+ i++;
3469
+ if (s[i] === "{")
3470
+ return readGroup();
3471
+ const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i));
3472
+ if (!m)
3473
+ throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
3474
+ i += m[0].length;
3475
+ return m[0];
3476
+ };
3477
+ /** A power written on a function command (`\\sin^{2}`, `\\sin^2`), or null. */
3478
+ const readFunctionPower = () => {
3479
+ if (s[i] !== "^")
3480
+ return null;
3481
+ i++;
3482
+ const power = readGroupOrToken();
3483
+ // `\sin^{-1}` is arcsin in most textbooks and calculators but 1/sin in strict TeX/notation; guessing
3484
+ // wrong would silently change the maths, so it is refused rather than interpreted.
3485
+ if (/^\s*-\s*1\s*$/.test(power)) {
3486
+ throw new Error("Ambiguous LaTeX: a function with exponent -1 (e.g. \\sin^{-1}) may mean the inverse function or the reciprocal; " +
3487
+ "write \\arcsin(x) for the inverse, or (\\sin x)^{-1} / 1/\\sin x for the reciprocal");
3488
+ }
3489
+ return power;
3490
+ };
3375
3491
  /** Read a `(...)`/`{...}` group holding exactly 2 comma-separated arguments, for `call2` functions. */
3376
3492
  const readArgPair = () => {
3377
3493
  if (s[i] !== "(" && s[i] !== "{")
@@ -3427,10 +3543,11 @@ function transformLatex(s) {
3427
3543
  if (condPart.trim() === "\\text{otherwise}") {
3428
3544
  if (otherwise !== null)
3429
3545
  throw new Error("\\begin{cases} may have only one \\text{otherwise} row");
3430
- otherwise = transformLatex(exprPart);
3546
+ otherwise = transformLatex(exprPart, opts);
3431
3547
  }
3432
3548
  else {
3433
- parts.push(`${transformLatex(condPart)}, ${transformLatex(exprPart)}`);
3549
+ const condition = condPart.replace(/^\s*\\text\{\s*(?:if|when)\s*\}/, ""); // "x & \text{if } x<0" as MathLive writes it
3550
+ parts.push(`${transformLatex(condition, opts)}, ${transformLatex(exprPart, opts)}`);
3434
3551
  }
3435
3552
  }
3436
3553
  if (otherwise === null) {
@@ -3439,29 +3556,29 @@ function transformLatex(s) {
3439
3556
  out += `piecewise(${parts.concat(otherwise).join(", ")})`;
3440
3557
  continue;
3441
3558
  }
3442
- if (cmd === "\\frac") {
3443
- const numerator = readGroup();
3444
- const denominator = readGroup();
3445
- out += `((${transformLatex(numerator)})/(${transformLatex(denominator)}))`;
3559
+ if (cmd === "\\frac" || cmd === "\\dfrac" || cmd === "\\tfrac") {
3560
+ const numerator = readGroupOrToken();
3561
+ const denominator = readGroupOrToken();
3562
+ out += `((${transformLatex(numerator, opts)})/(${transformLatex(denominator, opts)}))`;
3446
3563
  continue;
3447
3564
  }
3448
3565
  if (cmd === "\\sqrt") {
3449
3566
  if (s[i] === "[") {
3450
3567
  const { content: root, next } = extractGroup(s, i);
3451
3568
  i = next;
3452
- const arg = readGroup();
3453
- const transformedRoot = transformLatex(root).trim();
3569
+ const arg = readGroupOrToken();
3570
+ const transformedRoot = transformLatex(root, opts).trim();
3454
3571
  // \sqrt[3]{...} maps to cbrt(...) rather than a fractional power so
3455
3572
  // negative arguments evaluate correctly (Math.cbrt(-8) = -2, but
3456
3573
  // (-8)^(1/3) is NaN under JS's `**`).
3457
3574
  out +=
3458
3575
  transformedRoot === "3"
3459
- ? `cbrt(${transformLatex(arg)})`
3460
- : `((${transformLatex(arg)})^(1/(${transformedRoot})))`;
3576
+ ? `cbrt(${transformLatex(arg, opts)})`
3577
+ : `((${transformLatex(arg, opts)})^(1/(${transformedRoot})))`;
3461
3578
  }
3462
3579
  else {
3463
- const arg = readGroup();
3464
- out += `sqrt(${transformLatex(arg)})`;
3580
+ const arg = readGroupOrToken();
3581
+ out += `sqrt(${transformLatex(arg, opts)})`;
3465
3582
  }
3466
3583
  continue;
3467
3584
  }
@@ -3475,8 +3592,10 @@ function transformLatex(s) {
3475
3592
  if (base !== "10" && base !== "2") {
3476
3593
  throw new Error(`Unsupported LaTeX construct: \\log base ${base} (only base 10 and base 2 are supported)`);
3477
3594
  }
3595
+ const power = readFunctionPower();
3478
3596
  const arg = readGroupOrParenOrToken();
3479
- out += `${base === "2" ? "log2" : "log10"}(${transformLatex(arg)})`;
3597
+ const call = `${base === "2" ? "log2" : "log10"}(${transformLatex(arg, opts)})`;
3598
+ out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
3480
3599
  continue;
3481
3600
  }
3482
3601
  if (cmd === "\\lfloor" || cmd === "\\lceil") {
@@ -3486,7 +3605,7 @@ function transformLatex(s) {
3486
3605
  throw new Error(`Unmatched '${cmd}' in LaTeX source`);
3487
3606
  const inner = s.slice(i, closeIdx);
3488
3607
  i = closeIdx + closeCmd.length;
3489
- out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner)})`;
3608
+ out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner, opts)})`;
3490
3609
  continue;
3491
3610
  }
3492
3611
  if (cmd === "\\operatorname") {
@@ -3494,26 +3613,30 @@ function transformLatex(s) {
3494
3613
  const binaryName = OPERATORNAME_BINARY_FUNC_NAMES[opName];
3495
3614
  if (binaryName) {
3496
3615
  const [left, right] = readArgPair();
3497
- out += `${binaryName}(${transformLatex(left)}, ${transformLatex(right)})`;
3616
+ out += `${binaryName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
3498
3617
  continue;
3499
3618
  }
3500
3619
  const funcName = OPERATORNAME_FUNC_NAMES[opName];
3501
3620
  if (!funcName)
3502
3621
  throw new Error(`Unsupported LaTeX construct: \\operatorname{${opName}}`);
3622
+ const power = readFunctionPower();
3503
3623
  const arg = readGroupOrParenOrToken();
3504
- out += `${funcName}(${transformLatex(arg)})`;
3624
+ const call = `${funcName}(${transformLatex(arg, opts)})`;
3625
+ out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
3505
3626
  continue;
3506
3627
  }
3507
3628
  const binaryFuncName = LATEX_BINARY_FUNC_NAMES[cmd];
3508
3629
  if (binaryFuncName) {
3509
3630
  const [left, right] = readArgPair();
3510
- out += `${binaryFuncName}(${transformLatex(left)}, ${transformLatex(right)})`;
3631
+ out += `${binaryFuncName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
3511
3632
  continue;
3512
3633
  }
3513
3634
  const funcName = LATEX_FUNC_NAMES[cmd];
3514
3635
  if (funcName) {
3636
+ const power = readFunctionPower();
3515
3637
  const arg = readGroupOrParenOrToken();
3516
- out += `${funcName}(${transformLatex(arg)})`;
3638
+ const call = `${funcName}(${transformLatex(arg, opts)})`;
3639
+ out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
3517
3640
  continue;
3518
3641
  }
3519
3642
  throw new Error(`Unsupported LaTeX construct: ${cmd}`);
@@ -3524,13 +3647,20 @@ function transformLatex(s) {
3524
3647
  throw new Error("Unmatched '|' in LaTeX source");
3525
3648
  const inner = s.slice(i + 1, closeIdx);
3526
3649
  i = closeIdx + 1;
3527
- out += `abs(${transformLatex(inner)})`;
3650
+ out += `abs(${transformLatex(inner, opts)})`;
3651
+ continue;
3652
+ }
3653
+ if (implicit && ch === "^" && /^[a-zA-Z0-9]|^\\[a-zA-Z]/.test(s.slice(i + 1))) {
3654
+ // TeX reads `x^23` as `x^2` times `3`: the exponent is one token
3655
+ const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i + 1));
3656
+ i += 1 + m[0].length;
3657
+ out += `^(${transformLatex(m[0], opts)})`;
3528
3658
  continue;
3529
3659
  }
3530
3660
  if (ch === "^" && s[i + 1] === "{") {
3531
3661
  const { content, next } = extractGroup(s, i + 1);
3532
3662
  i = next;
3533
- out += `^(${transformLatex(content)})`;
3663
+ out += `^(${transformLatex(content, opts)})`;
3534
3664
  continue;
3535
3665
  }
3536
3666
  if (ch === "_" && s[i + 1] === "{") {
@@ -3542,7 +3672,7 @@ function transformLatex(s) {
3542
3672
  if (ch === "{") {
3543
3673
  const { content, next } = extractGroup(s, i);
3544
3674
  i = next;
3545
- out += `(${transformLatex(content)})`;
3675
+ out += `(${transformLatex(content, opts)})`;
3546
3676
  continue;
3547
3677
  }
3548
3678
  if (ch === "=") {
@@ -3558,6 +3688,378 @@ function transformLatex(s) {
3558
3688
  }
3559
3689
  return out;
3560
3690
  }
3691
+ const MAX_POLY_TERMS = 400;
3692
+ const MAX_RATIONAL_POWER = 8;
3693
+ class RatBail extends Error {
3694
+ }
3695
+ function atomIndex(ctx, e) {
3696
+ const key = render(e, 0);
3697
+ let i = ctx.keys.indexOf(key);
3698
+ if (i < 0) {
3699
+ i = ctx.keys.length;
3700
+ ctx.keys.push(key);
3701
+ ctx.atoms.push(e);
3702
+ }
3703
+ return i;
3704
+ }
3705
+ function expsCmp(a, b) {
3706
+ const n = Math.max(a.length, b.length);
3707
+ for (let i = 0; i < n; i++) {
3708
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
3709
+ if (d !== 0)
3710
+ return d;
3711
+ }
3712
+ return 0;
3713
+ }
3714
+ function expsKey(a) {
3715
+ let end = a.length;
3716
+ while (end > 0 && !a[end - 1])
3717
+ end--;
3718
+ return a.slice(0, end).join(",");
3719
+ }
3720
+ function polyNormalize(terms) {
3721
+ const byKey = new Map();
3722
+ for (const t of terms) {
3723
+ const k = expsKey(t.exps);
3724
+ const prev = byKey.get(k);
3725
+ if (prev)
3726
+ byKey.set(k, { exps: prev.exps, c: prev.c.add(t.c) });
3727
+ else
3728
+ byKey.set(k, { exps: t.exps, c: t.c });
3729
+ }
3730
+ const out = [...byKey.values()].filter((t) => !t.c.isZero());
3731
+ out.sort((a, b) => expsCmp(b.exps, a.exps));
3732
+ if (out.length > MAX_POLY_TERMS)
3733
+ throw new RatBail();
3734
+ return out;
3735
+ }
3736
+ const polyConst = (c) => (c.isZero() ? [] : [{ exps: [], c }]);
3737
+ const polyAdd = (a, b) => polyNormalize([...a, ...b]);
3738
+ const polyNeg = (a) => a.map((t) => ({ exps: t.exps, c: t.c.negate() }));
3739
+ const polySub = (a, b) => polyAdd(a, polyNeg(b));
3740
+ function polyMul(a, b) {
3741
+ if (a.length * b.length > MAX_POLY_TERMS * 20)
3742
+ throw new RatBail();
3743
+ const out = [];
3744
+ for (const x of a) {
3745
+ for (const y of b) {
3746
+ const n = Math.max(x.exps.length, y.exps.length);
3747
+ const exps = [];
3748
+ for (let i = 0; i < n; i++)
3749
+ exps.push((x.exps[i] ?? 0) + (y.exps[i] ?? 0));
3750
+ out.push({ exps, c: x.c.multiply(y.c) });
3751
+ }
3752
+ }
3753
+ return polyNormalize(out);
3754
+ }
3755
+ function polyEqual(a, b) {
3756
+ return a.length === b.length && a.every((t, i) => expsKey(t.exps) === expsKey(b[i].exps) && t.c.equals(b[i].c));
3757
+ }
3758
+ const polyIsConst = (a) => a.length === 0 || (a.length === 1 && expsKey(a[0].exps) === "");
3759
+ /** Division in lex order: `a = q*b + r` (`b` must be nonzero). */
3760
+ function polyDivMod(a, b) {
3761
+ const lead = b[0];
3762
+ let rem = a;
3763
+ const q = [];
3764
+ const r = [];
3765
+ let guard = 0;
3766
+ while (rem.length > 0) {
3767
+ if (++guard > 20_000)
3768
+ throw new RatBail();
3769
+ const t = rem[0];
3770
+ const n = Math.max(t.exps.length, lead.exps.length);
3771
+ const exps = [];
3772
+ let divisible = true;
3773
+ for (let i = 0; i < n; i++) {
3774
+ const d = (t.exps[i] ?? 0) - (lead.exps[i] ?? 0);
3775
+ if (d < 0)
3776
+ divisible = false;
3777
+ exps.push(d);
3778
+ }
3779
+ if (!divisible) {
3780
+ r.push(t);
3781
+ rem = rem.slice(1);
3782
+ continue;
3783
+ }
3784
+ const qt = { exps, c: t.c.divide(lead.c) };
3785
+ q.push(qt);
3786
+ rem = polySub(rem, polyMul([qt], b));
3787
+ }
3788
+ return { q: polyNormalize(q), r: polyNormalize(r) };
3789
+ }
3790
+ function polyAtoms(...ps) {
3791
+ const s = new Set();
3792
+ for (const p of ps) {
3793
+ for (const t of p) {
3794
+ for (let i = 0; i < t.exps.length; i++)
3795
+ if (t.exps[i] > 0)
3796
+ s.add(i);
3797
+ }
3798
+ }
3799
+ return s;
3800
+ }
3801
+ /** Monic GCD of two polynomials in one atom (Euclid); 1 when several atoms are involved. */
3802
+ function polyGcd(a, b) {
3803
+ if (polyAtoms(a, b).size > 1)
3804
+ return polyConst(Rational.One);
3805
+ let x = a;
3806
+ let y = b;
3807
+ while (y.length > 0) {
3808
+ const { r } = polyDivMod(x, y);
3809
+ x = y;
3810
+ y = r;
3811
+ }
3812
+ if (x.length === 0)
3813
+ return polyConst(Rational.One);
3814
+ const lc = x[0].c;
3815
+ return x.map((t) => ({ exps: t.exps, c: t.c.divide(lc) }));
3816
+ }
3817
+ /** The largest monomial dividing every term (exponent-wise minimum). */
3818
+ function monomialContent(p) {
3819
+ if (p.length === 0)
3820
+ return [];
3821
+ const n = Math.max(...p.map((t) => t.exps.length));
3822
+ const out = [];
3823
+ for (let i = 0; i < n; i++)
3824
+ out.push(Math.min(...p.map((t) => t.exps[i] ?? 0)));
3825
+ return out;
3826
+ }
3827
+ function polyDivMonomial(p, m) {
3828
+ return p.map((t) => ({ exps: t.exps.map((e, i) => e - (m[i] ?? 0)), c: t.c }));
3829
+ }
3830
+ const ratioFromPoly = (n) => ({ n, d: polyConst(Rational.One) });
3831
+ function ratioAdd(a, b, subtract) {
3832
+ const bn = subtract ? polyNeg(b.n) : b.n;
3833
+ if (polyEqual(a.d, b.d))
3834
+ return { n: polyAdd(a.n, bn), d: a.d };
3835
+ return { n: polyAdd(polyMul(a.n, b.d), polyMul(bn, a.d)), d: polyMul(a.d, b.d) };
3836
+ }
3837
+ const ratioMul = (a, b) => ({ n: polyMul(a.n, b.n), d: polyMul(a.d, b.d) });
3838
+ function ratioDiv(a, b) {
3839
+ if (b.n.length === 0)
3840
+ throw new RatBail(); // division by zero: leave it to evaluation
3841
+ return { n: polyMul(a.n, b.d), d: polyMul(a.d, b.n) };
3842
+ }
3843
+ function ratioPow(a, n) {
3844
+ const k = Math.abs(n);
3845
+ if (k > 1000)
3846
+ throw new RatBail();
3847
+ const singleTerm = a.n.length === 1 && a.d.length === 1;
3848
+ if (!singleTerm && k > MAX_RATIONAL_POWER)
3849
+ throw new RatBail();
3850
+ let result;
3851
+ if (singleTerm) {
3852
+ const mono = (t) => ({ exps: t.exps.map((e) => e * k), c: t.c.pow(k) });
3853
+ result = { n: [mono(a.n[0])], d: [mono(a.d[0])] };
3854
+ }
3855
+ else {
3856
+ result = ratioFromPoly(polyConst(Rational.One));
3857
+ for (let i = 0; i < k; i++)
3858
+ result = ratioMul(result, a);
3859
+ }
3860
+ return n < 0 ? ratioDiv(ratioFromPoly(polyConst(Rational.One)), result) : result;
3861
+ }
3862
+ function exactRational(value) {
3863
+ if (!Number.isFinite(value))
3864
+ return null;
3865
+ if (Number.isInteger(value))
3866
+ return new Rational(BigInt(value));
3867
+ const r = Rational.fromNumber(value, 10_000);
3868
+ return r.toNumber() === value ? r : null;
3869
+ }
3870
+ function toRatio(e, ctx) {
3871
+ const atom = (x) => {
3872
+ const i = atomIndex(ctx, x);
3873
+ const exps = new Array(i + 1).fill(0);
3874
+ exps[i] = 1;
3875
+ return ratioFromPoly([{ exps, c: Rational.One }]);
3876
+ };
3877
+ switch (e.type) {
3878
+ case "const": {
3879
+ const r = exactRational(e.value);
3880
+ return r ? ratioFromPoly(polyConst(r)) : atom(e);
3881
+ }
3882
+ case "var":
3883
+ return atom(e);
3884
+ case "neg": {
3885
+ const a = toRatio(e.arg, ctx);
3886
+ return { n: polyNeg(a.n), d: a.d };
3887
+ }
3888
+ case "add":
3889
+ return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), false);
3890
+ case "sub":
3891
+ return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), true);
3892
+ case "mul":
3893
+ return ratioMul(toRatio(e.left, ctx), toRatio(e.right, ctx));
3894
+ case "div":
3895
+ return ratioDiv(toRatio(e.left, ctx), toRatio(e.right, ctx));
3896
+ case "pow": {
3897
+ if (e.exp.type === "const" && Number.isInteger(e.exp.value)) {
3898
+ return ratioPow(toRatio(e.base, ctx), e.exp.value);
3899
+ }
3900
+ return atom(pow(normalizeRational(e.base), normalizeRational(e.exp)));
3901
+ }
3902
+ case "func":
3903
+ return atom(fn(e.name, normalizeRational(e.arg)));
3904
+ case "call2":
3905
+ return atom(call2(e.name, normalizeRational(e.left), normalizeRational(e.right)));
3906
+ case "cmp":
3907
+ return atom(cmp(e.op, normalizeRational(e.left), normalizeRational(e.right)));
3908
+ case "piecewise":
3909
+ return atom(piecewise(e.branches.map((br) => ({ cond: normalizeRational(br.cond), expr: normalizeRational(br.expr) })), normalizeRational(e.otherwise)));
3910
+ case "sum":
3911
+ return atom(sumExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
3912
+ case "product":
3913
+ return atom(productExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
3914
+ }
3915
+ }
3916
+ function bigGcd(a, b) {
3917
+ let x = a < 0n ? -a : a;
3918
+ let y = b < 0n ? -b : b;
3919
+ while (y)
3920
+ [x, y] = [y, x % y];
3921
+ return x;
3922
+ }
3923
+ /** Cancel common factors, then scale to coprime integer coefficients with a positive denominator lead. */
3924
+ function ratioReduce(r) {
3925
+ if (r.n.length === 0)
3926
+ return ratioFromPoly([]);
3927
+ let { n, d } = r;
3928
+ const m = monomialContent([...n, ...d]);
3929
+ if (m.some((e) => e > 0)) {
3930
+ n = polyDivMonomial(n, m);
3931
+ d = polyDivMonomial(d, m);
3932
+ }
3933
+ if (!polyIsConst(d)) {
3934
+ const g = polyGcd(n, d);
3935
+ if (!polyIsConst(g)) {
3936
+ n = polyDivMod(n, g).q;
3937
+ d = polyDivMod(d, g).q;
3938
+ }
3939
+ else {
3940
+ // Several atoms: still cancel when one side divides the other exactly.
3941
+ const dn = polyDivMod(n, d);
3942
+ if (dn.r.length === 0) {
3943
+ n = dn.q;
3944
+ d = polyConst(Rational.One);
3945
+ }
3946
+ else {
3947
+ const nd = polyDivMod(d, n);
3948
+ if (nd.r.length === 0) {
3949
+ d = nd.q;
3950
+ n = polyConst(Rational.One);
3951
+ }
3952
+ }
3953
+ }
3954
+ }
3955
+ if (polyIsConst(d)) {
3956
+ const c = d[0].c;
3957
+ return { n: n.map((t) => ({ exps: t.exps, c: t.c.divide(c) })), d: polyConst(Rational.One) };
3958
+ }
3959
+ // integer coefficients: multiply through by the lcm of the denominators, divide by the content
3960
+ let lcm = 1n;
3961
+ for (const t of [...n, ...d]) {
3962
+ const den = t.c.denominator;
3963
+ lcm = (lcm / bigGcd(lcm, den)) * den;
3964
+ }
3965
+ const scale = new Rational(lcm);
3966
+ n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
3967
+ d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
3968
+ let content = 0n;
3969
+ for (const t of [...n, ...d])
3970
+ content = bigGcd(content, t.c.numerator);
3971
+ if (content > 1n) {
3972
+ const inv = new Rational(1n, content);
3973
+ n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
3974
+ d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
3975
+ }
3976
+ if (d[0].c.sign < 0) {
3977
+ n = polyNeg(n);
3978
+ d = polyNeg(d);
3979
+ }
3980
+ return { n, d };
3981
+ }
3982
+ function monomialExpr(exps, ctx) {
3983
+ let out = null;
3984
+ for (let i = 0; i < exps.length; i++) {
3985
+ const e = exps[i] ?? 0;
3986
+ if (e === 0)
3987
+ continue;
3988
+ const factor = e === 1 ? ctx.atoms[i] : pow(ctx.atoms[i], num(e));
3989
+ out = out ? mul(out, factor) : factor;
3990
+ }
3991
+ return out;
3992
+ }
3993
+ /** A polynomial as a descending-degree sum; fractional coefficients print as `p*m/q`. */
3994
+ function polyToTree(p, ctx) {
3995
+ if (p.length === 0)
3996
+ return num(0);
3997
+ const ordered = [...p].sort((a, b) => {
3998
+ const da = a.exps.reduce((s, x) => s + x, 0);
3999
+ const db = b.exps.reduce((s, x) => s + x, 0);
4000
+ return db - da || expsCmp(b.exps, a.exps);
4001
+ });
4002
+ let out = null;
4003
+ for (const t of ordered) {
4004
+ const negative = t.c.sign < 0;
4005
+ const abs = t.c.abs();
4006
+ const mono = monomialExpr(t.exps, ctx);
4007
+ const numer = abs.numerator;
4008
+ let body;
4009
+ if (!mono)
4010
+ body = num(abs.toNumber());
4011
+ else if (numer === 1n && abs.denominator === 1n)
4012
+ body = mono;
4013
+ else if (abs.denominator === 1n)
4014
+ body = mul(num(Number(numer)), mono);
4015
+ else
4016
+ body = div(numer === 1n ? mono : mul(num(Number(numer)), mono), num(Number(abs.denominator)));
4017
+ if (!out)
4018
+ out = negative ? neg(body) : body;
4019
+ else
4020
+ out = negative ? sub(out, body) : add(out, body);
4021
+ }
4022
+ return out;
4023
+ }
4024
+ function nodeCount(e) {
4025
+ switch (e.type) {
4026
+ case "const":
4027
+ case "var":
4028
+ return 1;
4029
+ case "neg":
4030
+ case "func":
4031
+ return 1 + nodeCount(e.arg);
4032
+ case "pow":
4033
+ return 1 + nodeCount(e.base) + nodeCount(e.exp);
4034
+ case "piecewise":
4035
+ return 1 + nodeCount(e.otherwise) + e.branches.reduce((s, br) => s + nodeCount(br.cond) + nodeCount(br.expr), 0);
4036
+ case "sum":
4037
+ case "product":
4038
+ return 1 + nodeCount(e.from) + nodeCount(e.to) + nodeCount(e.body);
4039
+ default:
4040
+ return 1 + nodeCount(e.left) + nodeCount(e.right);
4041
+ }
4042
+ }
4043
+ /**
4044
+ * Expand `e` as a rational function and cancel common factors, returning the result only
4045
+ * when it has strictly fewer nodes than `e`; otherwise returns `e` itself.
4046
+ */
4047
+ function normalizeRational(e) {
4048
+ if (e.type === "const" || e.type === "var")
4049
+ return e;
4050
+ try {
4051
+ const ctx = { atoms: [], keys: [] };
4052
+ const reduced = ratioReduce(toRatio(e, ctx));
4053
+ const numer = polyToTree(reduced.n, ctx);
4054
+ const candidate = polyIsConst(reduced.d) ? numer : div(numer, polyToTree(reduced.d, ctx));
4055
+ return nodeCount(candidate) < nodeCount(e) ? candidate : e;
4056
+ }
4057
+ catch (err) {
4058
+ if (err instanceof RatBail)
4059
+ return e;
4060
+ throw err;
4061
+ }
4062
+ }
3561
4063
  // -- equation solving & factoring ----------------------------------------------
3562
4064
  /** Best-effort exact `Expr` for a numeric value: an integer, a reduced fraction, or (as a last resort) the raw float. */
3563
4065
  function toExactExpr(value) {
@@ -3591,7 +4093,7 @@ function linearCoeffsForSystem(eq, variables) {
3591
4093
  return null;
3592
4094
  const coeffs = [];
3593
4095
  for (const name of variables) {
3594
- const derivative = Symbolic.simplify(diff(eq, name));
4096
+ const derivative = simplifyCore(diff(eq, name));
3595
4097
  if (variables.some((other) => containsVar(derivative, other)))
3596
4098
  return null;
3597
4099
  const coeff = evalExpr(derivative, zeroEnv);
@@ -3623,7 +4125,7 @@ function polynomialCoeffs(expr, variable, maxDegree) {
3623
4125
  // derivative of x^0 (`0·x^-1`) evaluates to `0·Infinity = NaN` at x=0
3624
4126
  // even though it is symbolically zero — simplifying collapses `0·anything`
3625
4127
  // to `0` before that indeterminate form can arise.
3626
- term = Symbolic.simplify(diff(term, variable));
4128
+ term = simplifyCore(diff(term, variable));
3627
4129
  }
3628
4130
  while (coeffs.length > 1 && Math.abs(coeffs[coeffs.length - 1]) < 1e-9)
3629
4131
  coeffs.pop();
@@ -3818,7 +4320,7 @@ function factorPolynomial(coeffsIn, variable) {
3818
4320
  else if (Math.abs(remaining[0] - 1) > 1e-9) {
3819
4321
  result = mul(result, toExactExpr(remaining[0]));
3820
4322
  }
3821
- return Symbolic.simplify(result);
4323
+ return simplifyCore(result);
3822
4324
  }
3823
4325
  // -- limits ---------------------------------------------------------------------
3824
4326
  function evalNear(e, x, point, direction) {
@@ -3842,19 +4344,25 @@ function limitAt(e, x, point, direction, depth) {
3842
4344
  // operand verbatim into the result) and a limit needing more than a
3843
4345
  // couple of rounds (e.g. most limits at infinity) blows up memory
3844
4346
  // long before the depth<12 cap is reached.
3845
- const df = Symbolic.simplify(diff(e.left, x));
3846
- const dg = Symbolic.simplify(diff(e.right, x));
4347
+ const df = simplifyCore(diff(e.left, x));
4348
+ const dg = simplifyCore(diff(e.right, x));
3847
4349
  return limitAt(div(df, dg), x, point, direction, depth + 1);
3848
4350
  }
3849
4351
  }
3850
4352
  return direct;
3851
4353
  }
3852
4354
  // -- recursive-descent parser ----------------------------------------------
4355
+ /** Every name the parser treats as a function, longest first, so `asinh(` is not read as `a*sinh(`. */
4356
+ const FUNCTION_NAMES_LONGEST_FIRST = [...FUNCTION_NAMES].sort((a, b) => b.length - a.length);
3853
4357
  class Parser {
3854
4358
  s;
4359
+ implicit;
3855
4360
  pos = 0;
3856
- constructor(input) {
3857
- this.s = input.replace(/\s+/g, "");
4361
+ constructor(input, options = {}) {
4362
+ this.implicit = options.implicitMultiplication === true;
4363
+ // With implicit multiplication, white space between two numbers is a product (`2 3`), not one number.
4364
+ const spaced = this.implicit ? input.replace(/(\d)\s+(?=[\d.])/g, "$1*") : input;
4365
+ this.s = spaced.replace(/\s+/g, "");
3858
4366
  }
3859
4367
  parse() {
3860
4368
  const e = this.comparison();
@@ -3973,12 +4481,38 @@ class Parser {
3973
4481
  }
3974
4482
  term() {
3975
4483
  let left = this.factor();
3976
- while (this.peek() === "*" || this.peek() === "/") {
3977
- const op = this.s[this.pos++];
3978
- const right = this.factor();
3979
- left = op === "*" ? mul(left, right) : div(left, right);
4484
+ for (;;) {
4485
+ const ch = this.peek();
4486
+ if (ch === "*" || ch === "/") {
4487
+ this.pos++;
4488
+ const right = this.factor();
4489
+ left = ch === "*" ? mul(left, right) : div(left, right);
4490
+ }
4491
+ else if (this.implicit && /[a-zA-Z0-9.(]/.test(ch)) {
4492
+ left = mul(left, this.factor()); // juxtaposition: `2x`, `x y`, `2(x+1)`
4493
+ }
4494
+ else {
4495
+ return left;
4496
+ }
3980
4497
  }
3981
- return left;
4498
+ }
4499
+ /**
4500
+ * The identifier at `this.pos` under implicit multiplication: a function name followed by `(`, `pi`, or a
4501
+ * single letter with an optional `_subscript`. A letter run like `xy` is therefore read one letter at a time.
4502
+ */
4503
+ implicitIdentifier() {
4504
+ const rest = this.s.slice(this.pos);
4505
+ const run = /^[a-zA-Z]+/.exec(rest)?.[0] ?? "";
4506
+ const call = FUNCTION_NAMES_LONGEST_FIRST.find((f) => rest.startsWith(f) && rest[f.length] === "(");
4507
+ if (call)
4508
+ return call;
4509
+ const bare = FUNCTION_NAMES_LONGEST_FIRST.find((f) => f.length > 1 && run.startsWith(f));
4510
+ if (bare)
4511
+ throw new Error(`Function ${bare} needs parentheses: ${bare}(...)`);
4512
+ if (rest.startsWith("pi"))
4513
+ return "pi";
4514
+ const sub = /^[a-zA-Z]_(?:\d+|[a-zA-Z])/.exec(rest);
4515
+ return sub ? sub[0] : rest[0];
3982
4516
  }
3983
4517
  factor() {
3984
4518
  if (this.peek() === "-") {
@@ -4016,9 +4550,11 @@ class Parser {
4016
4550
  return num(Number(numMatch[0]));
4017
4551
  }
4018
4552
  // identifier (function or variable or constant)
4019
- const idMatch = /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
4553
+ const idMatch = this.implicit
4554
+ ? /^[a-zA-Z_]/.exec(this.s.slice(this.pos))
4555
+ : /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
4020
4556
  if (idMatch) {
4021
- const name = idMatch[0];
4557
+ const name = this.implicit ? this.implicitIdentifier() : idMatch[0];
4022
4558
  this.pos += name.length;
4023
4559
  if ((name === "log" || name === "clamp") && this.peek() === "(") {
4024
4560
  this.pos++;