@fitchmultz/pi-calculator 0.0.0-stage → 4.1.1
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/CHANGELOG.md +92 -0
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/decimal.ts +91 -0
- package/eval.ts +452 -0
- package/expr-eval-fork.d.ts +8 -0
- package/index.ts +46 -0
- package/package.json +69 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 4.1.1 - 2026-10-04
|
|
4
|
+
|
|
5
|
+
- Publish the existing calculator as the public scoped package `@fitchmultz/pi-calculator`, continuing the 4.1.x version line. The unrelated unscoped npm package is not this project.
|
|
6
|
+
- Lead installation guidance with the scoped npm source while preserving Git installation, existing tags, and the calculator's numerical, schema, structured-result, rendering, and native-composition contracts.
|
|
7
|
+
- Qualify the latest stable official Pi and maintained fork main with once-frozen host identities, and add a main-only gated repository release pipeline.
|
|
8
|
+
|
|
9
|
+
## 4.1.0 - 2026-10-01
|
|
10
|
+
|
|
11
|
+
- Require Pi 1.0.0 and qualify its exact published cohort with host TypeBox 1.3.27.
|
|
12
|
+
- Expose the existing expression/value outcome through native structured output for nested callers, preserving decimal strings, scalar/array behavior, grammar, work limits, errors, and rendering.
|
|
13
|
+
- Exercise native nested scalar/array receipts and distinct validation, policy, and evaluation failures without provider calls. Keep Git/GitHub-only distribution.
|
|
14
|
+
|
|
15
|
+
## 4.0.0 - 2026-09-26
|
|
16
|
+
|
|
17
|
+
### Breaking changes
|
|
18
|
+
|
|
19
|
+
- Require Node.js 24 or later.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Qualify development against Pi 0.87.1 and the TypeBox version Pi ships (1.3.27), on Node 24 for both official Pi and the maintained fork.
|
|
24
|
+
- Type-check with Node 24 types and explicit Node module resolution under TypeScript 7.
|
|
25
|
+
|
|
26
|
+
## 3.0.0 - 2026-09-21
|
|
27
|
+
|
|
28
|
+
### Breaking changes
|
|
29
|
+
|
|
30
|
+
- Replace `deg(x)` with `radians(x)` for degrees-to-radians conversion, and `rad(x)` with `degrees(x)` for radians-to-degrees conversion. The old names are removed without aliases.
|
|
31
|
+
- Return value-only model content instead of `expression = value`. Details retain the trimmed expression and a value that can now be a decimal string or an array of decimal strings.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Evaluate independent calculations in one flat array. Reject nested, nonnumeric, or nonfinite results as a whole, and reject serialized expression/value details larger than 50 KiB.
|
|
36
|
+
- Show safely escaped expressions in native Pi tool cards while keeping model output concise.
|
|
37
|
+
- Request native strict-prefer JSON-schema sampling with provider-controlled fallback.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Pass Decimal values directly through the parser while preserving decimal-literal precision, numerical algorithms, private contexts, and work limits.
|
|
42
|
+
- Clarify percentage argument order, population/sample standard deviation, logarithm bases, small-argument `expm1`/`log1p`, and when to combine calculations or skip a trivial tool call.
|
|
43
|
+
- Qualify development against Pi 0.87.0 and TypeBox 1.3.34; declare host-provided Pi TUI as an optional peer. The package remains private and Git-distributed.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Require finite inputs consistently for aggregates, including `min`, `max`, and `hypot`/`pyt`.
|
|
48
|
+
|
|
49
|
+
## 2.0.2 - 2026-09-18
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- Preserve tangent accuracy near poles by dividing guarded sine and cosine values.
|
|
54
|
+
- Preserve cancellation remainders in sums without allocating across large exponent gaps, and round means and even medians only after division.
|
|
55
|
+
- Center standard-deviation calculations before averaging so identical measurements have zero spread and large common offsets do not distort the result.
|
|
56
|
+
- Reject non-finite measurements in statistics, including medians that previously hid invalid square roots or division by zero.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- Regression checks for numeric correctness and GitHub CI verification on the minimum supported Node version and Node 24.
|
|
61
|
+
|
|
62
|
+
## 2.0.1 - 2026-08-08
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- Restrict evaluation to null-prototype deterministic allowlists instead of inheriting broken parser functions, operators, or object properties.
|
|
67
|
+
- Evaluate supported scientific helpers in a private Decimal context, including cancellation-safe `expm1`/`log1p`, hyperbolic functions, `pow`, `atan2`, `sum`, `sign`, and `trunc`.
|
|
68
|
+
- Return the requested element for array indexes instead of always coercing wrapped indexes to zero.
|
|
69
|
+
- Preserve single-quoted numeric strings and quoted `**` text during literal rewriting, and reject unsupported comments.
|
|
70
|
+
- Accept negative zero factorials, correctly round factorials once from an exact BigInt result, and reject extra function arguments.
|
|
71
|
+
- Restore the full 40-digit `E` constant, round input literals to the documented precision, reset Decimal state after every evaluation, cap nesting deterministically, and replace raw dependency errors with stable calculator errors.
|
|
72
|
+
- Disable member access and unadvertised conditional, comparison, logical, higher-order, random, string-escape, comment, and multi-expression operations.
|
|
73
|
+
- Bound circular and hyperbolic trig operands plus modulo exponent gaps with a shared work budget to prevent synchronous CPU amplification.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- Keep only stable expression and value fields in tool details and trim always-on prompt guidance.
|
|
78
|
+
- Limit npm package contents to runtime source and documentation.
|
|
79
|
+
|
|
80
|
+
## 2.0.0 - 2026-08-06
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- Require Pi 0.84.0 or later.
|
|
85
|
+
- Import the host-provided TypeBox package directly, following the Pi 0.84 extension contract.
|
|
86
|
+
- Format very large integers in scientific notation so calculator output stays below Pi's 50KB tool-output limit.
|
|
87
|
+
- Keep evaluation details JSON-safe by removing the lossy JavaScript number conversion.
|
|
88
|
+
- Escape line breaks in displayed expressions so tool results stay within Pi's 2,000-line output limit.
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
|
|
92
|
+
- Type checking and an actual Pi 0.84 extension-load check to the release verification command.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mitchell Fultz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-calculator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A deterministic calculator tool for [Pi](https://github.com/earendil-works/pi) with 40-significant-digit decimal precision. Input literals and arithmetic are rounded using half-up rounding; results are decimal strings, not lossless symbolic answers.
|
|
4
|
+
|
|
5
|
+
Requires Node.js 24 or later and Pi 1.0.0 or later. Release qualification selects the latest stable official Pi and the [fitchmultz/pi fork](https://github.com/fitchmultz/pi) main once per run, freezing their version and commit throughout the checks. Each host is qualified separately; a matching version string is not a compatibility result.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pi install npm:@fitchmultz/pi-calculator
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**The unscoped `pi-calculator` npm package is NOT this project.** This repository publishes only `@fitchmultz/pi-calculator`.
|
|
14
|
+
|
|
15
|
+
Git remains supported as a fallback:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pi install git:github.com/fitchmultz/pi-calculator
|
|
19
|
+
# Existing versioned tags remain installable:
|
|
20
|
+
pi install git:github.com/fitchmultz/pi-calculator@v4.1.0
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When switching from Git to npm, remove the exact Git source shown by `pi list` before installing the scoped package to avoid loading the calculator twice. Existing Git installs can stay on Git.
|
|
24
|
+
|
|
25
|
+
The package adds one `calculator` tool with one required `expression` string. Pi supplies the schema and terminal UI libraries; the calculator uses `decimal.js` and `expr-eval-fork` for evaluation.
|
|
26
|
+
|
|
27
|
+
## Expressions
|
|
28
|
+
|
|
29
|
+
Use the calculator for non-trivial calculations; simple arithmetic does not need a tool call. Independent results can share one flat array:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
(12.5 * 1.0825) ^ 3
|
|
33
|
+
sin(radians(90))
|
|
34
|
+
percent(15, 200)
|
|
35
|
+
mean([2,4,6,8])
|
|
36
|
+
[stdev([2,4,4,4,5,5,7,9]), stdevs([2,4,4,4,5,5,7,9])]
|
|
37
|
+
[0.1 + 0.2, 2^64]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Operators include `+`, `-`, `*`, `/`, `%`, `^` and `**`; constants are `PI` and `E`. Arrays support zero-based indexing, such as `[1,2,3][2]`.
|
|
41
|
+
|
|
42
|
+
Supported functions include:
|
|
43
|
+
|
|
44
|
+
- `sin`, `cos`, `tan`, their inverse/hyperbolic variants, and `atan2`. Trig uses radians. `radians(degrees)` and `degrees(radians)` convert angles.
|
|
45
|
+
- `sqrt`, `cbrt`, `abs`, `pow`, `exp`, `expm1`, `ln`/`log`, `log1p`, `log2`, `log10`/`lg`. `ln` and `log` are natural logarithms. For small inputs, use `expm1(x)` for `exp(x)-1` and `log1p(x)` for `ln(1+x)` to preserve precision.
|
|
46
|
+
- `ceil`, `floor`, `round`, `roundTo`, `trunc`, `sign`. `roundTo(value, places)` rounds halves away from zero; negative places round to tens, hundreds, and so on.
|
|
47
|
+
- `percent(rate, amount)` gives `rate%` of `amount`: `percent(15, 200)` is `30`.
|
|
48
|
+
- `sum`, `mean`, `median`, `stdev`, `stdevs` take numeric arrays. `stdev` is population standard deviation; `stdevs` is sample standard deviation and requires at least two elements.
|
|
49
|
+
- `min`, `max`, `hypot`/`pyt` take either an array or individual arguments.
|
|
50
|
+
- `n!` and `fac(n)` compute factorials for integers from 0 through 1000.
|
|
51
|
+
|
|
52
|
+
Aggregates require finite numeric elements. Sums preserve cancellation remainders before rounding; means and even medians round after division.
|
|
53
|
+
|
|
54
|
+
## Results and limits
|
|
55
|
+
|
|
56
|
+
The tool returns a finite scalar or flat array of finite scalars, represented as decimal strings. A nested array, nonnumeric result, or nonfinite member fails the whole expression. Large finite values use scientific notation.
|
|
57
|
+
|
|
58
|
+
Model-facing content contains only the value, such as `0.3`, or a JSON array of strings, such as `["0.3","18446744073709551616"]`. Structured details retain `{ expression, value }`, where `expression` is trimmed and `value` is a string or string array. The same stable outcome is exposed as native `structuredContent` with an `outputSchema`, so nested/codemode callers receive decimal strings without reparsing display text. Evaluation failures still throw and become native error results; schema and policy checks remain Pi-owned. Pi's tool card displays the safely escaped expression above the result.
|
|
59
|
+
|
|
60
|
+
Expressions are limited to 4,096 characters and 128 nesting levels. Serialized expression/value details must fit within 50 KiB; oversized results are rejected rather than truncated. The tool requests native strict JSON-schema sampling where the provider supports it and uses Pi's normal fallback elsewhere.
|
|
61
|
+
|
|
62
|
+
Each expression has a shared 1,000-step factorial budget. `sin`/`cos`/`tan` reject absolute inputs above `1e100`, hyperbolic functions reject absolute inputs above 10,000, and `%` rejects operand exponent gaps above 10,000. Expensive operations share a 10,000-unit work budget. Circular trig calls cost 100 units each, allowing at most 100 per expression.
|
|
63
|
+
|
|
64
|
+
### Upgrading from v2
|
|
65
|
+
|
|
66
|
+
Version 3 removes the old angle-conversion names: replace `deg(x)` with `radians(x)` and `rad(x)` with `degrees(x)`. There are no compatibility aliases. Tool content no longer includes `expression =`; consumers should read the expression from details and handle both string and string-array values.
|
|
67
|
+
|
|
68
|
+
## Verification
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm ci --ignore-scripts
|
|
72
|
+
npm run verify
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Verification includes numerical and work-limit checks, schema and structured result contracts, actual extension loading, native nested validation/policy/evaluation errors, and native tool-card rendering. The exact development cohort uses TypeBox 1.3.27; no evaluator or grammar compatibility layer is needed.
|
|
76
|
+
|
|
77
|
+
Update the development TypeBox pin alongside the qualified Pi host, using the version that host ships. Independent TypeBox updates are disabled in Renovate because they install duplicate host schema libraries and slow development startup. Each compatibility lane still selects its host's TypeBox version.
|
|
78
|
+
|
|
79
|
+
The locked development cohort is reproducible local tooling, not a release-qualification target. CI runs the full verification against both frozen latest hosts, including fresh Git and packed npm consumers. Intentional version bumps with versioned changelog notes publish from the main-only repository pipeline when `NPM_RELEASE_ENABLED` is `true`; the release caller reuses the same frozen host inputs for qualification and publishing.
|
|
80
|
+
|
|
81
|
+
Pi extensions execute with full system access. Review the source before installing.
|
package/decimal.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Decimal as DecimalBase } from "decimal.js";
|
|
2
|
+
|
|
3
|
+
export const DECIMAL_PRECISION = 40;
|
|
4
|
+
export const MAX_EXPRESSION_DEPTH = 128;
|
|
5
|
+
const DECIMAL_CONFIG = {
|
|
6
|
+
defaults: true,
|
|
7
|
+
precision: DECIMAL_PRECISION,
|
|
8
|
+
rounding: DecimalBase.ROUND_HALF_UP,
|
|
9
|
+
} as const;
|
|
10
|
+
export const Decimal = DecimalBase.clone(DECIMAL_CONFIG);
|
|
11
|
+
export type DecimalValue = DecimalBase;
|
|
12
|
+
|
|
13
|
+
export function resetDecimal(): void {
|
|
14
|
+
Decimal.set(DECIMAL_CONFIG);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DECIMAL_LITERAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
18
|
+
|
|
19
|
+
function decimalFromString(value: string): DecimalValue {
|
|
20
|
+
if (!DECIMAL_LITERAL.test(value)) throw new Error("invalid decimal literal");
|
|
21
|
+
const decimal = new Decimal(value);
|
|
22
|
+
if (!decimal.isFinite()) throw new Error("decimal literal overflow");
|
|
23
|
+
if (decimal.isZero() && /[1-9]/.test(value.replace(/[eE].*$/, ""))) {
|
|
24
|
+
throw new Error("decimal literal underflow");
|
|
25
|
+
}
|
|
26
|
+
const rounded = decimal.toSignificantDigits(DECIMAL_PRECISION);
|
|
27
|
+
if (!rounded.isFinite()) throw new Error("decimal literal overflow");
|
|
28
|
+
if (rounded.isZero() && !decimal.isZero()) throw new Error("decimal literal underflow");
|
|
29
|
+
return rounded;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toDec(value: unknown): DecimalValue {
|
|
33
|
+
if (value instanceof Decimal) return value;
|
|
34
|
+
if (typeof value === "string") return decimalFromString(value);
|
|
35
|
+
if (typeof value === "number") {
|
|
36
|
+
if (!Number.isFinite(value)) throw new Error("non-finite number");
|
|
37
|
+
return new Decimal(String(value));
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`expected number, got ${typeof value}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Preserve numeric literal precision and normalize ** outside quoted strings. */
|
|
43
|
+
export function decimalizeExpression(expression: string): string {
|
|
44
|
+
if (expression.includes("\\")) throw new Error("escape sequences are not supported");
|
|
45
|
+
|
|
46
|
+
let result = "";
|
|
47
|
+
let i = 0;
|
|
48
|
+
let nestingDepth = 0;
|
|
49
|
+
|
|
50
|
+
while (i < expression.length) {
|
|
51
|
+
const ch = expression[i]!;
|
|
52
|
+
|
|
53
|
+
if (ch === '"' || ch === "'") {
|
|
54
|
+
const end = expression.indexOf(ch, i + 1);
|
|
55
|
+
if (end < 0) throw new Error("unclosed string in expression");
|
|
56
|
+
result += expression.slice(i, end + 1);
|
|
57
|
+
i = end + 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ch === "/" && expression[i + 1] === "*") throw new Error("comments are not supported");
|
|
62
|
+
if (ch === ";") throw new Error("multiple expressions are not supported");
|
|
63
|
+
|
|
64
|
+
if (ch === "(" || ch === "[") {
|
|
65
|
+
nestingDepth++;
|
|
66
|
+
if (nestingDepth > MAX_EXPRESSION_DEPTH) {
|
|
67
|
+
throw new Error(`expression is too deeply nested (max ${MAX_EXPRESSION_DEPTH})`);
|
|
68
|
+
}
|
|
69
|
+
} else if ((ch === ")" || ch === "]") && nestingDepth > 0) {
|
|
70
|
+
nestingDepth--;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (ch === "*" && expression[i + 1] === "*") {
|
|
74
|
+
result += "^";
|
|
75
|
+
i += 2;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const match = expression.slice(i).match(/^(?:\d+\.\d+|\d+\.|\.\d+|\d+)(?:[eE][+-]?\d+)?/);
|
|
80
|
+
if (match && (i === 0 || !/[\w.]/.test(expression[i - 1]!))) {
|
|
81
|
+
result += `d("${match[0]}")`;
|
|
82
|
+
i += match[0].length;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
result += ch;
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
package/eval.ts
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { Parser } from "expr-eval-fork";
|
|
2
|
+
import { DECIMAL_PRECISION, Decimal, decimalizeExpression, resetDecimal, toDec, type DecimalValue } from "./decimal.ts";
|
|
3
|
+
|
|
4
|
+
export const MAX_EXPRESSION_LENGTH = 4096;
|
|
5
|
+
const MAX_FACTORIAL_OPERAND = 1000;
|
|
6
|
+
const MAX_FACTORIAL_WORK = MAX_FACTORIAL_OPERAND;
|
|
7
|
+
const MAX_ROUND_DIGITS = 1_000_000_000;
|
|
8
|
+
const MAX_HYPERBOLIC_ABS = 10_000;
|
|
9
|
+
const MAX_MODULO_EXPONENT_GAP = 10_000;
|
|
10
|
+
const MAX_EXPENSIVE_WORK = 10_000;
|
|
11
|
+
const TRIG_WORK = 100;
|
|
12
|
+
const PI = new Decimal("3.141592653589793238462643383279502884197");
|
|
13
|
+
const E = new Decimal("2.718281828459045235360287471352662497757");
|
|
14
|
+
const MAX_TRIG_ABS = new Decimal("1e100");
|
|
15
|
+
const SMALL_ATAN_ABS = new Decimal("1e-100");
|
|
16
|
+
const LARGE_ATAN_ABS = new Decimal("1e100");
|
|
17
|
+
const GUARD_DECIMAL_CONFIG = {
|
|
18
|
+
defaults: true,
|
|
19
|
+
precision: DECIMAL_PRECISION + 10,
|
|
20
|
+
rounding: Decimal.ROUND_HALF_UP,
|
|
21
|
+
} as const;
|
|
22
|
+
const GuardDecimal = Decimal.clone(GUARD_DECIMAL_CONFIG);
|
|
23
|
+
|
|
24
|
+
let factorialWorkLeft = 0;
|
|
25
|
+
let expensiveWorkLeft = 0;
|
|
26
|
+
const EMPTY_VARIABLES: Record<string, never> = Object.create(null);
|
|
27
|
+
|
|
28
|
+
const parser = new Parser({
|
|
29
|
+
allowMemberAccess: false,
|
|
30
|
+
operators: {
|
|
31
|
+
assignment: false,
|
|
32
|
+
comparison: false,
|
|
33
|
+
concatenate: false,
|
|
34
|
+
conditional: false,
|
|
35
|
+
fndef: false,
|
|
36
|
+
in: false,
|
|
37
|
+
logical: false,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function nullMap<T extends object>(values: T): T {
|
|
42
|
+
return Object.assign(Object.create(null), values);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function requireArity(name: string, args: unknown[], count: number): void {
|
|
46
|
+
if (args.length !== count) {
|
|
47
|
+
throw new Error(`${name}() needs exactly ${count} argument${count === 1 ? "" : "s"}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function factorial(value: unknown) {
|
|
52
|
+
const n = toDec(value);
|
|
53
|
+
if (!n.isInteger() || n.lt(0)) throw new Error("factorial needs a non-negative integer");
|
|
54
|
+
if (n.gt(MAX_FACTORIAL_OPERAND)) throw new Error(`factorial operand too large (max ${MAX_FACTORIAL_OPERAND})`);
|
|
55
|
+
|
|
56
|
+
const count = n.toNumber();
|
|
57
|
+
const cost = Math.max(count, 1);
|
|
58
|
+
if (cost > factorialWorkLeft) throw new Error("factorial work budget exceeded");
|
|
59
|
+
factorialWorkLeft -= cost;
|
|
60
|
+
|
|
61
|
+
let result = 1n;
|
|
62
|
+
for (let i = 2; i <= count; i++) result *= BigInt(i);
|
|
63
|
+
return new Decimal(result.toString()).toSignificantDigits(DECIMAL_PRECISION);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function decimals(values: unknown, name: string, minLength = 1): DecimalValue[] {
|
|
67
|
+
if (!Array.isArray(values) || values.length < minLength) {
|
|
68
|
+
const need = minLength === 1 ? "a non-empty number array" : `at least ${minLength} numbers`;
|
|
69
|
+
throw new Error(`${name}() needs ${need}`);
|
|
70
|
+
}
|
|
71
|
+
return values.map((value, index) => {
|
|
72
|
+
try {
|
|
73
|
+
const number = toDec(value);
|
|
74
|
+
if (!number.isFinite()) throw new Error("expected a finite number");
|
|
75
|
+
return number;
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error(`${name}(): invalid number at index ${index}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function decimalArguments(args: unknown[], name: string): DecimalValue[] {
|
|
83
|
+
const values = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
|
84
|
+
if (values.length === 0) throw new Error(`${name}() needs at least one number`);
|
|
85
|
+
return decimals(values, name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sum(xs: DecimalValue[], Ctor = Decimal, divisor = 1): DecimalValue {
|
|
89
|
+
if (xs.some((x) => !x.isFinite())) return Ctor.sum(...xs).div(divisor);
|
|
90
|
+
|
|
91
|
+
// Decimal.sum also truncates across large exponent gaps. Signed BigInt chunks
|
|
92
|
+
// keep the exact sum sparse, including intermediate values outside Decimal's range.
|
|
93
|
+
const width = BigInt(Ctor.precision);
|
|
94
|
+
const base = 10n ** width;
|
|
95
|
+
const bins = new Map<bigint, bigint>();
|
|
96
|
+
const add = (key: bigint, value: bigint) => bins.set(key, (bins.get(key) ?? 0n) + value);
|
|
97
|
+
for (const x of xs) {
|
|
98
|
+
if (x.isZero()) continue;
|
|
99
|
+
const digits = x.toExponential().split("e")[0]!.replace(".", "");
|
|
100
|
+
const exponent = BigInt(x.e) - BigInt(x.sd() - 1);
|
|
101
|
+
const key = exponent >= 0n ? exponent / width : (exponent - width + 1n) / width;
|
|
102
|
+
add(key, BigInt(digits) * 10n ** (exponent - key * width));
|
|
103
|
+
}
|
|
104
|
+
for (let key of [...bins.keys()].sort((a, b) => Number(a - b))) {
|
|
105
|
+
for (;;) {
|
|
106
|
+
const value = bins.get(key)!;
|
|
107
|
+
const carry = value / base;
|
|
108
|
+
bins.set(key, value % base);
|
|
109
|
+
if (!carry) break;
|
|
110
|
+
add(++key, carry);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const keys = [...bins.keys()].filter((key) => bins.get(key) !== 0n).sort((a, b) => Number(b - a));
|
|
115
|
+
if (keys.length === 0) return new Ctor(0);
|
|
116
|
+
let key = keys[0]!;
|
|
117
|
+
let coefficient = bins.get(key)!;
|
|
118
|
+
let i = 1;
|
|
119
|
+
const digitCount = (n: bigint) => n.toString().replace("-", "").length;
|
|
120
|
+
const retained = Ctor.precision + Math.ceil(Math.log10(divisor)) + 2;
|
|
121
|
+
for (; i < keys.length; i++) {
|
|
122
|
+
const next = keys[i]!;
|
|
123
|
+
if (digitCount(coefficient) >= retained || key - next > 2n) break;
|
|
124
|
+
coefficient = coefficient * base ** (key - next) + bins.get(next)!;
|
|
125
|
+
key = next;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Normalized chunks have magnitude < base, so the leading remaining chunk
|
|
129
|
+
// determines the tail's sign. A sticky digit preserves which side of a tie it lies on.
|
|
130
|
+
let shift = 0n;
|
|
131
|
+
if (i < keys.length) {
|
|
132
|
+
shift = BigInt(Math.max(2, retained - digitCount(coefficient)));
|
|
133
|
+
coefficient = coefficient * 10n ** shift + (bins.get(keys[i]!)! > 0n ? 1n : -1n);
|
|
134
|
+
}
|
|
135
|
+
// Divide and round before restoring the exponent to avoid intermediate overflow/underflow.
|
|
136
|
+
const [mantissa, exponent] = new Ctor(coefficient.toString()).div(divisor).toExponential().split("e");
|
|
137
|
+
return new Ctor(`${mantissa}e${BigInt(exponent!) + key * width - shift}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function mean(xs: DecimalValue[]): DecimalValue {
|
|
141
|
+
return sum(xs, Decimal, xs.length);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function standardDeviation(xs: DecimalValue[], sample: boolean): DecimalValue {
|
|
145
|
+
// Scale before subtraction; a power of ten preserves small spreads around a large offset.
|
|
146
|
+
const scale = new GuardDecimal(`1e${GuardDecimal.max(...xs.map((x) => x.abs())).e}`);
|
|
147
|
+
const scaled = xs.map((x) => new GuardDecimal(x).div(scale));
|
|
148
|
+
// Center on an input first so a large common offset cannot round away the spread.
|
|
149
|
+
const shifted = scaled.map((x) => x.minus(scaled[0]!));
|
|
150
|
+
const center = sum(shifted, GuardDecimal, xs.length);
|
|
151
|
+
const squared = shifted.map((x) => x.minus(center).pow(2));
|
|
152
|
+
return new Decimal(sum(squared, GuardDecimal, sample ? xs.length - 1 : xs.length)
|
|
153
|
+
.sqrt().times(scale).toSignificantDigits(DECIMAL_PRECISION));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function tangent(value: DecimalValue): DecimalValue {
|
|
157
|
+
// Avoid Decimal.tan's cancellation in sqrt(1 - sin(x)^2) near a pole.
|
|
158
|
+
const x = new GuardDecimal(value);
|
|
159
|
+
return new Decimal(x.sin().div(x.cos()).toSignificantDigits(DECIMAL_PRECISION));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function arctangent(value: DecimalValue): DecimalValue {
|
|
163
|
+
// Decimal.atan can loop when argument reduction overflows or underflows.
|
|
164
|
+
const abs = value.abs();
|
|
165
|
+
if (abs.lt(SMALL_ATAN_ABS)) return value;
|
|
166
|
+
if (abs.gt(LARGE_ATAN_ABS)) return PI.div(value.isNegative() ? -2 : 2);
|
|
167
|
+
return Decimal.atan(value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function arctangent2(y: DecimalValue, x: DecimalValue): DecimalValue {
|
|
171
|
+
if (x.isZero() || y.isZero() || !x.isFinite() || !y.isFinite()) return Decimal.atan2(y, x);
|
|
172
|
+
const ratio = y.div(x);
|
|
173
|
+
if (ratio.abs().lt(SMALL_ATAN_ABS)) return x.isNegative() ? (y.isNegative() ? PI.negated() : PI) : ratio;
|
|
174
|
+
if (ratio.abs().gt(LARGE_ATAN_ABS)) return PI.div(y.isNegative() ? -2 : 2);
|
|
175
|
+
return Decimal.atan2(y, x);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function expm1(value: DecimalValue): DecimalValue {
|
|
179
|
+
const x = new GuardDecimal(value.toString());
|
|
180
|
+
if (x.abs().gte(0.1)) {
|
|
181
|
+
return new Decimal(GuardDecimal.exp(x).minus(1).toSignificantDigits(DECIMAL_PRECISION).toString());
|
|
182
|
+
}
|
|
183
|
+
let term = x;
|
|
184
|
+
let total = x;
|
|
185
|
+
for (let n = 2; n <= DECIMAL_PRECISION + 10; n++) {
|
|
186
|
+
term = term.times(x).div(n);
|
|
187
|
+
const next = total.plus(term);
|
|
188
|
+
if (next.eq(total)) break;
|
|
189
|
+
total = next;
|
|
190
|
+
}
|
|
191
|
+
return new Decimal(total.toSignificantDigits(DECIMAL_PRECISION).toString());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function log1p(value: DecimalValue): DecimalValue {
|
|
195
|
+
const x = new GuardDecimal(value.toString());
|
|
196
|
+
if (x.abs().gte(0.1)) {
|
|
197
|
+
return new Decimal(GuardDecimal.ln(x.plus(1)).toSignificantDigits(DECIMAL_PRECISION).toString());
|
|
198
|
+
}
|
|
199
|
+
let power = x;
|
|
200
|
+
let total = x;
|
|
201
|
+
for (let n = 2; n <= DECIMAL_PRECISION + 10; n++) {
|
|
202
|
+
power = power.times(x);
|
|
203
|
+
const next = n % 2 === 0 ? total.minus(power.div(n)) : total.plus(power.div(n));
|
|
204
|
+
if (next.eq(total)) break;
|
|
205
|
+
total = next;
|
|
206
|
+
}
|
|
207
|
+
return new Decimal(total.toSignificantDigits(DECIMAL_PRECISION).toString());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function spendExpensiveWork(cost: number): void {
|
|
211
|
+
if (cost > expensiveWorkLeft) throw new Error("expression work budget exceeded");
|
|
212
|
+
expensiveWorkLeft -= cost;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function trigonometric(
|
|
216
|
+
name: string,
|
|
217
|
+
fn: (x: DecimalValue) => DecimalValue,
|
|
218
|
+
maxAbs?: DecimalValue,
|
|
219
|
+
): (x: DecimalValue) => DecimalValue {
|
|
220
|
+
return (x) => {
|
|
221
|
+
if (maxAbs && x.abs().gt(maxAbs)) {
|
|
222
|
+
throw new Error(`${name}() argument too large (max absolute value ${maxAbs.toString()})`);
|
|
223
|
+
}
|
|
224
|
+
spendExpensiveWork(TRIG_WORK);
|
|
225
|
+
return fn(x);
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function hyperbolic(name: string, fn: (x: DecimalValue) => DecimalValue): (x: DecimalValue) => DecimalValue {
|
|
230
|
+
return (x) => {
|
|
231
|
+
if (x.abs().gt(MAX_HYPERBOLIC_ABS)) {
|
|
232
|
+
throw new Error(`${name}() argument too large (max absolute value ${MAX_HYPERBOLIC_ABS})`);
|
|
233
|
+
}
|
|
234
|
+
spendExpensiveWork(x.isFinite() ? Math.max(1, Math.ceil(x.abs().toNumber())) : 1);
|
|
235
|
+
return fn(x);
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function modulo(a: unknown, b: unknown) {
|
|
240
|
+
const left = toDec(a);
|
|
241
|
+
const right = toDec(b);
|
|
242
|
+
if (right.isZero()) throw new Error("modulo by zero");
|
|
243
|
+
const exponentGap = left.isFinite() && right.isFinite() ? Math.abs(left.e - right.e) : 0;
|
|
244
|
+
if (exponentGap > MAX_MODULO_EXPONENT_GAP) {
|
|
245
|
+
throw new Error(`modulo exponent gap too large (max ${MAX_MODULO_EXPONENT_GAP})`);
|
|
246
|
+
}
|
|
247
|
+
spendExpensiveWork(Math.max(1, exponentGap));
|
|
248
|
+
return left.mod(right);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function roundTo(...args: unknown[]) {
|
|
252
|
+
requireArity("roundTo", args, 2);
|
|
253
|
+
const digits = toDec(args[1]);
|
|
254
|
+
if (!digits.isInteger() || digits.abs().gt(MAX_ROUND_DIGITS)) {
|
|
255
|
+
throw new Error(`roundTo() digits must be an integer from -${MAX_ROUND_DIGITS} to ${MAX_ROUND_DIGITS}`);
|
|
256
|
+
}
|
|
257
|
+
const places = digits.toNumber();
|
|
258
|
+
const value = toDec(args[0]);
|
|
259
|
+
if (places >= 0) return value.toDecimalPlaces(places);
|
|
260
|
+
const scale = new Decimal(10).pow(-places);
|
|
261
|
+
return value.div(scale).toDecimalPlaces(0).times(scale);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function arrayIndex(values: unknown, index: unknown): unknown {
|
|
265
|
+
if (!Array.isArray(values)) throw new Error("indexing needs an array");
|
|
266
|
+
let n: DecimalValue;
|
|
267
|
+
try {
|
|
268
|
+
n = toDec(index);
|
|
269
|
+
} catch {
|
|
270
|
+
throw new Error("array index needs an integer");
|
|
271
|
+
}
|
|
272
|
+
if (!n.isInteger()) throw new Error("array index needs an integer");
|
|
273
|
+
if (n.lt(0) || n.gte(values.length)) throw new Error("array index out of range");
|
|
274
|
+
return values[n.toNumber()];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const decimalUnary: Record<string, (x: DecimalValue) => DecimalValue> = {
|
|
278
|
+
sin: trigonometric("sin", (x) => Decimal.sin(x), MAX_TRIG_ABS),
|
|
279
|
+
cos: trigonometric("cos", (x) => Decimal.cos(x), MAX_TRIG_ABS),
|
|
280
|
+
tan: trigonometric("tan", tangent, MAX_TRIG_ABS),
|
|
281
|
+
asin: trigonometric("asin", (x) => Decimal.asin(x)),
|
|
282
|
+
acos: trigonometric("acos", (x) => Decimal.acos(x)),
|
|
283
|
+
atan: trigonometric("atan", arctangent),
|
|
284
|
+
sinh: hyperbolic("sinh", (x) => Decimal.sinh(x)),
|
|
285
|
+
cosh: hyperbolic("cosh", (x) => Decimal.cosh(x)),
|
|
286
|
+
tanh: hyperbolic("tanh", (x) => Decimal.tanh(x)),
|
|
287
|
+
asinh: hyperbolic("asinh", (x) => Decimal.asinh(x)),
|
|
288
|
+
acosh: hyperbolic("acosh", (x) => Decimal.acosh(x)),
|
|
289
|
+
atanh: hyperbolic("atanh", (x) => Decimal.atanh(x)),
|
|
290
|
+
sqrt: (x) => Decimal.sqrt(x),
|
|
291
|
+
cbrt: (x) => Decimal.cbrt(x),
|
|
292
|
+
abs: (x) => Decimal.abs(x),
|
|
293
|
+
ln: (x) => Decimal.ln(x),
|
|
294
|
+
log: (x) => Decimal.ln(x),
|
|
295
|
+
lg: (x) => Decimal.log10(x),
|
|
296
|
+
log10: (x) => Decimal.log10(x),
|
|
297
|
+
log2: (x) => Decimal.log2(x),
|
|
298
|
+
exp: (x) => Decimal.exp(x),
|
|
299
|
+
expm1,
|
|
300
|
+
log1p,
|
|
301
|
+
ceil: (x) => Decimal.ceil(x),
|
|
302
|
+
floor: (x) => Decimal.floor(x),
|
|
303
|
+
round: (x) => Decimal.round(x),
|
|
304
|
+
trunc: (x) => Decimal.trunc(x),
|
|
305
|
+
sign: (x) => new Decimal(Decimal.sign(x)),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
parser.unaryOps = nullMap({
|
|
309
|
+
...Object.fromEntries(Object.entries(decimalUnary).map(([name, fn]) => [name, (value: unknown) => fn(toDec(value))])),
|
|
310
|
+
"+": toDec,
|
|
311
|
+
"-": (value: unknown) => toDec(value).negated(),
|
|
312
|
+
"!": factorial,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
parser.binaryOps = nullMap({
|
|
316
|
+
"+": (a, b) => toDec(a).plus(toDec(b)),
|
|
317
|
+
"-": (a, b) => toDec(a).minus(toDec(b)),
|
|
318
|
+
"*": (a, b) => toDec(a).times(toDec(b)),
|
|
319
|
+
"/": (a, b) => {
|
|
320
|
+
const numerator = toDec(a);
|
|
321
|
+
const denominator = toDec(b);
|
|
322
|
+
if (denominator.isZero()) throw new Error("division by zero");
|
|
323
|
+
return numerator.div(denominator);
|
|
324
|
+
},
|
|
325
|
+
"%": modulo,
|
|
326
|
+
"^": (a, b) => toDec(a).pow(toDec(b)),
|
|
327
|
+
"[": arrayIndex,
|
|
328
|
+
});
|
|
329
|
+
parser.ternaryOps = nullMap({});
|
|
330
|
+
|
|
331
|
+
const hypot = (...args: unknown[]) => Decimal.hypot(...decimalArguments(args, "hypot"));
|
|
332
|
+
|
|
333
|
+
parser.functions = nullMap({
|
|
334
|
+
d: (...args: unknown[]) => {
|
|
335
|
+
requireArity("d", args, 1);
|
|
336
|
+
if (typeof args[0] !== "string") throw new Error("invalid decimal literal");
|
|
337
|
+
return toDec(args[0]);
|
|
338
|
+
},
|
|
339
|
+
fac: (...args: unknown[]) => {
|
|
340
|
+
requireArity("fac", args, 1);
|
|
341
|
+
return factorial(args[0]);
|
|
342
|
+
},
|
|
343
|
+
pow: (...args: unknown[]) => {
|
|
344
|
+
requireArity("pow", args, 2);
|
|
345
|
+
return toDec(args[0]).pow(toDec(args[1]));
|
|
346
|
+
},
|
|
347
|
+
atan2: (...args: unknown[]) => {
|
|
348
|
+
requireArity("atan2", args, 2);
|
|
349
|
+
const values = args.map(toDec);
|
|
350
|
+
spendExpensiveWork(TRIG_WORK);
|
|
351
|
+
return arctangent2(values[0]!, values[1]!);
|
|
352
|
+
},
|
|
353
|
+
min: (...args: unknown[]) => Decimal.min(...decimalArguments(args, "min")),
|
|
354
|
+
max: (...args: unknown[]) => Decimal.max(...decimalArguments(args, "max")),
|
|
355
|
+
sum: (...args: unknown[]) => {
|
|
356
|
+
requireArity("sum", args, 1);
|
|
357
|
+
return sum(decimals(args[0], "sum"));
|
|
358
|
+
},
|
|
359
|
+
hypot,
|
|
360
|
+
pyt: hypot,
|
|
361
|
+
roundTo,
|
|
362
|
+
percent: (...args: unknown[]) => {
|
|
363
|
+
requireArity("percent", args, 2);
|
|
364
|
+
const rate = toDec(args[0]);
|
|
365
|
+
const amount = toDec(args[1]);
|
|
366
|
+
// Scale the larger factor first so intermediate arithmetic stays in range.
|
|
367
|
+
return amount.abs().gte(rate.abs())
|
|
368
|
+
? amount.div(100).times(rate)
|
|
369
|
+
: rate.div(100).times(amount);
|
|
370
|
+
},
|
|
371
|
+
radians: (...args: unknown[]) => {
|
|
372
|
+
requireArity("radians", args, 1);
|
|
373
|
+
return toDec(args[0]).times(PI).div(180);
|
|
374
|
+
},
|
|
375
|
+
degrees: (...args: unknown[]) => {
|
|
376
|
+
requireArity("degrees", args, 1);
|
|
377
|
+
return toDec(args[0]).times(180).div(PI);
|
|
378
|
+
},
|
|
379
|
+
mean: (...args: unknown[]) => {
|
|
380
|
+
requireArity("mean", args, 1);
|
|
381
|
+
return mean(decimals(args[0], "mean"));
|
|
382
|
+
},
|
|
383
|
+
median: (...args: unknown[]) => {
|
|
384
|
+
requireArity("median", args, 1);
|
|
385
|
+
const xs = decimals(args[0], "median").sort((a, b) => a.comparedTo(b));
|
|
386
|
+
const mid = Math.floor(xs.length / 2);
|
|
387
|
+
return xs.length % 2 === 0 ? mean([xs[mid - 1]!, xs[mid]!]) : xs[mid]!;
|
|
388
|
+
},
|
|
389
|
+
stdev: (...args: unknown[]) => {
|
|
390
|
+
requireArity("stdev", args, 1);
|
|
391
|
+
return standardDeviation(decimals(args[0], "stdev"), false);
|
|
392
|
+
},
|
|
393
|
+
stdevs: (...args: unknown[]) => {
|
|
394
|
+
requireArity("stdevs", args, 1);
|
|
395
|
+
return standardDeviation(decimals(args[0], "stdevs", 2), true);
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
parser.consts = nullMap({ PI, E });
|
|
400
|
+
|
|
401
|
+
function normalizeExpression(expression: string): string {
|
|
402
|
+
const trimmed = expression.trim();
|
|
403
|
+
if (!trimmed) throw new Error("Expression is empty");
|
|
404
|
+
if (trimmed.length > MAX_EXPRESSION_LENGTH) {
|
|
405
|
+
throw new Error(`Expression too long (max ${MAX_EXPRESSION_LENGTH} chars)`);
|
|
406
|
+
}
|
|
407
|
+
return trimmed;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function resultString(result: unknown): string {
|
|
411
|
+
if (typeof result === "number") result = new Decimal(String(result));
|
|
412
|
+
if (!(result instanceof Decimal)) {
|
|
413
|
+
const type = Array.isArray(result) ? "array" : typeof result;
|
|
414
|
+
throw new Error(`Expression did not evaluate to a number (got ${type})`);
|
|
415
|
+
}
|
|
416
|
+
if (!result.isFinite()) {
|
|
417
|
+
if (result.isNaN()) throw new Error("Result is NaN");
|
|
418
|
+
throw new Error(result.isPositive() ? "Result is Infinity" : "Result is -Infinity");
|
|
419
|
+
}
|
|
420
|
+
return result.toString();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function evaluateExpression(expression: string): { expression: string; value: string | string[] } {
|
|
424
|
+
const normalized = normalizeExpression(expression);
|
|
425
|
+
let result: unknown;
|
|
426
|
+
factorialWorkLeft = MAX_FACTORIAL_WORK;
|
|
427
|
+
expensiveWorkLeft = MAX_EXPENSIVE_WORK;
|
|
428
|
+
try {
|
|
429
|
+
result = parser.evaluate(decimalizeExpression(normalized), EMPTY_VARIABLES);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
432
|
+
message = message
|
|
433
|
+
.replace(/^\[DecimalError\]\s*/, "")
|
|
434
|
+
.replace(/^parse error \[\d+:\d+\]:\s*/, "")
|
|
435
|
+
.replace(/\bT[A-Z]+:\s*/g, "");
|
|
436
|
+
if (/precision limit exceeded/i.test(message)) message = "numeric argument exceeds precision limit";
|
|
437
|
+
if (error instanceof RangeError && /call stack/i.test(message)) message = "expression is too deeply nested";
|
|
438
|
+
throw new Error(`Invalid expression: ${message}`);
|
|
439
|
+
} finally {
|
|
440
|
+
factorialWorkLeft = 0;
|
|
441
|
+
expensiveWorkLeft = 0;
|
|
442
|
+
resetDecimal();
|
|
443
|
+
GuardDecimal.set(GUARD_DECIMAL_CONFIG);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const value = Array.isArray(result) ? result.map(resultString) : resultString(result);
|
|
447
|
+
const evaluated = { expression: normalized, value };
|
|
448
|
+
if (Buffer.byteLength(JSON.stringify(evaluated)) > 50 * 1024) {
|
|
449
|
+
throw new Error("Result exceeds 50 KiB output limit");
|
|
450
|
+
}
|
|
451
|
+
return evaluated;
|
|
452
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "expr-eval-fork";
|
|
2
|
+
|
|
3
|
+
declare module "expr-eval-fork" {
|
|
4
|
+
interface Parser {
|
|
5
|
+
binaryOps: Record<string, (left: unknown, right: unknown) => unknown>;
|
|
6
|
+
ternaryOps: Record<string, (condition: unknown, consequent: unknown, alternate: unknown) => unknown>;
|
|
7
|
+
}
|
|
8
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { DECIMAL_PRECISION } from "./decimal.ts";
|
|
5
|
+
import { evaluateExpression, MAX_EXPRESSION_LENGTH } from "./eval.ts";
|
|
6
|
+
|
|
7
|
+
const calculatorTool = defineTool({
|
|
8
|
+
name: "calculator",
|
|
9
|
+
label: "Calculator",
|
|
10
|
+
description: `Calculate arithmetic, scientific functions and statistics with ${DECIMAL_PRECISION}-significant-digit decimal precision. Returns one number or a flat array of numbers.`,
|
|
11
|
+
promptSnippet: "Calculate numeric answers and verify arithmetic",
|
|
12
|
+
promptGuidelines: [
|
|
13
|
+
"Use calculator for non-trivial calculations; simple arithmetic does not need a tool call. Results are rounded decimal strings. In calculator expressions, use sum([...]) for totals and roundTo(value, places) for requested rounding. Put independent results in a flat array.",
|
|
14
|
+
],
|
|
15
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
16
|
+
parameters: Type.Object(
|
|
17
|
+
{
|
|
18
|
+
expression: Type.String({
|
|
19
|
+
minLength: 1,
|
|
20
|
+
maxLength: MAX_EXPRESSION_LENGTH,
|
|
21
|
+
description: "Numeric expression: + - * / % ^ **, PI, E, roots, trig, n! (0–1000), sum/mean/median. log/ln are natural; log2/log10 choose a base. Use expm1(x) for exp(x)-1 and log1p(x) for ln(1+x) when x is small. Trig uses radians; radians(degrees) and degrees(radians) convert angles. percent(rate, amount) gives rate% of amount. stdev(array) is population; stdevs(array) is sample. roundTo(value, places) rounds halves away from zero.",
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
{ additionalProperties: false },
|
|
25
|
+
),
|
|
26
|
+
outputSchema: Type.Object({
|
|
27
|
+
expression: Type.String(),
|
|
28
|
+
value: Type.Union([Type.String(), Type.Array(Type.String())]),
|
|
29
|
+
}, { additionalProperties: false }),
|
|
30
|
+
renderCall(args, theme) {
|
|
31
|
+
return new Text(theme.fg("toolTitle", theme.bold("calculator ")) + theme.fg("dim", JSON.stringify(args.expression ?? "")), 0, 0);
|
|
32
|
+
},
|
|
33
|
+
async execute(_toolCallId, params) {
|
|
34
|
+
const evaluated = evaluateExpression(params.expression);
|
|
35
|
+
const text = Array.isArray(evaluated.value) ? JSON.stringify(evaluated.value) : evaluated.value;
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text", text }],
|
|
38
|
+
details: evaluated,
|
|
39
|
+
structuredContent: evaluated,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export default function (pi: ExtensionAPI) {
|
|
45
|
+
pi.registerTool(calculatorTool);
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitchmultz/pi-calculator",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
"version": "4.1.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"description": "Deterministic high-precision calculator tool for Pi",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"eval.ts",
|
|
13
|
+
"decimal.ts",
|
|
14
|
+
"expr-eval-fork.d.ts",
|
|
15
|
+
"README.md",
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"pi-package",
|
|
21
|
+
"pi",
|
|
22
|
+
"calculator",
|
|
23
|
+
"decimal"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/fitchmultz/pi-calculator.git"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24.0.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"check": "node check.ts",
|
|
34
|
+
"check:load": "node check-extension.ts",
|
|
35
|
+
"typecheck": "tsc",
|
|
36
|
+
"verify": "npm run typecheck && npm run check && npm run check:load",
|
|
37
|
+
"check:compat": "npm run verify"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"extensions": [
|
|
41
|
+
"./index.ts"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"decimal.js": "10.6.0",
|
|
46
|
+
"expr-eval-fork": "3.0.3"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
50
|
+
"@earendil-works/pi-tui": "*",
|
|
51
|
+
"typebox": "*"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@earendil-works/pi-coding-agent": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@earendil-works/pi-tui": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"typebox": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
66
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
67
|
+
"@types/node": "24.19.1",
|
|
68
|
+
"typebox": "1.3.27",
|
|
69
|
+
"typescript": "7.0.2"
|
|
70
|
+
}
|
|
71
|
+
}
|