@johnhenry/math 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Symbolic.js CHANGED
@@ -323,13 +323,16 @@ export class InfiniteSolutionsError extends Error {
323
323
  }
324
324
  }
325
325
  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();
326
+ /**
327
+ * Parse an expression string such as `"sin(x^2) + 3*x"` into an AST.
328
+ * Pass `{ implicitMultiplication: true }` to also read `2x`, `x y` and `2(x+1)`; see {@link ParseOptions}.
329
+ */
330
+ static parse(input, options = {}) {
331
+ return new Parser(input, options ?? {}).parse();
329
332
  }
330
333
  static differentiate(expr, variable = "x") {
331
334
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
332
- return Symbolic.simplify(diff(e, variable));
335
+ return simplifyCore(diff(e, variable));
333
336
  }
334
337
  /**
335
338
  * Like {@link differentiate}, but also returns a bottom-up trace of every
@@ -342,17 +345,20 @@ export class Symbolic {
342
345
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
343
346
  const steps = [];
344
347
  const raw = diffTraced(e, variable, steps);
345
- return { steps, result: Symbolic.simplify(raw) };
348
+ return { steps, result: simplifyCore(raw) };
346
349
  }
350
+ /**
351
+ * Algebraic simplification: constant folding, like-term collection, and, as a last
352
+ * step, expanding the expression as a rational function and cancelling common
353
+ * factors -- `(x+1)^2 - x^2 - 2*x` gives `1` and `(x^2-1)/(x-1)` gives `x+1`.
354
+ * The expanded/cancelled form is used only when it is strictly smaller than the
355
+ * collected form, so a product such as `(x+1)*2` keeps its shape. Non-polynomial
356
+ * subexpressions (`sin(x)`, `x^y`) are treated as opaque symbols. Like any CAS
357
+ * cancellation, `(x^2-1)/(x-1)` is `x+1` as an expression, ignoring that the
358
+ * original is undefined at `x = 1`.
359
+ */
347
360
  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;
361
+ return normalizeRational(simplifyCore(expr));
356
362
  }
357
363
  /**
358
364
  * Simplifies `expr` with additional domain knowledge about specific
@@ -371,14 +377,14 @@ export class Symbolic {
371
377
  * assumptions (e.g. "x+y is positive" isn't expressible).
372
378
  */
373
379
  static simplifyAssuming(expr, assumptions) {
374
- const simplified = Symbolic.simplify(expr);
380
+ const simplified = simplifyCore(expr);
375
381
  const rewritten = applyAssumptions(simplified, assumptions);
376
- return Symbolic.simplify(rewritten);
382
+ return simplifyCore(rewritten);
377
383
  }
378
384
  /** Symbolic anti-derivative (elementary rules; throws {@link NotIntegrableError} otherwise). */
379
385
  static integrate(expr, variable = "x") {
380
386
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
381
- return Symbolic.simplify(integ(e, variable));
387
+ return simplifyCore(integ(e, variable));
382
388
  }
383
389
  /**
384
390
  * Definite integral of `expr` over `[lower, upper]` with respect to
@@ -410,13 +416,13 @@ export class Symbolic {
410
416
  * @throws {IntegrationSingularityError} if an interior singularity is detected.
411
417
  */
