Skip to content

Commit 60ecdfd

Browse files
committed
Add tests for costFunction setter, enhance Fresnel tests, and introduce special functions tests
- Implement tests for the costFunction setter in ComputeEngine to ensure proper fallback to default when non-function values are assigned. - Update Fresnel tests to reflect accurate values for large arguments S(50) and C(50). - Add comprehensive tests for arbitrary-precision kernels for Erf/Erfc/ErfInv and Sinc/FresnelS/FresnelC, ensuring symbolic evaluation and numeric approximation correctness. - Introduce tests for the intersection of numeric primitives in the type lattice, ensuring soundness and maximality of subtype reductions. - Add exact values tests for the Riemann zeta function at integer literals, including checks for Bernoulli numbers and symbolic trivial zeros.
1 parent eee360d commit 60ecdfd

46 files changed

Lines changed: 3179 additions & 1848 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

REVIEW.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ findings there are edge cases. The areas with the most serious problems are:
253253
| ✅ A2 | HIGH | `boxed-expression/boxed-function.ts:853` | `ln()` of `Root(a,b)` computes the reciprocal: `b.div(a.ln(base))` instead of `a.ln(base).div(b)`. **✓ verified:** `Root(x,3).ln()``3/ln(x)`. Fixed: now `(1/3)·ln(x)`. Regression test in `arithmetic.test.ts` → "Ln of Root (REVIEW.md A2)". |
254254
| ✅ A3 | HIGH | `boxed-expression/abstract-boxed-expression.ts:636-658` | `isLess`/`isGreater`/etc. return definitive `false` when `cmp()` returns the indeterminate `'<='`/`'>='`. **✓ verified:** after `assume(y >= 3)`, `y.isGreater(3)``false` (should be `undefined`). These predicates feed sign inference engine-wide. |
255255
| ✅ A4 | HIGH | `boxed-expression/boxed-number.ts:786-791` | `canonicalNumber` returns **+∞** for a rational with −∞ numerator (inverted sign logic; denominator sign also ignored). **✓ verified:** `ce.number([-Infinity, 5])``+oo`. Fixed: result sign is now the product of numerator/denominator signs. Regression test in `numbers.test.ts` → "Rational with an infinite numerator/denominator (REVIEW.md A4)". |
256-
| A5 | HIGH | `index.ts:1000-1003` | `costFunction` setter is missing an `else`: the guard assignment is always overwritten, so any non-function value is stored and later invoked, crashing `simplify()`. |
256+
| A5 | HIGH | `index.ts:1000-1003` | `costFunction` setter is missing an `else`: the guard assignment is always overwritten, so any non-function value is stored and later invoked, crashing `simplify()`. |
257257
| ✅ A6 | MED | `boxed-expression/arithmetic-power.ts:577-605` | `root()` numeric path returns a positive real for even roots of negatives. **✓ verified:** `Root(-16, 4).N()``2` (should be NaN/complex). **✓ verified + fixed:** even root of a negative now returns the complex principal root `|a|^(1/n)·(cos(π/n)+i·sin(π/n))``Root(-16,4).N()`=√2+√2i, consistent with `Sqrt(-4)`=2i. |
258258
| ✅ A7 | MED | `boxed-number.ts:391-403`, `boxed-function.ts:866-872` | `ln(base)` silently drops non-integer bases (falls through to natural log). **✓ verified:** `ce.number(8).ln(2.5)` loses the base. `BoxedSymbol.ln` handles it correctly — the three implementations are inconsistent. **✓ verified + fixed:** BoxedNumber/BoxedFunction `ln` now honor any base — `(8).ln(2.5)`=log_2.5(8)≈2.269 (was ln 8). |
259259
| ✅ A8 | MED | `boxed-expression/boxed-symbol.ts:774-794` | Plain symbols report `isEmptyCollection: true`, `isFiniteCollection: true`, `count: 0` via `?? 0` fallbacks, contradicting the abstract-class contract (`undefined` for non-collections). **✓ verified.** **✓ verified + fixed:** removed the `??0`/`count===0`/`isFinite(count)` fallbacks; a plain symbol now returns `undefined` for `count`/`isEmptyCollection`/`isFiniteCollection`. |
@@ -292,7 +292,7 @@ findings there are edge cases. The areas with the most serious problems are:
292292
| ✅ B20 | MED | `library/trigonometry.ts:63-91` | `Degrees` canonical handler reduces literals mod 360, but the evaluate handler doesn't — the same operator denotes different values depending on whether the arg was a literal at canonicalization. **✓ verified:** `Degrees(390)` → π/6 vs 13π/6. **Fixed (2026-06-10):** removed the mod-360 reduction from the *canonical* handler (rather than adding it to evaluate) so `Degrees` is a faithful `d·π/180` conversion in both paths → both give `13π/6`. This is the correct direction: `serialize-dms.test.ts` shows range normalization is a *serialization* concern (`angleNormalization`) and that faithful negatives (`Degrees(-45.5)``-45°30'`) must be preserved; the previously-failing `@fixme` `\tan…\degree` test now passes unchanged. Test in `trigonometry.test.ts` → "Degrees is a faithful conversion (B20)". |
293293
| ✅ B21 | MED | `library/number-theory.ts:200-216` | `IsHappy` throws on negative input (`BigInt('-')`). **✓ verified + fixed (2026-06-10):** non-positive integers (`k < 1`) now return `False` (happy numbers are positive). Test in `number-theory.test.ts` → "IsHappy on non-positive input (B21)". |
294294
| ✅ B22 | MED | `library/combinatorics.ts:187-247` | `Multinomial`/`BellNumber` use machine floats: `Multinomial(20,20)``137846528820.00003`; overflow past n≈170/25. Siblings `Binomial`/`Fibonacci` already use bigint. **✓ verified + fixed (2026-06-10):** `Multinomial` uses an exact bigint factorial (integer division is exact); `BellNumber` uses the bigint Bell triangle (Aitken's array). `Multinomial(20,20)`=137846528820, `BellNumber(25)`=4638590332229999353. The now-dead float `binomial` helper was removed. Tests in `combinatorics.test.ts` → "Exact Multinomial and BellNumber (B22)". |
295-
| ⏸️ B23 | LOW | `library/statistics.ts:38-67`, `trigonometry.ts:269-298` | `Erf`/`Erfc`/`ErfInv`/`Sinc`/`Fresnel*` ignore `numericApproximation` — exact `evaluate()` returns machine floats, and high-precision engines silently get 64-bit accuracy. **⏸️ DEFERRED (2026-06-10):** a proper fix needs arbitrary-precision (BigDecimal) kernels for erf/erfc/sinc/Fresnel — substantial new numeric code, distinct in character from the per-function library fixes here. G1 already made the *machine* `Erf`/`Erfc` full double precision; the bignum/`numericApproximation` path is the remaining LOW work. |
295+
| B23 | LOW | `library/statistics.ts:38-67`, `trigonometry.ts:269-298` | `Erf`/`Erfc`/`ErfInv`/`Sinc`/`Fresnel*` ignore `numericApproximation` — exact `evaluate()` returns machine floats, and high-precision engines silently get 64-bit accuracy. **⏸️ DEFERRED (2026-06-10):** a proper fix needs arbitrary-precision (BigDecimal) kernels for erf/erfc/sinc/Fresnel — substantial new numeric code, distinct in character from the per-function library fixes here. G1 already made the *machine* `Erf`/`Erfc` full double precision; the bignum/`numericApproximation` path is the remaining LOW work. |
296296

297297
### LaTeX syntax & MathJSON
298298

@@ -554,9 +554,19 @@ verification standard as above (all reproduced at runtime):
554554
| ✅ G13 | HIGH | `boxed-expression/arithmetic-mul-div.ts:~685` (canonicalDivide) | `ce.box(['Divide', ['Add', 1, 'ImaginaryUnit'], 2])` canonicalizes to `Multiply(1/2, NaN)`. Dividing a Gaussian-integer sum by an integer destroys the value at boxing time. **✓ verified + fixed:** root cause is in `factor()` (not canonicalDivide) — its Add case takes the gcd of term coefficients, but `gcd(1, i)` = NaN (complex coeff), poisoning the result → `factor(1+i)`=NaN → `toNumericValue` returns `[1, NaN]``Multiply(1/2, NaN)`. Guarded `factor()` to leave sums with a complex coefficient (`coeff.im !== 0`) unfactored; `Divide((1+i),2)``Multiply(1/2, 1+i)` (=0.5+0.5i). Tests in `factor.test.ts` "Gaussian-integer sums (G13)". |
555555

556556
**G3 addendum (2026-06-09, fixed in working tree):** the `sets.ts` audit left one stub behind — `setMinus` (the `SetMinus` evaluate handler) unconditionally returned `EmptySet`, so `Element(x, SetMinus(...))` was always False through the evaluate path. Fixed: computes the difference for finite collections, stays symbolic otherwise; trailing set-valued operands now exclude their *members* (consistently in `contains`, `count`, and the iterator). Verified: `Element(3, CC∖{0}) → True`, `Element(0, CC∖{0}) → False`, `SetMinus({1,2,3}, {2,3}) → {1}`.
557-
| G14 | MED | comparison engine (infinity ordering) | `-∞ > -∞` evaluates to `true` (strict self-comparison of infinities). Found while fixing interval membership; literal interval containment now routes around it via numeric comparison. |
558-
| G15 | MED | `common/type/reduce.ts` (intersection of numeric primitives) | The type lattice reduces incomparable-but-overlapping numeric primitives to `nothing` — e.g. `integer ∩ finite_real = nothing`, `finite_number ∩ real = nothing` — making type-based membership refutation unsound if used naively. Workaround in sets.ts uses `'number'`-level checks only; the lattice reduction deserves a proper fix. |
557+
| G14 | MED | comparison engine (infinity ordering) | `-∞ > -∞` evaluates to `true` (strict self-comparison of infinities). Found while fixing interval membership; literal interval containment now routes around it via numeric comparison. |
558+
| G15 | MED | `common/type/reduce.ts` (intersection of numeric primitives) | The type lattice reduces incomparable-but-overlapping numeric primitives to `nothing` — e.g. `integer ∩ finite_real = nothing`, `finite_number ∩ real = nothing` — making type-based membership refutation unsound if used naively. Workaround in sets.ts uses `'number'`-level checks only; the lattice reduction deserves a proper fix. |
559559

560560
**G2 + G10 addendum (2026-06-10, fixed in working tree by the Fungrim thread):**
561561
- **G2 fixed** — three compounding defects: the `_x` binding mismatch across solve passes (all passes now share one literal-`_x` convention with root back-substitution), `captureWildcard`'s blanket rejection (now rejects only *unbound*-wildcard captures), and two masking bugs (`clearDenominators` mangling `e^x` via the trivial `1^x` denominator; `matchVariations`' Exp case built backwards). Harmonization now chains (depth 4), `Equal` gets injective-wrapper peeling, and two dead built-in harmonization rules had wildcard typos fixed. `e^x=5 → ln 5`, `10^x=100 → 2`, `ln x = ln 3 → 3` exactly; 3 oracle solve snapshots improved ([] → correct root).
562562
- **G10 fixed** — Set comprehensions are first-class: conservative builder-vs-literal disambiguation (Fungrim `Element` indexing-set form + both LaTeX `Condition` forms), comprehension-aware count/iterator/contains (three-valued, deduped, 1000-value enumeration cap), lazy Set evaluation so the condition isn't pre-evaluated. `Count{k∈Range(1,n): gcd(n,k)=1}` = Totient(n) verified n=2..8; literal sets byte-identical.
563+
564+
565+
**Final round addendum (2026-06-10, Fungrim thread):**
566+
- **A5 ✅** costFunction setter guarded (`typeof fn === 'function' ? fn : undefined`).
567+
- **G14 ✅** NaN/equal-infinity comparisons fixed in all three `cmp()` paths (strict self-comparison of ±∞ now false, non-strict true, NaN → undefined, identical at machine/bignum). Follow-on: explicit NaN absorption added to `evaluateMinMax` (Min/Max with a NaN operand → NaN, now symmetric; 1 snapshot updated).
568+
- **G15 ✅** principled pairwise meet from the transitive closure of PRIMITIVE_SUBTYPES (`integer ∩ finite_real = finite_integer`, …), + repaired a latent transitivity hole (`imaginary ⊄ finite_number`). 309-test lattice suite.
569+
- **D6 ✅** BigDecimal transcendental bridge rearchitected: decimal-exponent factoring for exp/ln/sqrt/cbrt, small-argument relative-precision policy for trig/hyperbolics, huge-argument saturation, fpln 0-guard, pow guard digits, honest NaN beyond the stored-π trig-reduction range. `ln(1e-100)` terminates; `exp(−200)` exact to 50 digits.
570+
- **B23 ✅** bignum kernels for Erf/Erfc/ErfInv/Sinc/FresnelS/C with `numericApproximation` gating (exact mode stays symbolic); full precision at p=40/100, no machine fallback needed. Opportunistic: machine erfInv rewritten (was ~4 digits mid-range), machine Fresnel cutoff fixed (36 → 36974; fresnelS(50) had ~2 correct digits), complex Erf args now stay symbolic (were computing erf(Re z)).
571+
- **Performance (section c) ✅** LaTeX parsing **~12.6×** faster (dictionary indexing, single-parse symbol triggers, lookAhead cache, sticky-regex tokenizer — ~200× on non-ASCII symbol-heavy input — hoisted constants), byte-identical output across 1,330 snapshots. Type system: parseType memoized (**~200×**), string-operand isSubtype ~29×, widen ~6×; legacy TypeParser deleted (−1,080 net lines).
572+
- **Feature:** exact `Zeta` at integer literals (ζ(2k) → rational·π^2k via exact Bernoulli; ζ(−n) rationals; ζ(−2k) → 0; cap |s| ≤ 100) + injected symbolic rule `fungrim:zeta-trivial-zeros` (artifact → 1,350 rules); 2 baseline snapshots updated as strictly-better (ascii-math sign-consistency; canonical-form last digit now correctly rounded per D6).

scripts/fungrim/compile-rules.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ export type CurationOverride = {
108108
export type CurationOverrides = {
109109
overrides?: Record<string, CurationOverride>;
110110
transformAllowlist?: string[];
111+
/**
112+
* Hand-curated synthetic entries appended to the corpus slice. Each entry
113+
* is corpus-`Entry`-shaped and goes through the FULL compile pipeline
114+
* (guard compilation, orientation, canonicalization, dedup, self-test) —
115+
* injection adds candidates, it never bypasses the machinery. Use for
116+
* sound identities missing from the upstream corpus (e.g. the trivial
117+
* zeros of Zeta). Ids must not collide with corpus entry ids.
118+
*/
119+
inject?: Entry[];
111120
solveSeeds?: Record<string, { target: 'solve'; note: string }>;
112121
};
113122

@@ -1557,10 +1566,15 @@ function main(): void {
15571566
const reportPath = path.join(scriptDir, 'rule-compile-report.json');
15581567

15591568
const corpus = loadCorpus(corpusDir);
1560-
const slice = corpus.entries.filter(isSliceEntry);
15611569
const overrides: CurationOverrides = fs.existsSync(overridesPath)
15621570
? JSON.parse(fs.readFileSync(overridesPath, 'utf8'))
15631571
: {};
1572+
// Curated injected entries are appended to the slice and compiled through
1573+
// the full pipeline (guards, orientation, self-test) like corpus entries.
1574+
const slice = [
1575+
...corpus.entries.filter(isSliceEntry),
1576+
...(overrides.inject ?? []),
1577+
];
15641578

15651579
const started = Date.now();
15661580
const result = compileEntries(slice, corpus.declarations, overrides);
@@ -1619,6 +1633,7 @@ function main(): void {
16191633
Object.entries(headBuckets).sort(([, a], [, b]) => b - a)
16201634
),
16211635
declaredShells: Object.keys(result.declarations).length,
1636+
injected: (overrides.inject ?? []).map((e) => e.id).sort(),
16221637
solveSeeds: Object.keys(overrides.solveSeeds ?? {}).sort(),
16231638
skips: result.skips,
16241639
};

scripts/fungrim/curation-overrides.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$comment": "Hand-maintained curation overrides for the Fungrim Phase-1 rule compiler (FUNGRIM-PLAN-5-LOADER.md \u00a72.3/\u00a72.6). Merged last by scripts/fungrim/compile-rules.ts. `overrides` entries ({ direction?, purpose?, target?, exclude?, note? }, keyed by corpus entry id) adjust or exclude compiled artifact rules. `transformAllowlist` promotes hand-vetted growth-neutral canonicalizations from 'expand' to 'transform' (the machine policy never emits 'transform'). `solveSeeds` is the \u00a72.6 hand-curated solve-template seed set: it is NOT compiled into the M1 artifact and does not affect default loading \u2014 the M2 runtime loader compiles these entries into UNIVARIATE_ROOTS-style templates behind loadFungrim(ce, { solve: true }) (off by default, Q7).",
2+
"$comment": "Hand-maintained curation overrides for the Fungrim Phase-1 rule compiler (FUNGRIM-PLAN-5-LOADER.md \u00a72.3/\u00a72.6). Merged last by scripts/fungrim/compile-rules.ts. `overrides` entries ({ direction?, purpose?, target?, exclude?, note? }, keyed by corpus entry id) adjust or exclude compiled artifact rules. `transformAllowlist` promotes hand-vetted growth-neutral canonicalizations from 'expand' to 'transform' (the machine policy never emits 'transform'). `inject` is a list of hand-curated corpus-Entry-shaped synthetic entries appended to the slice and compiled through the FULL pipeline (guards, orientation, self-test) \u2014 for sound identities missing from the upstream corpus. `solveSeeds` is the \u00a72.6 hand-curated solve-template seed set: it is NOT compiled into the M1 artifact and does not affect default loading \u2014 the M2 runtime loader compiles these entries into UNIVARIATE_ROOTS-style templates behind loadFungrim(ce, { solve: true }) (off by default, Q7).",
33
"overrides": {
44
"304559": {
55
"exclude": true,
@@ -23,6 +23,22 @@
2323
}
2424
},
2525
"transformAllowlist": [],
26+
"inject": [
27+
{
28+
"id": "zeta-trivial-zeros",
29+
"formula": ["Equal", ["Zeta", ["Multiply", -2, "n"]], 0],
30+
"variables": ["n"],
31+
"assumptions": ["Element", "n", "PositiveIntegers"],
32+
"class": "specific-value",
33+
"subclass": null,
34+
"heads": ["Zeta"],
35+
"guardLevel": "real-simple",
36+
"flavor": null,
37+
"references": "Curated injection (not in the upstream corpus slice): the trivial zeros of the Riemann zeta function, Zeta(-2n) = 0 for positive integer n. The Track-3 guard machinery discharges the positive-integer guard for symbolic n (declared integer + assumed n > 0); literal arguments are covered by the exact Zeta evaluate path in library/arithmetic.ts.",
38+
"topics": ["riemann_zeta"],
39+
"topic": "riemann_zeta"
40+
}
41+
],
2642
"solveSeeds": {
2743
"296627": {
2844
"target": "solve",

scripts/fungrim/rule-compile-report.json

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"generator": "scripts/fungrim/compile-rules.ts",
3-
"sliceEntries": 1647,
4-
"emitted": 1349,
3+
"sliceEntries": 1648,
4+
"emitted": 1350,
55
"ledger": {
66
"box-error": 16,
77
"compat-signature": 42,
@@ -14,18 +14,18 @@
1414
},
1515
"byPurpose": {
1616
"expand": 109,
17-
"simplify": 1240
17+
"simplify": 1241
1818
},
1919
"byClass": {
2020
"identity": 1025,
21-
"specific-value": 324
21+
"specific-value": 325
2222
},
2323
"byTarget": {
24-
"simplify": 1349
24+
"simplify": 1350
2525
},
2626
"sampleKinds": {
2727
"numeric": 41,
28-
"symbolic": 1308
28+
"symbolic": 1309
2929
},
3030
"headBuckets": {
3131
"Multiply": 256,
@@ -89,6 +89,7 @@
8989
"StieltjesGamma": 3,
9090
"Totient": 3,
9191
"WeierstrassP": 3,
92+
"Zeta": 3,
9293
"Arctan2": 2,
9394
"ArgMin": 2,
9495
"BernoulliPolynomial": 2,
@@ -99,7 +100,6 @@
99100
"Max": 2,
100101
"Sign": 2,
101102
"XGCD": 2,
102-
"Zeta": 2,
103103
"AGMSequence": 1,
104104
"Arccot": 1,
105105
"Arcsin": 1,
@@ -126,6 +126,9 @@
126126
"WeierstrassZeta": 1
127127
},
128128
"declaredShells": 112,
129+
"injected": [
130+
"zeta-trivial-zeros"
131+
],
129132
"solveSeeds": [
130133
"1f026d",
131134
"296627",

src/big-decimal/big-decimal.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -775,8 +775,29 @@ export class BigDecimal {
775775
// Negative base with non-integer exponent → NaN (not real-valued)
776776
if (this.significand < 0n) return BigDecimal.NAN;
777777

778-
// Positive base, non-integer exponent: exp(n * ln(this))
779-
return n.mul(this.ln()).exp();
778+
// Positive base, non-integer exponent: exp(n · ln(this)).
779+
//
780+
// ln() is computed to `precision` *relative* digits, so its absolute
781+
// error scales with |ln(this)|; multiplied by n it becomes the result's
782+
// relative error — losing ~log10(|n·ln(this)|) digits (e.g. 10^100.5
783+
// was correct to only ~47 of 50 digits). Temporarily carry that many
784+
// extra digits, capped at 20: beyond |n·ln(this)| ≈ 10^17 the result's
785+
// decimal exponent exceeds the representable bound and exp() saturates
786+
// to 0/Infinity anyway.
787+
const baseSig = this.significand; // positive here
788+
const decExpBase = this.exponent + bigintDigits(baseSig) - 1;
789+
const nSig = n.significand < 0n ? -n.significand : n.significand;
790+
const decExpN = n.exponent + bigintDigits(nSig) - 1;
791+
const argMag = decExpN + Math.log10(Math.abs(decExpBase) * 2.303 + 3) + 1;
792+
const extra = Math.min(20, Math.max(2, Math.ceil(argMag) + 2));
793+
794+
const savedPrec = BigDecimal.precision;
795+
BigDecimal.precision = savedPrec + extra;
796+
try {
797+
return n.mul(this.ln()).exp().toPrecision(savedPrec);
798+
} finally {
799+
BigDecimal.precision = savedPrec;
800+
}
780801
}
781802

782803
// ---------- Conversion methods ----------

0 commit comments

Comments
 (0)