412
418
  static integrateDefinite(expr, lower, upper, variable = "x", env = {}) {
413
- const e = Symbolic.simplify(typeof expr === "string" ? Symbolic.parse(expr) : expr);
419
+ const e = simplifyCore(typeof expr === "string" ? Symbolic.parse(expr) : expr);
414
420
  const probe = compileExpr(e);
415
421
  if (hasInteriorSingularity((val) => probe({ ...env, [variable]: val }), lower, upper)) {
416
422
  throw new IntegrationSingularityError();
417
423
  }
418
424
  try {
419
- const F = Symbolic.simplify(integ(e, variable));
425
+ const F = simplifyCore(integ(e, variable));
420
426
  const at = (val) => evalExpr(F, { ...env, [variable]: val });
421
427
  return at(upper) - at(lower);
422
428
  }
@@ -471,7 +477,7 @@ export class Symbolic {
471
477
  * @throws {NoClosedFormError} if a method matches but its required integral(s) aren't elementary.
472
478
  */
473
479
  static solveOdeClosedForm(dyDxExpr, x0, y0, xVar = "x", yVar = "y") {
474
- const e = Symbolic.simplify(typeof dyDxExpr === "string" ? Symbolic.parse(dyDxExpr) : dyDxExpr);
480
+ const e = simplifyCore(typeof dyDxExpr === "string" ? Symbolic.parse(dyDxExpr) : dyDxExpr);
475
481
  const sep = matchSeparable(e, xVar, yVar);
476
482
  if (sep)
477
483
  return solveSeparableOde(sep, xVar, yVar, x0, y0);
@@ -580,20 +586,20 @@ export class Symbolic {
580
586
  }
581
587
  term = diff(term, variable);
582
588
  }
583
- return Symbolic.simplify(result);
589
+ return simplifyCore(result);
584
590
  }
585
591
  /** Replace every occurrence of a variable with a sub-expression (e.g. for changing variables or plugging in a solved value). */
586
592
  static substitute(expr, variable, replacement) {
587
593
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
588
594
  const r = typeof replacement === "string" ? Symbolic.parse(replacement) : replacement;
589
- return Symbolic.simplify(subst(e, variable, r));
595
+ return simplifyCore(subst(e, variable, r));
590
596
  }
591
597
  /** Distribute multiplication over addition/subtraction and expand small integer powers of sums, e.g. `(x+1)^2 -> x^2+2*x+1`. */
592
598
  static expand(expr) {
593
599
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
594
600
  let cur = e;
595
601
  for (let i = 0; i < 30; i++) {
596
- const next = Symbolic.simplify(expandOnce(cur));
602
+ const next = simplifyCore(expandOnce(cur));
597
603
  if (equal(next, cur))
598
604
  return next;
599
605
  cur = next;
@@ -694,12 +700,16 @@ export class Symbolic {
694
700
  * commands (`\sin`, `\arcsin`, `\sinh`, `\operatorname{sech}`, ...), and the
695
701
  * two-argument `BinaryFuncName` commands (`\min`, `\max`, `\gcd`,
696
702
  * `\operatorname{atan2}`, `\operatorname{hypot}`, `\operatorname{lcm}`).
697
- * Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored.
703
+ * Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored. Single-token arguments as TeX
704
+ * reads them (`\frac12`, `\frac1x`, `\sqrt2`) and a power on a function command
705
+ * (`\sin^{2}(x)` is `sin(x)^2`) are accepted. An exponent of `-1` on a function command (`\sin^{-1}`) is
706
+ * refused as ambiguous (inverse function or reciprocal): write `\arcsin(x)` or `(\sin x)^{-1}`. For what people type into a math field
707
+ * (`2x`, `\sin 2x`, `(x+1)(x-1)`, `x^23`) pass `{ implicitMultiplication: true }`; see {@link ParseOptions}.
698
708
  *
699
709
  * @throws on LaTeX constructs with no `Expr` equivalent, e.g. `\int` or `\sum`.
700
710
  */
701
- static fromLatex(latex) {
702
- return Symbolic.parse(latexToInfix(latex));
711
+ static fromLatex(latex, options = {}) {
712
+ return Symbolic.parse(latexToInfix(latex, options), options);
703
713
  }
704
714
  /**
705
715
  * Solve `expr = 0` for `variable`, returning every *real* root @johnhenry/math
@@ -719,11 +729,11 @@ export class Symbolic {
719
729
  */
720
730
  static solve(expr, variable = "x") {
721
731
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
722
- const coeffs = polynomialCoeffs(Symbolic.simplify(e), variable, 6);
732
+ const coeffs = polynomialCoeffs(simplifyCore(e), variable, 6);
723
733
  if (!coeffs) {
724
734
  throw new Error(`solve: expression is not a polynomial in "${variable}" of degree ≤ 6.`);
725
735
  }
726
- return solvePolynomial(coeffs).map((r) => Symbolic.simplify(r));
736
+ return solvePolynomial(coeffs).map((r) => simplifyCore(r));
727
737
  }
728
738
  /**
729
739
  * Solve a system of linear equations, each written in the same "expr
@@ -740,7 +750,7 @@ export class Symbolic {
740
750
  * @throws {SingularSystemError} if the coefficient matrix is singular/rank-deficient.
741
751
  */
742
752
  static solveSystem(equations, variables) {
743
- const exprs = equations.map((eq) => Symbolic.simplify(typeof eq === "string" ? Symbolic.parse(eq) : eq));
753
+ const exprs = equations.map((eq) => simplifyCore(typeof eq === "string" ? Symbolic.parse(eq) : eq));
744
754
  if (exprs.length !== variables.length) {
745
755
  throw new Error(`solveSystem: expected ${variables.length} equations for ${variables.length} variables, got ${exprs.length}.`);
746
756
  }
@@ -948,7 +958,7 @@ export class Symbolic {
948
958
  */
949
959
  static factor(expr, variable = "x") {
950
960
  const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
951
- const simplified = Symbolic.simplify(e);
961
+ const simplified = simplifyCore(e);
952
962
  const coeffs = polynomialCoeffs(simplified, variable, 6);
953
963
  if (!coeffs)
954
964
  return simplified;
@@ -1132,6 +1142,17 @@ function diff(e, x) {
1132
1142
  }
1133
1143
  }
1134
1144
  // -- simplification ---------------------------------------------------------
1145
+ /** Constant folding and like-term collection to a fixed point -- no expansion or cancellation. */
1146
+ function simplifyCore(expr) {
1147
+ let e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
1148
+ for (let i = 0; i < 30; i++) {
1149
+ const next = collectLikeTerms(simplifyOnce(e));
1150
+ if (equal(next, e))
1151
+ return next;
1152
+ e = next;
1153
+ }
1154
+ return e;
1155
+ }
1135
1156
  function simplifyOnce(e) {
1136
1157
  if (e.type === "const" || e.type === "var")
1137
1158
  return e;
@@ -1876,7 +1897,7 @@ function freshVariableName(e, x) {
1876
1897
  * legitimately hit a domain error/NaN on either side).
1877
1898
  */
1878
1899
  function verifyByDifferentiation(candidate, e, x) {
1879
- const check = Symbolic.simplify(diff(candidate, x));
1900
+ const check = simplifyCore(diff(candidate, x));
1880
1901
  let agreements = 0;
1881
1902
  for (const p of [0.7, 1.3, -0.9]) {
1882
1903
  const expected = evalExpr(e, { [x]: p });
@@ -1902,10 +1923,10 @@ function verifyByDifferentiation(candidate, e, x) {
1902
1923
  */
1903
1924
  function tryUSubstitution(e, x) {
1904
1925
  for (const g of collectSubstitutionCandidates(e, x)) {
1905
- const gPrime = Symbolic.simplify(diff(g, x));
1926
+ const gPrime = simplifyCore(diff(g, x));
1906
1927
  if (isConst(gPrime, 0))
1907
1928
  continue;
1908
- const quotient = Symbolic.simplify(divideOutFactors(e, gPrime) ?? div(e, gPrime));
1929
+ const quotient = simplifyCore(divideOutFactors(e, gPrime) ?? div(e, gPrime));
1909
1930
  if (!containsExpr(quotient, g))
1910
1931
  continue;
1911
1932
  const uName = freshVariableName(quotient, x);
@@ -1914,7 +1935,7 @@ function tryUSubstitution(e, x) {
1914
1935
  continue;
1915
1936
  try {
1916
1937
  const antiderivativeInU = integRules(substituted, uName);
1917
- const candidate = Symbolic.simplify(substExpr(antiderivativeInU, v(uName), g));
1938
+ const candidate = simplifyCore(substExpr(antiderivativeInU, v(uName), g));
1918
1939
  if (verifyByDifferentiation(candidate, e, x))
1919
1940
  return candidate;
1920
1941
  }
@@ -2044,13 +2065,13 @@ function matchSeparable(e, xVar, yVar) {
2044
2065
  function matchLinearOde(e, xVar, yVar) {
2045
2066
  if (!containsVar(e, yVar))
2046
2067
  return null; // handled by matchSeparable's h(y)=1 case instead
2047
- const negP = Symbolic.simplify(diff(e, yVar));
2068
+ const negP = simplifyCore(diff(e, yVar));
2048
2069
  if (containsVar(negP, yVar))
2049
2070
  return null; // not affine in y
2050
- const q = Symbolic.simplify(sub(e, mul(negP, v(yVar))));
2071
+ const q = simplifyCore(sub(e, mul(negP, v(yVar))));
2051
2072
  if (containsVar(q, yVar))
2052
2073
  return null; // sanity check
2053
- const p = Symbolic.simplify(neg(negP));
2074
+ const p = simplifyCore(neg(negP));
2054
2075
  // Conservative, matching matchSeparable's philosophy: p/q must depend on
2055
2076
  // no free variable other than xVar (an extra symbolic parameter, or a
2056
2077
  // stray reference to yVar that survived simplification, both bail out).
@@ -2105,12 +2126,12 @@ function tryInvertForY(H, yVar, rhs, y0) {
2105
2126
  return null;
2106
2127
  }
2107
2128
  function solveSeparableOde(sep, xVar, yVar, x0, y0) {
2108
- const invH = Symbolic.simplify(div(num(1), sep.h));
2129
+ const invH = simplifyCore(div(num(1), sep.h));
2109
2130
  let H;
2110
2131
  let G;
2111
2132
  try {
2112
- H = Symbolic.simplify(integ(invH, yVar));
2113
- G = Symbolic.simplify(integ(sep.g, xVar));
2133
+ H = simplifyCore(integ(invH, yVar));
2134
+ G = simplifyCore(integ(sep.g, xVar));
2114
2135
  }
2115
2136
  catch (err) {
2116
2137
  if (err instanceof NotIntegrableError)
@@ -2118,20 +2139,20 @@ function solveSeparableOde(sep, xVar, yVar, x0, y0) {
2118
2139
  throw err;
2119
2140
  }
2120
2141
  const C = evalExpr(H, { [yVar]: y0 }) - evalExpr(G, { [xVar]: x0 });
2121
- const rhs = Symbolic.simplify(add(G, num(C)));
2142
+ const rhs = simplifyCore(add(G, num(C)));
2122
2143
  const explicitY = tryInvertForY(H, yVar, rhs, y0);
2123
2144
  if (explicitY)
2124
- return { explicit: true, y: Symbolic.simplify(explicitY) };
2125
- return { explicit: false, implicitRelation: Symbolic.simplify(sub(H, rhs)) };
2145
+ return { explicit: true, y: simplifyCore(explicitY) };
2146
+ return { explicit: false, implicitRelation: simplifyCore(sub(H, rhs)) };
2126
2147
  }
2127
2148
  function solveLinearOde(lin, xVar, _yVar, x0, y0) {
2128
2149
  let integralP;
2129
2150
  let mu;
2130
2151
  let muQIntegral;
2131
2152
  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));
2153
+ integralP = simplifyCore(integ(lin.p, xVar));
2154
+ mu = simplifyCore(fn("exp", integralP));
2155
+ muQIntegral = simplifyCore(integ(simplifyCore(mul(mu, lin.q)), xVar));
2135
2156
  }
2136
2157
  catch (err) {
2137
2158
  if (err instanceof NotIntegrableError)
@@ -2142,7 +2163,7 @@ function solveLinearOde(lin, xVar, _yVar, x0, y0) {
2142
2163
  const muAtX0 = evalExpr(mu, { [xVar]: x0 });
2143
2164
  const muQIntegralAtX0 = evalExpr(muQIntegral, { [xVar]: x0 });
2144
2165
  const C = y0 * muAtX0 - muQIntegralAtX0;
2145
- const y = Symbolic.simplify(div(add(muQIntegral, num(C)), mu));
2166
+ const y = simplifyCore(div(add(muQIntegral, num(C)), mu));
2146
2167
  return { explicit: true, y };
2147
2168
  }
2148
2169
  /** 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 +2219,7 @@ function matchHomogeneous(e, xVar, yVar) {
2198
2219
  }
2199
2220
  if (attempts === 0 || agreements < 4)
2200
2221
  return null; // not enough usable probes to trust the match
2201
- const F = Symbolic.simplify(subst(substituted, xVar, num(1)));
2222
+ const F = simplifyCore(subst(substituted, xVar, num(1)));
2202
2223
  return { vName, F };
2203
2224
  }
2204
2225
  /**
@@ -2223,15 +2244,15 @@ function matchHomogeneous(e, xVar, yVar) {
2223
2244
  function solveHomogeneousOde(hom, xVar, yVar, x0, y0) {
2224
2245
  const { vName, F } = hom;
2225
2246
  const v0 = y0 / x0;
2226
- const FMinusV = Symbolic.simplify(sub(F, v(vName)));
2247
+ const FMinusV = simplifyCore(sub(F, v(vName)));
2227
2248
  if (FMinusV.type === "const" && FMinusV.value === 0) {
2228
- return { explicit: true, y: Symbolic.simplify(mul(num(v0), v(xVar))) };
2249
+ return { explicit: true, y: simplifyCore(mul(num(v0), v(xVar))) };
2229
2250
  }
2230
2251
  const sepResult = solveSeparableOde({ g: div(num(1), v(xVar)), h: FMinusV }, xVar, vName, x0, v0);
2231
2252
  if (sepResult.explicit && sepResult.y) {
2232
- return { explicit: true, y: Symbolic.simplify(mul(sepResult.y, v(xVar))) };
2253
+ return { explicit: true, y: simplifyCore(mul(sepResult.y, v(xVar))) };
2233
2254
  }
2234
- const implicitRelation = Symbolic.simplify(subst(sepResult.implicitRelation, vName, div(v(yVar), v(xVar))));
2255
+ const implicitRelation = simplifyCore(subst(sepResult.implicitRelation, vName, div(v(yVar), v(xVar))));
2235
2256
  return { explicit: false, implicitRelation };
2236
2257
  }
2237
2258
  /**
@@ -2324,8 +2345,8 @@ function matchBernoulli(e, xVar, yVar) {
2324
2345
  }
2325
2346
  if (powMatch.n === 0)
2326
2347
  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;
2348
+ const q = powSign === 1 ? powMatch.coef : simplifyCore(neg(powMatch.coef));
2349
+ const p = linSign === 1 ? simplifyCore(neg(linMatch.coef)) : linMatch.coef;
2329
2350
  return { p, q, n: powMatch.n };
2330
2351
  }
2331
2352
  /**
@@ -2345,10 +2366,10 @@ function matchBernoulli(e, xVar, yVar) {
2345
2366
  function solveBernoulliOde(bern, xVar, yVar, x0, y0) {
2346
2367
  const oneMinusN = 1 - bern.n;
2347
2368
  const w0 = y0 ** oneMinusN;
2348
- const transformedP = Symbolic.simplify(mul(num(oneMinusN), bern.p));
2349
- const transformedQ = Symbolic.simplify(mul(num(oneMinusN), bern.q));
2369
+ const transformedP = simplifyCore(mul(num(oneMinusN), bern.p));
2370
+ const transformedQ = simplifyCore(mul(num(oneMinusN), bern.q));
2350
2371
  const wResult = solveLinearOde({ p: transformedP, q: transformedQ }, xVar, yVar, x0, w0);
2351
- const y = Symbolic.simplify(pow(wResult.y, num(1 / oneMinusN)));
2372
+ const y = simplifyCore(pow(wResult.y, num(1 / oneMinusN)));
2352
2373
  return { explicit: true, y };
2353
2374
  }
2354
2375
  /**
@@ -2371,7 +2392,7 @@ function matchExact(e, xVar, yVar) {
2371
2392
  if (![...vars].every((n) => n === xVar || n === yVar))
2372
2393
  return null;
2373
2394
  const M = e.left;
2374
- const N = Symbolic.simplify(neg(e.right));
2395
+ const N = simplifyCore(neg(e.right));
2375
2396
  const dMdy = diff(M, yVar);
2376
2397
  const dNdx = diff(N, xVar);
2377
2398
  let agreements = 0;
@@ -2421,8 +2442,8 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
2421
2442
  let g;
2422
2443
  let gPrime;
2423
2444
  try {
2424
- F0 = Symbolic.simplify(integ(M, xVar));
2425
- gPrime = Symbolic.simplify(sub(N, diff(F0, yVar)));
2445
+ F0 = simplifyCore(integ(M, xVar));
2446
+ gPrime = simplifyCore(sub(N, diff(F0, yVar)));
2426
2447
  if (containsVar(gPrime, xVar)) {
2427
2448
  // Symbolic cancellation fell short -- verify x-independence
2428
2449
  // numerically, then pin x to a probe value that evaluates finitely.
@@ -2435,21 +2456,21 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
2435
2456
  if (Math.abs(atProbe - atOther) > 1e-6 * Math.max(1, Math.abs(atProbe))) {
2436
2457
  throw new NoClosedFormError();
2437
2458
  }
2438
- pinned = Symbolic.simplify(subst(gPrime, xVar, num(xp)));
2459
+ pinned = simplifyCore(subst(gPrime, xVar, num(xp)));
2439
2460
  break;
2440
2461
  }
2441
2462
  if (!pinned)
2442
2463
  throw new NoClosedFormError();
2443
2464
  gPrime = pinned;
2444
2465
  }
2445
- g = Symbolic.simplify(integ(gPrime, yVar));
2466
+ g = simplifyCore(integ(gPrime, yVar));
2446
2467
  }
2447
2468
  catch (err) {
2448
2469
  if (err instanceof NotIntegrableError)
2449
2470
  throw new NoClosedFormError();
2450
2471
  throw err;
2451
2472
  }
2452
- const F = Symbolic.simplify(add(F0, g));
2473
+ const F = simplifyCore(add(F0, g));
2453
2474
  const C = evalExpr(F, { [xVar]: x0, [yVar]: y0 });
2454
2475
  if (!Number.isFinite(C))
2455
2476
  throw new NoClosedFormError();
@@ -2467,13 +2488,13 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
2467
2488
  continue;
2468
2489
  if (F.type === "sub" && yPart === b)
2469
2490
  continue; // F = G(x) - H(y): inverting -H is out of tryInvertForY's shapes
2470
- const rhs = Symbolic.simplify(sub(num(C), xPartSigned));
2491
+ const rhs = simplifyCore(sub(num(C), xPartSigned));
2471
2492
  const explicitY = tryInvertForY(yPart, yVar, rhs, y0);
2472
2493
  if (explicitY)
2473
- return { explicit: true, y: Symbolic.simplify(explicitY) };
2494
+ return { explicit: true, y: simplifyCore(explicitY) };
2474
2495
  }
2475
2496
  }
2476
- return { explicit: false, implicitRelation: Symbolic.simplify(sub(F, num(C))) };
2497
+ return { explicit: false, implicitRelation: simplifyCore(sub(F, num(C))) };
2477
2498
  }
2478
2499
  /** If `e` is `a·x + b` (a, b constant), return `{ a, b }`, else `null`. */
2479
2500
  function linearCoeffs(e, x) {
@@ -3345,11 +3366,54 @@ const CMP_LATEX_CMDS = {
3345
3366
  "\\ne": "!=",
3346
3367
  };
3347
3368
  /** 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);
3369
+ function latexToInfix(latex, opts = {}) {
3370
+ const cleaned = latex.replace(/\\left|\\right/g, "").replace(/(?<!\\)\\(?:,|;|!|quad|qquad| )/g, "");
3371
+ return transformLatex(cleaned, opts);
3372
+ }
3373
+ /**
3374
+ * Where the unbracketed argument of a function command ends under implicit multiplication: the longest
3375
+ * product of numbers, letters, `\\pi`, powers, subscripts and bracket groups starting at `start`
3376
+ * (`\\sin 2x` takes `2x`; `\\sin x\\cos x` takes just `x`). Returns `start` when there is none.
3377
+ */
3378
+ function bareArgumentEnd(s, start) {
3379
+ let j = start;
3380
+ const group = (at) => {
3381
+ const open = s[at];
3382
+ return findGroupEnd(s, at, open, open === "{" ? "}" : ")") + 1;
3383
+ };
3384
+ while (j < s.length) {
3385
+ const c = s[j];
3386
+ if (/[A-Za-z0-9.]/.test(c)) {
3387
+ j++;
3388
+ }
3389
+ else if (c === "(" || c === "{") {
3390
+ j = group(j);
3391
+ }
3392
+ else if (c === "_" && s[j + 1] === "{") {
3393
+ j = group(j + 1);
3394
+ }
3395
+ else if (c === "^") {
3396
+ if (s[j + 1] === "{")
3397
+ j = group(j + 1);
3398
+ else if (s[j + 1] === "\\")
3399
+ j += 1 + (/^\\[a-zA-Z]+/.exec(s.slice(j + 1))?.[0].length ?? 1);
3400
+ else
3401
+ j += 2;
3402
+ }
3403
+ else if (/^\\pi(?![a-zA-Z])/.test(s.slice(j))) {
3404
+ j += 3;
3405
+ }
3406
+ else if (c === " " && /^ +(?:[A-Za-z0-9.({]|\\pi(?![a-zA-Z]))/.test(s.slice(j))) {
3407
+ j += s.slice(j).length - s.slice(j).trimStart().length;
3408
+ }
3409
+ else {
3410
+ break;
3411
+ }
3412
+ }
3413
+ return j;
3351
3414
  }
3352
- function transformLatex(s) {
3415
+ function transformLatex(s, opts = {}) {
3416
+ const implicit = opts.implicitMultiplication === true;
3353
3417
  let out = "";
3354
3418
  let i = 0;
3355
3419
  const readGroup = () => {
@@ -3360,11 +3424,21 @@ function transformLatex(s) {
3360
3424
  return content;
3361
3425
  };
3362
3426
  const readGroupOrParenOrToken = () => {
3427
+ while (s[i] === " ")
3428
+ i++;
3363
3429
  if (s[i] === "{" || s[i] === "(") {
3364
3430
  const { content, next } = extractGroup(s, i);
3365
3431
  i = next;
3366
3432
  return content;
3367
3433
  }
3434
+ if (implicit) {
3435
+ const end = bareArgumentEnd(s, i);
3436
+ if (end > i) {
3437
+ const run = s.slice(i, end).trim();
3438
+ i = end;
3439
+ return run;
3440
+ }
3441
+ }
3368
3442
  const m = /^[a-zA-Z_]\w*|^\d+\.?\d*(?:[eE][+-]?\d+)?/.exec(s.slice(i));
3369
3443
  if (m) {
3370
3444
  i += m[0].length;
@@ -3372,6 +3446,32 @@ function transformLatex(s) {
3372
3446
  }
3373
3447
  throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
3374
3448
  };
3449
+ /** A `{...}` group, or the one token TeX takes without braces: a digit, a letter or a `\\command`. */
3450
+ const readGroupOrToken = () => {
3451
+ while (s[i] === " ")
3452
+ i++;
3453
+ if (s[i] === "{")
3454
+ return readGroup();
3455
+ const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i));
3456
+ if (!m)
3457
+ throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
3458
+ i += m[0].length;
3459
+ return m[0];
3460
+ };
3461
+ /** A power written on a function command (`\\sin^{2}`, `\\sin^2`), or null. */
3462
+ const readFunctionPower = () => {
3463
+ if (s[i] !== "^")
3464
+ return null;
3465
+ i++;
3466
+ const power = readGroupOrToken();
3467
+ // `\sin^{-1}` is arcsin in most textbooks and calculators but 1/sin in strict TeX/notation; guessing
3468
+ // wrong would silently change the maths, so it is refused rather than interpreted.
3469
+ if (/^\s*-\s*1\s*$/.test(power)) {
3470
+ throw new Error("Ambiguous LaTeX: a function with exponent -1 (e.g. \\sin^{-1}) may mean the inverse function or the reciprocal; " +
3471
+ "write \\arcsin(x) for the inverse, or (\\sin x)^{-1} / 1/\\sin x for the reciprocal");
3472
+ }
3473
+ return power;
3474
+ };
3375
3475
  /** Read a `(...)`/`{...}` group holding exactly 2 comma-separated arguments, for `call2` functions. */
3376
3476
  const readArgPair = () => {
3377
3477
  if (s[i] !== "(" && s[i] !== "{")
@@ -3427,10 +3527,11 @@ function transformLatex(s) {
3427
3527
  if (condPart.trim() === "\\text{otherwise}") {
3428
3528
  if (otherwise !== null)
3429
3529
  throw new Error("\\begin{cases} may have only one \\text{otherwise} row");
3430
- otherwise = transformLatex(exprPart);
3530
+ otherwise = transformLatex(exprPart, opts);
3431
3531
  }
3432
3532
  else {
3433
- parts.push(`${transformLatex(condPart)}, ${transformLatex(exprPart)}`);
3533
+ const condition = condPart.replace(/^\s*\\text\{\s*(?:if|when)\s*\}/, ""); // "x & \text{if } x<0" as MathLive writes it
3534
+ parts.push(`${transformLatex(condition, opts)}, ${transformLatex(exprPart, opts)}`);
3434
3535
  }
3435
3536
  }
3436
3537
  if (otherwise === null) {
@@ -3439,29 +3540,29 @@ function transformLatex(s) {
3439
3540
  out += `piecewise(${parts.concat(otherwise).join(", ")})`;
3440
3541
  continue;
3441
3542
  }
3442
- if (cmd === "\\frac") {
3443
- const numerator = readGroup();
3444
- const denominator = readGroup();
3445
- out += `((${transformLatex(numerator)})/(${transformLatex(denominator)}))`;
3543
+ if (cmd === "\\frac" || cmd === "\\dfrac" || cmd === "\\tfrac") {
3544
+ const numerator = readGroupOrToken();
3545
+ const denominator = readGroupOrToken();
3546
+ out += `((${transformLatex(numerator, opts)})/(${transformLatex(denominator, opts)}))`;
3446
3547
  continue;
3447
3548
  }
3448
3549
  if (cmd === "\\sqrt") {
3449
3550
  if (s[i] === "[") {
3450
3551
  const { content: root, next } = extractGroup(s, i);
3451
3552
  i = next;
3452
- const arg = readGroup();
3453
- const transformedRoot = transformLatex(root).trim();
3553
+ const arg = readGroupOrToken();
3554
+ const transformedRoot = transformLatex(root, opts).trim();
3454
3555
  // \sqrt[3]{...} maps to cbrt(...) rather than a fractional power so
3455
3556
  // negative arguments evaluate correctly (Math.cbrt(-8) = -2, but
3456
3557
  // (-8)^(1/3) is NaN under JS's `**`).
3457
3558
  out +=
3458
3559
  transformedRoot === "3"
3459
- ? `cbrt(${transformLatex(arg)})`
3460
- : `((${transformLatex(arg)})^(1/(${transformedRoot})))`;
3560
+ ? `cbrt(${transformLatex(arg, opts)})`
3561
+ : `((${transformLatex(arg, opts)})^(1/(${transformedRoot})))`;
3461
3562
  }
3462
3563
  else {
3463
- const arg = readGroup();
3464
- out += `sqrt(${transformLatex(arg)})`;
3564
+ const arg = readGroupOrToken();
3565
+ out += `sqrt(${transformLatex(arg, opts)})`;
3465
3566
  }
3466
3567
  continue;
3467
3568
  }
@@ -3475,8 +3576,10 @@ function transformLatex(s) {
3475
3576
  if (base !== "10" && base !== "2") {
3476
3577
  throw new Error(`Unsupported LaTeX construct: \\log base ${base} (only base 10 and base 2 are supported)`);
3477
3578
  }
3579
+ const power = readFunctionPower();
3478
3580
  const arg = readGroupOrParenOrToken();
3479
- out += `${base === "2" ? "log2" : "log10"}(${transformLatex(arg)})`;
3581
+ const call = `${base === "2" ? "log2" : "log10"}(${transformLatex(arg, opts)})`;
3582
+ out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
3480
3583
  continue;
3481
3584
  }
3482
3585
  if (cmd === "\\lfloor" || cmd === "\\lceil") {
@@ -3486,7 +3589,7 @@ function transformLatex(s) {
3486
3589
  throw new Error(`Unmatched '${cmd}' in LaTeX source`);
3487
3590
  const inner = s.slice(i, closeIdx);
3488
3591
  i = closeIdx + closeCmd.length;
3489
- out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner)})`;
3592
+ out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner, opts)})`;
3490
3593
  continue;
3491
3594
  }
3492
3595
  if (cmd === "\\operatorname") {
@@ -3494,26 +3597,30 @@ function transformLatex(s) {
3494
3597
  const binaryName = OPERATORNAME_BINARY_FUNC_NAMES[opName];
3495
3598
  if (binaryName) {
3496
3599
  const [left, right] = readArgPair();
3497
- out += `${binaryName}(${transformLatex(left)}, ${transformLatex(right)})`;
3600
+ out += `${binaryName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
3498
3601
  continue;
3499
3602
  }
3500
3603
  const funcName = OPERATORNAME_FUNC_NAMES[opName];
3501
3604
  if (!funcName)
3502
3605
  throw new Error(`Unsupported LaTeX construct: \\operatorname{${opName}}`);
3606
+ const power = readFunctionPower();
3503
3607
  const arg = readGroupOrParenOrToken();
3504
- out += `${funcName}(${transformLatex(arg)})`;
3608
+ const call = `${funcName}(${transformLatex(arg, opts)})`;
3609
+ out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
3505
3610
  continue;
3506
3611
  }
3507
3612
  const binaryFuncName = LATEX_BINARY_FUNC_NAMES[cmd];
3508
3613
  if (binaryFuncName) {
3509
3614
  const [left, right] = readArgPair();
3510
- out += `${binaryFuncName}(${transformLatex(left)}, ${transformLatex(right)})`;
3615
+ out += `${binaryFuncName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
3511
3616
  continue;
3512
3617
  }
3513
3618
  const funcName = LATEX_FUNC_NAMES[cmd];
3514
3619
  if (funcName) {
3620
+ const power = readFunctionPower();
3515
3621
  const arg = readGroupOrParenOrToken();
3516
- out += `${funcName}(${transformLatex(arg)})`;
3622
+ const call = `${funcName}(${transformLatex(arg, opts)})`;
3623
+ out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
3517
3624
  continue;
3518
3625
  }
3519
3626
  throw new Error(`Unsupported LaTeX construct: ${cmd}`);
@@ -3524,13 +3631,20 @@ function transformLatex(s) {
3524
3631
  throw new Error("Unmatched '|' in LaTeX source");
3525
3632
  const inner = s.slice(i + 1, closeIdx);
3526
3633
  i = closeIdx + 1;
3527
- out += `abs(${transformLatex(inner)})`;
3634
+ out += `abs(${transformLatex(inner, opts)})`;
3635
+ continue;
3636
+ }
3637
+ if (implicit && ch === "^" && /^[a-zA-Z0-9]|^\\[a-zA-Z]/.test(s.slice(i + 1))) {
3638
+ // TeX reads `x^23` as `x^2` times `3`: the exponent is one token
3639
+ const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i + 1));
3640
+ i += 1 + m[0].length;
3641
+ out += `^(${transformLatex(m[0], opts)})`;
3528
3642
  continue;
3529
3643
  }
3530
3644
  if (ch === "^" && s[i + 1] === "{") {
3531
3645
  const { content, next } = extractGroup(s, i + 1);
3532
3646
  i = next;
3533
- out += `^(${transformLatex(content)})`;
3647
+ out += `^(${transformLatex(content, opts)})`;
3534
3648
  continue;
3535
3649
  }
3536
3650
  if (ch === "_" && s[i + 1] === "{") {
@@ -3542,7 +3656,7 @@ function transformLatex(s) {
3542
3656
  if (ch === "{") {
3543
3657
  const { content, next } = extractGroup(s, i);
3544
3658
  i = next;
3545
- out += `(${transformLatex(content)})`;
3659
+ out += `(${transformLatex(content, opts)})`;
3546
3660
  continue;
3547
3661
  }
3548
3662
  if (ch === "=") {
@@ -3558,6 +3672,378 @@ function transformLatex(s) {
3558
3672
  }
3559
3673
  return out;
3560
3674
  }
3675
+ const MAX_POLY_TERMS = 400;
3676
+ const MAX_RATIONAL_POWER = 8;
3677
+ class RatBail extends Error {
3678
+ }
3679
+ function atomIndex(ctx, e) {
3680
+ const key = render(e, 0);
3681
+ let i = ctx.keys.indexOf(key);
3682
+ if (i < 0) {
3683
+ i = ctx.keys.length;
3684
+ ctx.keys.push(key);
3685
+ ctx.atoms.push(e);
3686
+ }
3687
+ return i;
3688
+ }
3689
+ function expsCmp(a, b) {
3690
+ const n = Math.max(a.length, b.length);
3691
+ for (let i = 0; i < n; i++) {
3692
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
3693
+ if (d !== 0)
3694
+ return d;
3695
+ }
3696
+ return 0;
3697
+ }
3698
+ function expsKey(a) {
3699
+ let end = a.length;
3700
+ while (end > 0 && !a[end - 1])
3701
+ end--;
3702
+ return a.slice(0, end).join(",");
3703
+ }
3704
+ function polyNormalize(terms) {
3705
+ const byKey = new Map();
3706
+ for (const t of terms) {
3707
+ const k = expsKey(t.exps);
3708
+ const prev = byKey.get(k);
3709
+ if (prev)
3710
+ byKey.set(k, { exps: prev.exps, c: prev.c.add(t.c) });
3711
+ else
3712
+ byKey.set(k, { exps: t.exps, c: t.c });
3713
+ }
3714
+ const out = [...byKey.values()].filter((t) => !t.c.isZero());
3715
+ out.sort((a, b) => expsCmp(b.exps, a.exps));
3716
+ if (out.length > MAX_POLY_TERMS)
3717
+ throw new RatBail();
3718
+ return out;
3719
+ }
3720
+ const polyConst = (c) => (c.isZero() ? [] : [{ exps: [], c }]);
3721
+ const polyAdd = (a, b) => polyNormalize([...a, ...b]);
3722
+ const polyNeg = (a) => a.map((t) => ({ exps: t.exps, c: t.c.negate() }));
3723
+ const polySub = (a, b) => polyAdd(a, polyNeg(b));
3724
+ function polyMul(a, b) {
3725
+ if (a.length * b.length > MAX_POLY_TERMS * 20)
3726
+ throw new RatBail();
3727
+ const out = [];
3728
+ for (const x of a) {
3729
+ for (const y of b) {
3730
+ const n = Math.max(x.exps.length, y.exps.length);
3731
+ const exps = [];
3732
+ for (let i = 0; i < n; i++)
3733
+ exps.push((x.exps[i] ?? 0) + (y.exps[i] ?? 0));
3734
+ out.push({ exps, c: x.c.multiply(y.c) });
3735
+ }
3736
+ }
3737
+ return polyNormalize(out);
3738
+ }
3739
+ function polyEqual(a, b) {
3740
+ return a.length === b.length && a.every((t, i) => expsKey(t.exps) === expsKey(b[i].exps) && t.c.equals(b[i].c));
3741
+ }
3742
+ const polyIsConst = (a) => a.length === 0 || (a.length === 1 && expsKey(a[0].exps) === "");
3743
+ /** Division in lex order: `a = q*b + r` (`b` must be nonzero). */
3744
+ function polyDivMod(a, b) {
3745
+ const lead = b[0];
3746
+ let rem = a;
3747
+ const q = [];
3748
+ const r = [];
3749
+ let guard = 0;
3750
+ while (rem.length > 0) {
3751
+ if (++guard > 20_000)
3752
+ throw new RatBail();
3753
+ const t = rem[0];
3754
+ const n = Math.max(t.exps.length, lead.exps.length);
3755
+ const exps = [];
3756
+ let divisible = true;
3757
+ for (let i = 0; i < n; i++) {
3758
+ const d = (t.exps[i] ?? 0) - (lead.exps[i] ?? 0);
3759
+ if (d < 0)
3760
+ divisible = false;
3761
+ exps.push(d);
3762
+ }
3763
+ if (!divisible) {
3764
+ r.push(t);
3765
+ rem = rem.slice(1);
3766
+ continue;
3767
+ }
3768
+ const qt = { exps, c: t.c.divide(lead.c) };
3769
+ q.push(qt);
3770
+ rem = polySub(rem, polyMul([qt], b));
3771
+ }
3772
+ return { q: polyNormalize(q), r: polyNormalize(r) };
3773
+ }
3774
+ function polyAtoms(...ps) {
3775
+ const s = new Set();
3776
+ for (const p of ps) {
3777
+ for (const t of p) {
3778
+ for (let i = 0; i < t.exps.length; i++)
3779
+ if (t.exps[i] > 0)
3780
+ s.add(i);
3781
+ }
3782
+ }
3783
+ return s;
3784
+ }
3785
+ /** Monic GCD of two polynomials in one atom (Euclid); 1 when several atoms are involved. */
3786
+ function polyGcd(a, b) {
3787
+ if (polyAtoms(a, b).size > 1)
3788
+ return polyConst(Rational.One);
3789
+ let x = a;
3790
+ let y = b;
3791
+ while (y.length > 0) {
3792
+ const { r } = polyDivMod(x, y);
3793
+ x = y;
3794
+ y = r;
3795
+ }
3796
+ if (x.length === 0)
3797
+ return polyConst(Rational.One);
3798
+ const lc = x[0].c;
3799
+ return x.map((t) => ({ exps: t.exps, c: t.c.divide(lc) }));
3800
+ }
3801
+ /** The largest monomial dividing every term (exponent-wise minimum). */
3802
+ function monomialContent(p) {
3803
+ if (p.length === 0)
3804
+ return [];
3805
+ const n = Math.max(...p.map((t) => t.exps.length));
3806
+ const out = [];
3807
+ for (let i = 0; i < n; i++)
3808
+ out.push(Math.min(...p.map((t) => t.exps[i] ?? 0)));
3809
+ return out;
3810
+ }
3811
+ function polyDivMonomial(p, m) {
3812
+ return p.map((t) => ({ exps: t.exps.map((e, i) => e - (m[i] ?? 0)), c: t.c }));
3813
+ }
3814
+ const ratioFromPoly = (n) => ({ n, d: polyConst(Rational.One) });
3815
+ function ratioAdd(a, b, subtract) {
3816
+ const bn = subtract ? polyNeg(b.n) : b.n;
3817
+ if (polyEqual(a.d, b.d))
3818
+ return { n: polyAdd(a.n, bn), d: a.d };
3819
+ return { n: polyAdd(polyMul(a.n, b.d), polyMul(bn, a.d)), d: polyMul(a.d, b.d) };
3820
+ }
3821
+ const ratioMul = (a, b) => ({ n: polyMul(a.n, b.n), d: polyMul(a.d, b.d) });
3822
+ function ratioDiv(a, b) {
3823
+ if (b.n.length === 0)
3824
+ throw new RatBail(); // division by zero: leave it to evaluation
3825
+ return { n: polyMul(a.n, b.d), d: polyMul(a.d, b.n) };
3826
+ }
3827
+ function ratioPow(a, n) {
3828
+ const k = Math.abs(n);
3829
+ if (k > 1000)
3830
+ throw new RatBail();
3831
+ const singleTerm = a.n.length === 1 && a.d.length === 1;
3832
+ if (!singleTerm && k > MAX_RATIONAL_POWER)
3833
+ throw new RatBail();
3834
+ let result;
3835
+ if (singleTerm) {
3836
+ const mono = (t) => ({ exps: t.exps.map((e) => e * k), c: t.c.pow(k) });
3837
+ result = { n: [mono(a.n[0])], d: [mono(a.d[0])] };
3838
+ }
3839
+ else {
3840
+ result = ratioFromPoly(polyConst(Rational.One));
3841
+ for (let i = 0; i < k; i++)
3842
+ result = ratioMul(result, a);
3843
+ }
3844
+ return n < 0 ? ratioDiv(ratioFromPoly(polyConst(Rational.One)), result) : result;
3845
+ }
3846
+ function exactRational(value) {
3847
+ if (!Number.isFinite(value))
3848
+ return null;
3849
+ if (Number.isInteger(value))
3850
+ return new Rational(BigInt(value));
3851
+ const r = Rational.fromNumber(value, 10_000);
3852
+ return r.toNumber() === value ? r : null;
3853
+ }
3854
+ function toRatio(e, ctx) {
3855
+ const atom = (x) => {
3856
+ const i = atomIndex(ctx, x);
3857
+ const exps = new Array(i + 1).fill(0);
3858
+ exps[i] = 1;
3859
+ return ratioFromPoly([{ exps, c: Rational.One }]);
3860
+ };
3861
+ switch (e.type) {
3862
+ case "const": {
3863
+ const r = exactRational(e.value);
3864
+ return r ? ratioFromPoly(polyConst(r)) : atom(e);
3865
+ }
3866
+ case "var":
3867
+ return atom(e);
3868
+ case "neg": {
3869
+ const a = toRatio(e.arg, ctx);
3870
+ return { n: polyNeg(a.n), d: a.d };
3871
+ }
3872
+ case "add":
3873
+ return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), false);
3874
+ case "sub":
3875
+ return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), true);
3876
+ case "mul":
3877
+ return ratioMul(toRatio(e.left, ctx), toRatio(e.right, ctx));
3878
+ case "div":
3879
+ return ratioDiv(toRatio(e.left, ctx), toRatio(e.right, ctx));
3880
+ case "pow": {
3881
+ if (e.exp.type === "const" && Number.isInteger(e.exp.value)) {
3882
+ return ratioPow(toRatio(e.base, ctx), e.exp.value);
3883
+ }
3884
+ return atom(pow(normalizeRational(e.base), normalizeRational(e.exp)));
3885
+ }
3886
+ case "func":
3887
+ return atom(fn(e.name, normalizeRational(e.arg)));
3888
+ case "call2":
3889
+ return atom(call2(e.name, normalizeRational(e.left), normalizeRational(e.right)));
3890
+ case "cmp":
3891
+ return atom(cmp(e.op, normalizeRational(e.left), normalizeRational(e.right)));
3892
+ case "piecewise":
3893
+ return atom(piecewise(e.branches.map((br) => ({ cond: normalizeRational(br.cond), expr: normalizeRational(br.expr) })), normalizeRational(e.otherwise)));
3894
+ case "sum":
3895
+ return atom(sumExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
3896
+ case "product":
3897
+ return atom(productExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
3898
+ }
3899
+ }
3900
+ function bigGcd(a, b) {
3901
+ let x = a < 0n ? -a : a;
3902
+ let y = b < 0n ? -b : b;
3903
+ while (y)
3904
+ [x, y] = [y, x % y];
3905
+ return x;
3906
+ }
3907
+ /** Cancel common factors, then scale to coprime integer coefficients with a positive denominator lead. */
3908
+ function ratioReduce(r) {
3909
+ if (r.n.length === 0)
3910
+ return ratioFromPoly([]);
3911
+ let { n, d } = r;
3912
+ const m = monomialContent([...n, ...d]);
3913
+ if (m.some((e) => e > 0)) {
3914
+ n = polyDivMonomial(n, m);
3915
+ d = polyDivMonomial(d, m);
3916
+ }
3917
+ if (!polyIsConst(d)) {
3918
+ const g = polyGcd(n, d);
3919
+ if (!polyIsConst(g)) {
3920
+ n = polyDivMod(n, g).q;
3921
+ d = polyDivMod(d, g).q;
3922
+ }
3923
+ else {
3924
+ // Several atoms: still cancel when one side divides the other exactly.
3925
+ const dn = polyDivMod(n, d);
3926
+ if (dn.r.length === 0) {
3927
+ n = dn.q;
3928
+ d = polyConst(Rational.One);
3929
+ }
3930
+ else {
3931
+ const nd = polyDivMod(d, n);
3932
+ if (nd.r.length === 0) {
3933
+ d = nd.q;
3934
+ n = polyConst(Rational.One);
3935
+ }
3936
+ }
3937
+ }
3938
+ }
3939
+ if (polyIsConst(d)) {
3940
+ const c = d[0].c;
3941
+ return { n: n.map((t) => ({ exps: t.exps, c: t.c.divide(c) })), d: polyConst(Rational.One) };
3942
+ }
3943
+ // integer coefficients: multiply through by the lcm of the denominators, divide by the content
3944
+ let lcm = 1n;
3945
+ for (const t of [...n, ...d]) {
3946
+ const den = t.c.denominator;
3947
+ lcm = (lcm / bigGcd(lcm, den)) * den;
3948
+ }
3949
+ const scale = new Rational(lcm);
3950
+ n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
3951
+ d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
3952
+ let content = 0n;
3953
+ for (const t of [...n, ...d])
3954
+ content = bigGcd(content, t.c.numerator);
3955
+ if (content > 1n) {
3956
+ const inv = new Rational(1n, content);
3957
+ n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
3958
+ d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
3959
+ }
3960
+ if (d[0].c.sign < 0) {
3961
+ n = polyNeg(n);
3962
+ d = polyNeg(d);
3963
+ }
3964
+ return { n, d };
3965
+ }
3966
+ function monomialExpr(exps, ctx) {
3967
+ let out = null;
3968
+ for (let i = 0; i < exps.length; i++) {
3969
+ const e = exps[i] ?? 0;
3970
+ if (e === 0)
3971
+ continue;
3972
+ const factor = e === 1 ? ctx.atoms[i] : pow(ctx.atoms[i], num(e));
3973
+ out = out ? mul(out, factor) : factor;
3974
+ }
3975
+ return out;
3976
+ }
3977
+ /** A polynomial as a descending-degree sum; fractional coefficients print as `p*m/q`. */
3978
+ function polyToTree(p, ctx) {
3979
+ if (p.length === 0)
3980
+ return num(0);
3981
+ const ordered = [...p].sort((a, b) => {
3982
+ const da = a.exps.reduce((s, x) => s + x, 0);
3983
+ const db = b.exps.reduce((s, x) => s + x, 0);
3984
+ return db - da || expsCmp(b.exps, a.exps);
3985
+ });
3986
+ let out = null;
3987
+ for (const t of ordered) {
3988
+ const negative = t.c.sign < 0;
3989
+ const abs = t.c.abs();
3990
+ const mono = monomialExpr(t.exps, ctx);
3991
+ const numer = abs.numerator;
3992
+ let body;
3993
+ if (!mono)
3994
+ body = num(abs.toNumber());
3995
+ else if (numer === 1n && abs.denominator === 1n)
3996
+ body = mono;
3997
+ else if (abs.denominator === 1n)
3998
+ body = mul(num(Number(numer)), mono);
3999
+ else
4000
+ body = div(numer === 1n ? mono : mul(num(Number(numer)), mono), num(Number(abs.denominator)));
4001
+ if (!out)
4002
+ out = negative ? neg(body) : body;
4003
+ else
4004
+ out = negative ? sub(out, body) : add(out, body);
4005
+ }
4006
+ return out;
4007
+ }
4008
+ function nodeCount(e) {
4009
+ switch (e.type) {
4010
+ case "const":
4011
+ case "var":
4012
+ return 1;
4013
+ case "neg":
4014
+ case "func":
4015
+ return 1 + nodeCount(e.arg);
4016
+ case "pow":
4017
+ return 1 + nodeCount(e.base) + nodeCount(e.exp);
4018
+ case "piecewise":
4019
+ return 1 + nodeCount(e.otherwise) + e.branches.reduce((s, br) => s + nodeCount(br.cond) + nodeCount(br.expr), 0);
4020
+ case "sum":
4021
+ case "product":
4022
+ return 1 + nodeCount(e.from) + nodeCount(e.to) + nodeCount(e.body);
4023
+ default:
4024
+ return 1 + nodeCount(e.left) + nodeCount(e.right);
4025
+ }
4026
+ }
4027
+ /**
4028
+ * Expand `e` as a rational function and cancel common factors, returning the result only
4029
+ * when it has strictly fewer nodes than `e`; otherwise returns `e` itself.
4030
+ */
4031
+ function normalizeRational(e) {
4032
+ if (e.type === "const" || e.type === "var")
4033
+ return e;
4034
+ try {
4035
+ const ctx = { atoms: [], keys: [] };
4036
+ const reduced = ratioReduce(toRatio(e, ctx));
4037
+ const numer = polyToTree(reduced.n, ctx);
4038
+ const candidate = polyIsConst(reduced.d) ? numer : div(numer, polyToTree(reduced.d, ctx));
4039
+ return nodeCount(candidate) < nodeCount(e) ? candidate : e;
4040
+ }
4041
+ catch (err) {
4042
+ if (err instanceof RatBail)
4043
+ return e;
4044
+ throw err;
4045
+ }
4046
+ }
3561
4047
  // -- equation solving & factoring ----------------------------------------------
3562
4048
  /** Best-effort exact `Expr` for a numeric value: an integer, a reduced fraction, or (as a last resort) the raw float. */
3563
4049
  function toExactExpr(value) {
@@ -3591,7 +4077,7 @@ function linearCoeffsForSystem(eq, variables) {
3591
4077
  return null;
3592
4078
  const coeffs = [];
3593
4079
  for (const name of variables) {
3594
- const derivative = Symbolic.simplify(diff(eq, name));
4080
+ const derivative = simplifyCore(diff(eq, name));
3595
4081
  if (variables.some((other) => containsVar(derivative, other)))
3596
4082
  return null;
3597
4083
  const coeff = evalExpr(derivative, zeroEnv);
@@ -3623,7 +4109,7 @@ function polynomialCoeffs(expr, variable, maxDegree) {
3623
4109
  // derivative of x^0 (`0·x^-1`) evaluates to `0·Infinity = NaN` at x=0
3624
4110
  // even though it is symbolically zero — simplifying collapses `0·anything`
3625
4111
  // to `0` before that indeterminate form can arise.
3626
- term = Symbolic.simplify(diff(term, variable));
4112
+ term = simplifyCore(diff(term, variable));
3627
4113
  }
3628
4114
  while (coeffs.length > 1 && Math.abs(coeffs[coeffs.length - 1]) < 1e-9)
3629
4115
  coeffs.pop();
@@ -3818,7 +4304,7 @@ function factorPolynomial(coeffsIn, variable) {
3818
4304
  else if (Math.abs(remaining[0] - 1) > 1e-9) {
3819
4305
  result = mul(result, toExactExpr(remaining[0]));
3820
4306
  }
3821
- return Symbolic.simplify(result);
4307
+ return simplifyCore(result);
3822
4308
  }
3823
4309
  // -- limits ---------------------------------------------------------------------
3824
4310
  function evalNear(e, x, point, direction) {
@@ -3842,19 +4328,25 @@ function limitAt(e, x, point, direction, depth) {
3842
4328
  // operand verbatim into the result) and a limit needing more than a
3843
4329
  // couple of rounds (e.g. most limits at infinity) blows up memory
3844
4330
  // 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));
4331
+ const df = simplifyCore(diff(e.left, x));
4332
+ const dg = simplifyCore(diff(e.right, x));
3847
4333
  return limitAt(div(df, dg), x, point, direction, depth + 1);
3848
4334
  }
3849
4335
  }
3850
4336
  return direct;
3851
4337
  }
3852
4338
  // -- recursive-descent parser ----------------------------------------------
4339
+ /** Every name the parser treats as a function, longest first, so `asinh(` is not read as `a*sinh(`. */
4340
+ const FUNCTION_NAMES_LONGEST_FIRST = [...FUNCTION_NAMES].sort((a, b) => b.length - a.length);
3853
4341
  class Parser {
3854
4342
  s;
4343
+ implicit;
3855
4344
  pos = 0;
3856
- constructor(input) {
3857
- this.s = input.replace(/\s+/g, "");
4345
+ constructor(input, options = {}) {
4346
+ this.implicit = options.implicitMultiplication === true;
4347
+ // With implicit multiplication, white space between two numbers is a product (`2 3`), not one number.
4348
+ const spaced = this.implicit ? input.replace(/(\d)\s+(?=[\d.])/g, "$1*") : input;
4349
+ this.s = spaced.replace(/\s+/g, "");
3858
4350
  }
3859
4351
  parse() {
3860
4352
  const e = this.comparison();
@@ -3973,12 +4465,38 @@ class Parser {
3973
4465
  }
3974
4466
  term() {
3975
4467
  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);
4468
+ for (;;) {
4469
+ const ch = this.peek();
4470
+ if (ch === "*" || ch === "/") {
4471
+ this.pos++;
4472
+ const right = this.factor();
4473
+ left = ch === "*" ? mul(left, right) : div(left, right);
4474
+ }
4475
+ else if (this.implicit && /[a-zA-Z0-9.(]/.test(ch)) {
4476
+ left = mul(left, this.factor()); // juxtaposition: `2x`, `x y`, `2(x+1)`
4477
+ }
4478
+ else {
4479
+ return left;
4480
+ }
3980
4481
  }
3981
- return left;
4482
+ }
4483
+ /**
4484
+ * The identifier at `this.pos` under implicit multiplication: a function name followed by `(`, `pi`, or a
4485
+ * single letter with an optional `_subscript`. A letter run like `xy` is therefore read one letter at a time.
4486
+ */
4487
+ implicitIdentifier() {
4488
+ const rest = this.s.slice(this.pos);
4489
+ const run = /^[a-zA-Z]+/.exec(rest)?.[0] ?? "";
4490
+ const call = FUNCTION_NAMES_LONGEST_FIRST.find((f) => rest.startsWith(f) && rest[f.length] === "(");
4491
+ if (call)
4492
+ return call;
4493
+ const bare = FUNCTION_NAMES_LONGEST_FIRST.find((f) => f.length > 1 && run.startsWith(f));
4494
+ if (bare)
4495
+ throw new Error(`Function ${bare} needs parentheses: ${bare}(...)`);
4496
+ if (rest.startsWith("pi"))
4497
+ return "pi";
4498
+ const sub = /^[a-zA-Z]_(?:\d+|[a-zA-Z])/.exec(rest);
4499
+ return sub ? sub[0] : rest[0];
3982
4500
  }
3983
4501
  factor() {
3984
4502
  if (this.peek() === "-") {
@@ -4016,9 +4534,11 @@ class Parser {
4016
4534
  return num(Number(numMatch[0]));
4017
4535
  }
4018
4536
  // identifier (function or variable or constant)
4019
- const idMatch = /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
4537
+ const idMatch = this.implicit
4538
+ ? /^[a-zA-Z_]/.exec(this.s.slice(this.pos))
4539
+ : /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
4020
4540
  if (idMatch) {
4021
- const name = idMatch[0];
4541
+ const name = this.implicit ? this.implicitIdentifier() : idMatch[0];
4022
4542
  this.pos += name.length;
4023
4543
  if ((name === "log" || name === "clamp") && this.peek() === "(") {
4024
4544
  this.pos++;