@cyanheads/calculator-mcp-server 0.4.2 → 0.5.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/AGENTS.md +7 -5
- package/CLAUDE.md +7 -5
- package/README.md +25 -30
- package/changelog/0.4.x/0.4.3.md +28 -0
- package/changelog/0.5.x/0.5.0.md +44 -0
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/calculate.tool.d.ts +38 -10
- package/dist/mcp-server/tools/definitions/calculate.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/calculate.tool.js +91 -23
- package/dist/mcp-server/tools/definitions/calculate.tool.js.map +1 -1
- package/dist/services/math/math-service.d.ts +62 -23
- package/dist/services/math/math-service.d.ts.map +1 -1
- package/dist/services/math/math-service.js +697 -257
- package/dist/services/math/math-service.js.map +1 -1
- package/dist/services/math/size-guard.d.ts +84 -0
- package/dist/services/math/size-guard.d.ts.map +1 -0
- package/dist/services/math/size-guard.js +733 -0
- package/dist/services/math/size-guard.js.map +1 -0
- package/package.json +8 -8
- package/server.json +3 -3
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Hardened math.js wrapper for secure expression evaluation.
|
|
3
3
|
* Creates a restricted math.js instance with dangerous functions disabled in
|
|
4
|
-
* the expression scope,
|
|
4
|
+
* the expression scope, parses each expression once and checks the tree before
|
|
5
|
+
* anything runs, and evaluates, inspects, and formats the result inside a vm
|
|
6
|
+
* sandbox with a timeout.
|
|
5
7
|
* @module services/math/math-service
|
|
6
8
|
*/
|
|
7
9
|
import vm from 'node:vm';
|
|
8
|
-
import { timeout, validationError } from '@cyanheads/mcp-ts-core/errors';
|
|
9
|
-
import { all, create } from 'mathjs';
|
|
10
|
+
import { McpError, timeout, validationError } from '@cyanheads/mcp-ts-core/errors';
|
|
11
|
+
import { all, create, isBigNumber, isComplex, isHelp, isMatrix, isSparseMatrix, isUnit, } from 'mathjs';
|
|
12
|
+
import { beginEvaluation, installSizeGuards, isPlainObject, meterNode, sizeLimitErrorIn, } from './size-guard.js';
|
|
10
13
|
/**
|
|
11
14
|
* Custom simplification rules extending math.js defaults.
|
|
12
15
|
* math.js ships with algebraic rules only — these add common trig identities.
|
|
@@ -43,6 +46,7 @@ const CUSTOM_UNITS = {
|
|
|
43
46
|
* Functions disabled in the expression scope for security.
|
|
44
47
|
* These are overridden via math.import() — expressions cannot call them.
|
|
45
48
|
* simplify/derivative are called programmatically by the tool handler, not from expressions.
|
|
49
|
+
* `config` is not listed: it gets a read-only guard instead (see createMathInstance).
|
|
46
50
|
*/
|
|
47
51
|
const DISABLED_FUNCTIONS = [
|
|
48
52
|
'import',
|
|
@@ -55,7 +59,6 @@ const DISABLED_FUNCTIONS = [
|
|
|
55
59
|
'reviver',
|
|
56
60
|
'compile',
|
|
57
61
|
'chain',
|
|
58
|
-
'config',
|
|
59
62
|
'parser',
|
|
60
63
|
];
|
|
61
64
|
/**
|
|
@@ -81,17 +84,92 @@ const BLOCKED_RESULT_TYPES = new Set(['function', 'Function', 'ResultSet', 'Pars
|
|
|
81
84
|
* accessor: `.toString()` / `.toLocaleString()` on a function-valued identifier
|
|
82
85
|
* (e.g. `cos.toString()`) otherwise returns internal source as a plain string,
|
|
83
86
|
* slipping past {@link BLOCKED_RESULT_TYPES} (which only sees the value after
|
|
84
|
-
* stringification). See {@link
|
|
87
|
+
* stringification). See {@link inspectTree}.
|
|
85
88
|
*/
|
|
86
89
|
const STRINGIFYING_METHODS = new Set(['toString', 'toLocaleString']);
|
|
87
90
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
91
|
+
* The tool's `operation` values. Each is also a disabled function name, so a
|
|
92
|
+
* call such as `derivative("x^2", "x")` inside an expression is a caller asking
|
|
93
|
+
* for the operation the wrong way — rejected at parse time with a pointer to the
|
|
94
|
+
* `operation` parameter rather than the generic disabled-function message.
|
|
95
|
+
*/
|
|
96
|
+
const OPERATION_FUNCTIONS = new Set(['evaluate', 'simplify', 'derivative']);
|
|
97
|
+
/**
|
|
98
|
+
* Standard-notation function names math.js does not define — natural log `ln`
|
|
99
|
+
* and the inverse-trig `arc*` family. Renamed on the parse tree (see
|
|
100
|
+
* {@link inspectTree}) to their math.js builtins.
|
|
101
|
+
*/
|
|
102
|
+
const NOTATION_ALIAS = /^(?:ln|arc(?:sin|cos|tan|sec|csc|cot)h?)$/;
|
|
103
|
+
/**
|
|
104
|
+
* Node types that compute a new value — a call, an operator, a matrix or object
|
|
105
|
+
* literal, an indexed read, a range. Each is metered against the evaluation's
|
|
106
|
+
* element budget; the rest (symbols, constants, parentheses, assignments,
|
|
107
|
+
* conditionals) pass along a value some other node built.
|
|
108
|
+
*/
|
|
109
|
+
const VALUE_BUILDING_NODES = new Set([
|
|
110
|
+
'AccessorNode',
|
|
111
|
+
'ArrayNode',
|
|
112
|
+
'FunctionNode',
|
|
113
|
+
'ObjectNode',
|
|
114
|
+
'OperatorNode',
|
|
115
|
+
'RangeNode',
|
|
116
|
+
]);
|
|
117
|
+
/**
|
|
118
|
+
* Lower bound on the characters one element of a collection result renders to:
|
|
119
|
+
* at least one character plus the `, ` separator (a one-element matrix, `[0]`,
|
|
120
|
+
* is three). A result with more than `maxResultLength / 3` elements can never
|
|
121
|
+
* fit, so it is rejected before formatting.
|
|
122
|
+
*/
|
|
123
|
+
const MIN_CHARS_PER_ELEMENT = 3;
|
|
124
|
+
/**
|
|
125
|
+
* math.js error thrown when a Fraction-mode expression calls a function with no
|
|
126
|
+
* Fraction implementation: irrational and transcendental ones (`sqrt`, `sin`,
|
|
127
|
+
* `log`, …) and some whose result can be rational (`sqrt(4)`, `5!`,
|
|
128
|
+
* `combinations`). math.js reports it as an implicit type-conversion failure that
|
|
129
|
+
* does not name the function; paired with a `numericType === 'Fraction'` guard it
|
|
130
|
+
* maps to the actionable `fraction_unsupported` error instead of the misleading,
|
|
131
|
+
* syntax-oriented `parse_failed`. See {@link MathService.classifyFailure}.
|
|
93
132
|
*/
|
|
94
133
|
const FRACTION_CONVERSION_ERROR = /Cannot implicitly convert a Fraction/;
|
|
134
|
+
/**
|
|
135
|
+
* math.js error thrown when a 64-bit float meets a Fraction and has no Fraction
|
|
136
|
+
* equal to it — `pi * 2/3`, `pi^40`, `sin(pi) + 1/3`. Under numericType
|
|
137
|
+
* "Fraction" the float can only have come from a value Fraction mode holds
|
|
138
|
+
* approximately, so it maps to `fraction_unsupported`, not `type_mismatch`.
|
|
139
|
+
*/
|
|
140
|
+
const FLOAT_TO_FRACTION_ERROR = /^Cannot implicitly convert a number to a Fraction when there will be a loss of precision/;
|
|
141
|
+
/** `fraction_unsupported` message for a function Fraction mode cannot compute. */
|
|
142
|
+
const FRACTION_FUNCTION_MESSAGE = 'numericType "Fraction" cannot compute this expression: either its result has no exact rational value (e.g. sqrt(2), sin(1), log(3)), or it calls a function Fraction mode cannot compute, even for a rational result (e.g. sqrt(4), 5!, combinations(5, 2)). Retry with numericType "number" or "BigNumber".';
|
|
143
|
+
/** `undefined_result` message: a non-finite value, or a Fraction division by zero. */
|
|
144
|
+
const UNDEFINED_RESULT_MESSAGE = 'Expression evaluated to a non-finite result (Infinity, -Infinity, or NaN): either the operation is mathematically undefined (division by zero, 0/0, log(0)), or a value overflowed its numeric range (under numericType "number", past about 1.8e308 — e.g. 171!, 2^1024, exp(1000)).';
|
|
145
|
+
/** `fraction_unsupported` message for a Fraction-mode evaluation that picked up a float. */
|
|
146
|
+
const FRACTION_FLOAT_MESSAGE = 'numericType "Fraction" cannot return this result exactly: the expression uses a value Fraction mode holds only as a rounded 64-bit float — an irrational constant (pi, e), a non-integer power (2^(1/2)), a complex number with a non-integer part, or a float from number() or random(). Retry with numericType "number" or "BigNumber".';
|
|
147
|
+
/** math.js derivative error for a function or operator it has no differentiation rule for. */
|
|
148
|
+
const NO_DERIVATIVE_RULE = /^Cannot process (function|operator) "([^"]+)" in derivative/;
|
|
149
|
+
/**
|
|
150
|
+
* Unit names math.js resolves to a function first — `5 min` multiplies by the
|
|
151
|
+
* minimum function — paired with a unit name to write instead.
|
|
152
|
+
*/
|
|
153
|
+
const FUNCTION_SHADOWED_UNITS = { min: 'minute', sec: 's' };
|
|
154
|
+
/** Message of the vm timeout error (`ERR_SCRIPT_EXECUTION_TIMEOUT`), also when re-wrapped. */
|
|
155
|
+
const VM_TIMEOUT_ERROR = /Script execution timed out after/;
|
|
156
|
+
/**
|
|
157
|
+
* fraction.js throws this for any Fraction division by zero — reachable in every
|
|
158
|
+
* numericType through `fraction()`. Fractions never hold Infinity, so it is the
|
|
159
|
+
* Fraction form of a non-finite result and maps to `undefined_result`.
|
|
160
|
+
*/
|
|
161
|
+
const FRACTION_DIVISION_BY_ZERO = /^Division by Zero$/;
|
|
162
|
+
/**
|
|
163
|
+
* Evaluation-time errors that are about the expression's names rather than its
|
|
164
|
+
* values: an undefined symbol, function, or unit, a disabled function, or a
|
|
165
|
+
* blocked property. They stay `parse_failed`, whose recovery is about names.
|
|
166
|
+
*/
|
|
167
|
+
const NAME_ERROR = /^(?:Undefined (?:symbol|function) |Unit ".*" not found|No access to )|is disabled for security\.$/;
|
|
168
|
+
/**
|
|
169
|
+
* Operand type and unit errors that math.js raises as plain messages (typed-function
|
|
170
|
+
* `wrongType` errors are recognized by `data.category` instead).
|
|
171
|
+
*/
|
|
172
|
+
const TYPE_MISMATCH_ERROR = /^(?:Units do not match|Cannot convert |Cannot implicitly convert )|is no angle$/;
|
|
95
173
|
/**
|
|
96
174
|
* Scope key names that could pollute the object prototype chain or shadow
|
|
97
175
|
* critical Object.prototype methods. Validated before passing to math.js.
|
|
@@ -112,96 +190,262 @@ const BLOCKED_SCOPE_KEYS = new Set([
|
|
|
112
190
|
'toLocaleString',
|
|
113
191
|
]);
|
|
114
192
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* - `[` / `]` inside a string literal — they must not shift the bracket depth,
|
|
121
|
-
* or a string such as `"]"` would let a later top-level `;` slip past.
|
|
193
|
+
* Walk a freshly parsed tree once: flag stringifier access and operation-named
|
|
194
|
+
* calls, collect the names used as values, meter every value-building node against the element budget (so a node
|
|
195
|
+
* in a loop body — a user-defined function or an inline `map` callback — is
|
|
196
|
+
* charged on each iteration), and rename standard-notation calls in place —
|
|
197
|
+
* `ln` → `log`, `arc<fn>` → `a<fn>` (`arcsin` → `asin`, `arctanh` → `atanh`, …).
|
|
122
198
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
199
|
+
* The rename works on `FunctionNode`s whose callee is a `SymbolNode`, so string
|
|
200
|
+
* literals (`"ln("` is a `ConstantNode`) and scope variables named `ln` (a bare
|
|
201
|
+
* `SymbolNode`) are never touched, and nested calls are reached because the walk
|
|
202
|
+
* covers every node. It is name-level rather than a `math.import` alias because
|
|
203
|
+
* symbolic `derivative` matches builtin names — an imported alias is not in its
|
|
204
|
+
* differentiation table. The tree is evaluated as-is afterwards, never
|
|
205
|
+
* re-stringified: a Fraction-mode tree prints `0.1` as `1/10`.
|
|
128
206
|
*/
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
207
|
+
function inspectTree(tree) {
|
|
208
|
+
const inspection = {
|
|
209
|
+
stringifies: false,
|
|
210
|
+
operationCall: undefined,
|
|
211
|
+
valueNames: new Set(),
|
|
212
|
+
};
|
|
213
|
+
tree.traverse((node, path) => {
|
|
214
|
+
if (VALUE_BUILDING_NODES.has(node.type))
|
|
215
|
+
meterNode(node);
|
|
216
|
+
if (node.type === 'SymbolNode' && path !== 'fn') {
|
|
217
|
+
inspection.valueNames.add(node.name);
|
|
140
218
|
}
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return
|
|
151
|
-
|
|
219
|
+
if (node.type === 'IndexNode') {
|
|
220
|
+
const { dimensions } = node;
|
|
221
|
+
if (dimensions.some((dim) => dim.type === 'ConstantNode' &&
|
|
222
|
+
STRINGIFYING_METHODS.has(String(dim.value)))) {
|
|
223
|
+
inspection.stringifies = true;
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (node.type !== 'FunctionNode')
|
|
228
|
+
return;
|
|
229
|
+
const callee = node.fn;
|
|
230
|
+
if (callee.type !== 'SymbolNode' || callee.name === undefined)
|
|
231
|
+
return;
|
|
232
|
+
if (OPERATION_FUNCTIONS.has(callee.name)) {
|
|
233
|
+
inspection.operationCall ??= callee.name;
|
|
234
|
+
}
|
|
235
|
+
else if (NOTATION_ALIAS.test(callee.name)) {
|
|
236
|
+
callee.name = callee.name === 'ln' ? 'log' : `a${callee.name.slice(3)}`;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return inspection;
|
|
240
|
+
}
|
|
241
|
+
/** Whether a scalar math.js value is Infinity, -Infinity, or NaN. */
|
|
242
|
+
function isNonFiniteScalar(value) {
|
|
243
|
+
if (typeof value === 'number')
|
|
244
|
+
return !Number.isFinite(value);
|
|
245
|
+
if (isBigNumber(value))
|
|
246
|
+
return !value.isFinite();
|
|
247
|
+
if (isComplex(value))
|
|
248
|
+
return isNonFiniteScalar(value.re) || isNonFiniteScalar(value.im);
|
|
152
249
|
return false;
|
|
153
250
|
}
|
|
154
251
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
252
|
+
* Whether a scalar is a 64-bit float that need not be the exact value: a
|
|
253
|
+
* `number`, or a `Complex` part, that is not a safe integer. Under numericType
|
|
254
|
+
* "Fraction" an exact value is a Fraction or a whole-number count (`count`,
|
|
255
|
+
* `size`), so such a float came from an irrational constant, a non-integer
|
|
256
|
+
* power, complex arithmetic, or an explicit float conversion. The test is
|
|
257
|
+
* safe-integer rather than integer because every double past 2^53 is an integer.
|
|
258
|
+
* A `BigNumber` only appears when an expression asks for one (`bignumber(x)`),
|
|
259
|
+
* and a `Unit` is judged by its magnitude.
|
|
160
260
|
*/
|
|
161
|
-
function
|
|
261
|
+
function isFloatApproximation(value) {
|
|
162
262
|
if (typeof value === 'number')
|
|
163
|
-
return !Number.
|
|
164
|
-
if (value
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
263
|
+
return !Number.isSafeInteger(value);
|
|
264
|
+
if (isComplex(value))
|
|
265
|
+
return isFloatApproximation(value.re) || isFloatApproximation(value.im);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
/** Record a scalar's non-finite and float-approximation findings on the scan. */
|
|
269
|
+
function scanScalar(value, scan) {
|
|
270
|
+
if (isNonFiniteScalar(value))
|
|
271
|
+
scan.nonFinite = true;
|
|
272
|
+
if (scan.exactOnly && isFloatApproximation(value))
|
|
273
|
+
scan.inexact = true;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Walk a result once, counting leaf elements (stopping past `limit`) and flagging
|
|
277
|
+
* `help()` objects, non-finite numbers, and — with `exactOnly` — float
|
|
278
|
+
* approximations. The number checks reach every container a result can nest a
|
|
279
|
+
* number in — matrix elements (dense data, or a sparse matrix's stored values,
|
|
280
|
+
* never densified), `Complex` parts, a `Unit`'s magnitude, `BigNumber`s, and
|
|
281
|
+
* object-literal values. `Fraction`s are always finite (fraction.js throws
|
|
282
|
+
* instead of producing Infinity) and always exact.
|
|
283
|
+
*/
|
|
284
|
+
function scanResult(value, limit, scan) {
|
|
285
|
+
if (scan.elements > limit)
|
|
286
|
+
return;
|
|
287
|
+
if (value === null || typeof value !== 'object') {
|
|
288
|
+
scan.elements++;
|
|
289
|
+
scanScalar(value, scan);
|
|
290
|
+
return;
|
|
171
291
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
292
|
+
if (isHelp(value)) {
|
|
293
|
+
scan.elements++;
|
|
294
|
+
scan.help = true;
|
|
295
|
+
return;
|
|
175
296
|
}
|
|
176
|
-
|
|
297
|
+
if (isSparseMatrix(value)) {
|
|
298
|
+
scanResult(value._values ?? [], limit, scan);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (isMatrix(value)) {
|
|
302
|
+
const size = value.size();
|
|
303
|
+
if (size.reduce((n, d) => n * d, 1) > limit) {
|
|
304
|
+
scan.elements = limit + 1;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
scanResult(value.valueOf(), limit, scan);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (Array.isArray(value)) {
|
|
311
|
+
for (const item of value) {
|
|
312
|
+
scanResult(item, limit, scan);
|
|
313
|
+
if (scan.elements > limit)
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (isUnit(value)) {
|
|
319
|
+
scan.elements++;
|
|
320
|
+
scanScalar(value.value, scan);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// BigNumber, Complex, Fraction, and other class instances are scalars.
|
|
324
|
+
if (!isPlainObject(value)) {
|
|
325
|
+
scan.elements++;
|
|
326
|
+
scanScalar(value, scan);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
for (const item of Object.values(value)) {
|
|
330
|
+
scanResult(item, limit, scan);
|
|
331
|
+
if (scan.elements > limit)
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/** Script every timed call runs in the service's reused vm context. */
|
|
336
|
+
const TIMED_CALL = new vm.Script('result = fn()');
|
|
337
|
+
/**
|
|
338
|
+
* Largest decimal exponent a Fraction-mode literal may carry. `1e1000` already
|
|
339
|
+
* spells out a 1001-digit integer; past this, an exponent only builds numbers too
|
|
340
|
+
* large to return, so it fails fast instead of allocating.
|
|
341
|
+
*/
|
|
342
|
+
const MAX_FRACTION_LITERAL_EXPONENT = 1000;
|
|
343
|
+
/** A decimal literal in exponent notation: mantissa digits, fraction digits, exponent. */
|
|
344
|
+
const EXPONENT_LITERAL = /^(\d*)(?:\.(\d*))?[eE]([+-]?\d+)$/;
|
|
345
|
+
/**
|
|
346
|
+
* Rewrite an exponent-notation literal as the plain decimal it denotes
|
|
347
|
+
* (`1.5e-3` → `0.0015`), which fraction.js reads exactly. Returns `undefined`
|
|
348
|
+
* for anything that is not exponent notation.
|
|
349
|
+
*/
|
|
350
|
+
function expandExponentLiteral(literal) {
|
|
351
|
+
const match = EXPONENT_LITERAL.exec(literal);
|
|
352
|
+
if (!match)
|
|
353
|
+
return undefined;
|
|
354
|
+
const [, whole = '', fraction = '', exponentText = '0'] = match;
|
|
355
|
+
const exponent = Number(exponentText);
|
|
356
|
+
if (Math.abs(exponent) > MAX_FRACTION_LITERAL_EXPONENT) {
|
|
357
|
+
throw new SyntaxError(`Exponent literal "${literal}" is out of range for numericType "Fraction" (exponents up to ±${MAX_FRACTION_LITERAL_EXPONENT}); use numericType "BigNumber" for numbers this large or small.`);
|
|
358
|
+
}
|
|
359
|
+
const digits = whole + fraction;
|
|
360
|
+
const point = whole.length + exponent;
|
|
361
|
+
if (point <= 0)
|
|
362
|
+
return `0.${'0'.repeat(-point)}${digits}`;
|
|
363
|
+
if (point >= digits.length)
|
|
364
|
+
return digits + '0'.repeat(point - digits.length);
|
|
365
|
+
return `${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
177
366
|
}
|
|
178
367
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
368
|
+
* Let a Fraction instance read exponent-notation literals exactly. `numeric` is
|
|
369
|
+
* what the parser uses to turn a literal into a value; for a Fraction target, an
|
|
370
|
+
* exponent literal is expanded to plain decimal first. Every other conversion is
|
|
371
|
+
* passed through unchanged.
|
|
182
372
|
*/
|
|
183
|
-
|
|
373
|
+
function installExponentLiterals(math, mathImport) {
|
|
374
|
+
const numeric = math.numeric.bind(math);
|
|
375
|
+
const exactNumeric = (value, outputType) => {
|
|
376
|
+
const expanded = outputType === 'Fraction' && typeof value === 'string'
|
|
377
|
+
? expandExponentLiteral(value)
|
|
378
|
+
: undefined;
|
|
379
|
+
return numeric(expanded ?? value, outputType);
|
|
380
|
+
};
|
|
381
|
+
mathImport({ numeric: exactNumeric }, { override: true });
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Read Fraction-mode scope values the way the instance's own arithmetic does:
|
|
385
|
+
* math.js converts a number to a Fraction implicitly when the Fraction equals it
|
|
386
|
+
* exactly (0.5 → 1/2, 0.1 → 1/10), so `x + 0` already reads 0.5 as 1/2. Applying
|
|
387
|
+
* that rule up front makes a bare `x` read the same. A number with no equal
|
|
388
|
+
* Fraction stays a float, which the result scan then rejects.
|
|
389
|
+
*/
|
|
390
|
+
function fractionScopeValue(fraction) {
|
|
391
|
+
return (value) => {
|
|
392
|
+
const exact = fraction(value);
|
|
393
|
+
return exact.valueOf() === value ? exact : value;
|
|
394
|
+
};
|
|
395
|
+
}
|
|
184
396
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* (`arcsin` → `asin`, `arctanh` → `atanh`, …). All 12 `a*` targets are math.js
|
|
188
|
-
* builtins, so every operation resolves them.
|
|
397
|
+
* Register {@link CUSTOM_UNITS} without changing how other results simplify, and
|
|
398
|
+
* return a function that resets the instance's auto unit system.
|
|
189
399
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
400
|
+
* math.js simplifies a combined result (`10 m / 2 s`) through its "auto" unit
|
|
401
|
+
* system: one unit per base dimension, overwritten by every unit it parses. That
|
|
402
|
+
* state lives on the instance, so it leaked between calls, and `createUnit`
|
|
403
|
+
* disturbed it three ways. A unit whose dimensions match no base unit (a velocity)
|
|
404
|
+
* got a base of its own (`mph_STUFF`), so every velocity then simplified to mph or
|
|
405
|
+
* knot. A unit with a base enters the auto system whenever it is parsed, so a
|
|
406
|
+
* `lightyear` elsewhere turned length results into `ly`. And parsing the
|
|
407
|
+
* definitions re-seeded the system (`hour` as the time unit). So the new base is
|
|
408
|
+
* removed, the custom units lose their base (they keep their dimensions, so
|
|
409
|
+
* conversions and unit checks are unchanged), and the auto system is reset to its
|
|
410
|
+
* seed before every call (#37).
|
|
194
411
|
*/
|
|
195
|
-
function
|
|
196
|
-
|
|
412
|
+
function registerCustomUnits(math) {
|
|
413
|
+
const registry = math.Unit;
|
|
414
|
+
const auto = registry.UNIT_SYSTEMS.auto;
|
|
415
|
+
const seed = { ...auto };
|
|
416
|
+
const bases = new Set(Object.keys(registry.BASE_UNITS));
|
|
417
|
+
const units = new Set(Object.keys(registry.UNITS));
|
|
418
|
+
math.createUnit(CUSTOM_UNITS);
|
|
419
|
+
for (const key of Object.keys(registry.BASE_UNITS)) {
|
|
420
|
+
if (bases.has(key))
|
|
421
|
+
continue;
|
|
422
|
+
delete registry.BASE_UNITS[key];
|
|
423
|
+
for (const system of Object.values(registry.UNIT_SYSTEMS))
|
|
424
|
+
delete system[key];
|
|
425
|
+
}
|
|
426
|
+
for (const [name, unit] of Object.entries(registry.UNITS)) {
|
|
427
|
+
if (!units.has(name))
|
|
428
|
+
delete unit.base;
|
|
429
|
+
}
|
|
430
|
+
return () => {
|
|
431
|
+
for (const key of Object.keys(auto))
|
|
432
|
+
delete auto[key];
|
|
433
|
+
Object.assign(auto, seed);
|
|
434
|
+
};
|
|
197
435
|
}
|
|
198
436
|
/** Create and harden a math.js instance with the given numeric type. */
|
|
199
437
|
function createMathInstance(number) {
|
|
200
438
|
// biome-ignore lint/style/noNonNullAssertion: math.js types declare `all` as potentially undefined, but it's always defined at runtime
|
|
201
439
|
const math = create(all, { number });
|
|
440
|
+
// Capture math.import before it's disabled — needed for the imports and guards below.
|
|
441
|
+
const mathImport = math.import.bind(math);
|
|
442
|
+
// fraction.js cannot read exponent notation, so a Fraction-mode literal such as
|
|
443
|
+
// `2e5` fails to parse. The parser converts literals through `numeric`, which it
|
|
444
|
+
// captures when first created — so the override must precede the `parse` capture.
|
|
445
|
+
if (number === 'Fraction')
|
|
446
|
+
installExponentLiterals(math, mathImport);
|
|
202
447
|
// Capture references BEFORE the override step — the import shim replaces these
|
|
203
448
|
// on the instance, so binding after would capture the disabled stubs.
|
|
204
|
-
const evaluate = math.evaluate.bind(math);
|
|
205
449
|
const format = math.format.bind(math);
|
|
206
450
|
const typeOf = math.typeOf.bind(math);
|
|
207
451
|
// Symbolic operations are captured here too so a hardened instance can back
|
|
@@ -212,23 +456,24 @@ function createMathInstance(number) {
|
|
|
212
456
|
const simplify = math.simplify.bind(math);
|
|
213
457
|
const derivative = math.derivative.bind(math);
|
|
214
458
|
const simplifyRules = [...math.simplify.rules];
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
459
|
+
const scopeValue = number === 'Fraction' ? fractionScopeValue(math.fraction.bind(math)) : (value) => value;
|
|
460
|
+
// Read the config before the override (an empty options object changes nothing).
|
|
461
|
+
// math.js injects `config` into every factory it instantiates lazily, and reads it
|
|
462
|
+
// both as a call (config()) and as properties (config.relTol, config.precision). A
|
|
463
|
+
// throwing stub breaks those readers — BigNumber arithmetic fails with
|
|
464
|
+
// `[DecimalError] precision: NaN`, and number-mode functions created after the
|
|
465
|
+
// override (eigs, schur, intersect, isPositive, compare, …) see `relTol`/`absTol` as
|
|
466
|
+
// undefined (#25). Every instance therefore gets a read-only guard (installed
|
|
467
|
+
// below): reads pass through, writes throw.
|
|
468
|
+
const currentConfig = math.config({});
|
|
224
469
|
// Register custom units and function-name aliases (natural-language plus common
|
|
225
470
|
// cross-ecosystem synonyms from Excel/NumPy/calculator notation). None are math.js
|
|
226
471
|
// builtins, so the imports are purely additive — they turn an agent's natural guess
|
|
227
472
|
// into a result instead of an "undefined function" error. Statistics/combinatorics
|
|
228
473
|
// functions aren't differentiable, so a value-alias suffices (unlike ln/arc*, which
|
|
229
|
-
//
|
|
474
|
+
// are renamed on the parse tree to stay in the derivative table — see inspectTree).
|
|
230
475
|
// Must run before createUnit/import are disabled below.
|
|
231
|
-
math
|
|
476
|
+
const resetUnitSystem = registerCustomUnits(math);
|
|
232
477
|
mathImport({
|
|
233
478
|
average: math.mean,
|
|
234
479
|
avg: math.mean,
|
|
@@ -241,55 +486,63 @@ function createMathInstance(number) {
|
|
|
241
486
|
length: math.count,
|
|
242
487
|
len: math.count,
|
|
243
488
|
});
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
continue; // read-only guard installed below
|
|
251
|
-
disabled[fn] = () => {
|
|
252
|
-
throw new Error(`Function "${fn}" is disabled for security.`);
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
// Redact constants that leak implementation details
|
|
256
|
-
for (const [key, value] of Object.entries(REDACTED_CONSTANTS)) {
|
|
257
|
-
disabled[key] = value;
|
|
258
|
-
}
|
|
259
|
-
mathImport(disabled, { override: true });
|
|
260
|
-
// For BigNumber/Fraction: install a read-only config guard using the pre-captured
|
|
261
|
-
// mathImport reference (math.import is now disabled in the expression scope).
|
|
262
|
-
//
|
|
263
|
-
// Two access patterns must be preserved for math.js internals:
|
|
489
|
+
// Bound what an expression can build from a size argument, a product, a broadcast,
|
|
490
|
+
// a repeated input, an index, or a formatting precision. Installed on the instance,
|
|
491
|
+
// so simplify/derivative constant folding on this instance hits the same limits.
|
|
492
|
+
installSizeGuards(math, mathImport, all);
|
|
493
|
+
// Replace `config` with a read-only guard. Two access patterns must be preserved
|
|
494
|
+
// for math.js internals:
|
|
264
495
|
// 1. config() — gamma/factorial call this to read the full config object
|
|
265
|
-
// 2. config.precision — gamma accesses precision directly as a property on the
|
|
496
|
+
// 2. config.precision — gamma accesses precision directly as a property on the
|
|
497
|
+
// function; eigs, intersect, compare, … read config.relTol / config.absTol
|
|
266
498
|
//
|
|
267
499
|
// Replacing config with a plain stub breaks both; the guard function below satisfies
|
|
268
500
|
// both patterns while blocking write access (calls with a non-empty options object).
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
501
|
+
// A bare `config()` from an expression therefore returns the config object — a fresh
|
|
502
|
+
// copy each call, as stock math.js does: the instance outlives the request, and an
|
|
503
|
+
// expression can assign into a plain object it holds (`c.k = …` after `c = config()`).
|
|
504
|
+
const configGuard = Object.assign((options) => {
|
|
505
|
+
if (options !== undefined && Object.keys(options).length > 0) {
|
|
506
|
+
throw new Error('"config" is disabled for security.');
|
|
507
|
+
}
|
|
508
|
+
return { ...currentConfig };
|
|
509
|
+
}, currentConfig);
|
|
510
|
+
// Disable dangerous functions and redact constants that leak implementation details,
|
|
511
|
+
// through the pre-captured import (this step disables math.import too).
|
|
512
|
+
const overrides = { ...REDACTED_CONSTANTS, config: configGuard };
|
|
513
|
+
for (const fn of DISABLED_FUNCTIONS) {
|
|
514
|
+
overrides[fn] = () => {
|
|
515
|
+
throw new Error(`Function "${fn}" is disabled for security.`);
|
|
516
|
+
};
|
|
278
517
|
}
|
|
279
|
-
|
|
518
|
+
mathImport(overrides, { override: true });
|
|
519
|
+
// The namespace an expression's names resolve in (before units), transforms included.
|
|
520
|
+
const expressionNames = math.expression.mathWithTransform;
|
|
521
|
+
return {
|
|
522
|
+
format,
|
|
523
|
+
typeOf,
|
|
524
|
+
parse,
|
|
525
|
+
isFunction: (name) => Object.hasOwn(expressionNames, name) && typeof expressionNames[name] === 'function',
|
|
526
|
+
resetUnitSystem,
|
|
527
|
+
scopeValue,
|
|
528
|
+
simplify: simplify,
|
|
529
|
+
derivative: derivative,
|
|
530
|
+
simplifyRules,
|
|
531
|
+
};
|
|
280
532
|
}
|
|
281
533
|
export class MathService {
|
|
282
534
|
/** Default IEEE 754 instance — used for the vast majority of evaluations. */
|
|
283
535
|
defaultInstance;
|
|
284
|
-
/** BigNumber instance —
|
|
536
|
+
/** BigNumber instance — 64 significant digits; selected via numericType: "BigNumber". */
|
|
285
537
|
bigNumberInstance;
|
|
286
538
|
/** Fraction instance — exact rational arithmetic; selected via numericType: "Fraction". */
|
|
287
539
|
fractionInstance;
|
|
288
|
-
parse;
|
|
289
540
|
simplify;
|
|
290
541
|
derivative;
|
|
291
542
|
simplifyRules;
|
|
292
543
|
config;
|
|
544
|
+
/** vm context reused by every timed call; see {@link runWithTimeout}. */
|
|
545
|
+
sandbox = vm.createContext({});
|
|
293
546
|
constructor(config) {
|
|
294
547
|
this.config = config;
|
|
295
548
|
// Pre-initialize one hardened instance per numeric type so numeric-type selection
|
|
@@ -303,7 +556,6 @@ export class MathService {
|
|
|
303
556
|
// simplify/derivative resolves disabled functions to their throwing stubs and reads
|
|
304
557
|
// the redacted `version` — closing the bypass where folded `evaluate("…")` ran on an
|
|
305
558
|
// unhardened instance (#18).
|
|
306
|
-
this.parse = this.defaultInstance.parse;
|
|
307
559
|
this.simplify = this.defaultInstance.simplify;
|
|
308
560
|
this.derivative = this.defaultInstance.derivative;
|
|
309
561
|
this.simplifyRules = [...this.defaultInstance.simplifyRules, ...TRIG_SIMPLIFY_RULES];
|
|
@@ -324,56 +576,57 @@ export class MathService {
|
|
|
324
576
|
this.validateInput(expression, ctx);
|
|
325
577
|
if (scope)
|
|
326
578
|
this.validateScope(scope, ctx);
|
|
327
|
-
const normalized = normalizeNotation(expression);
|
|
328
|
-
this.validateNoFunctionStringification(normalized, ctx);
|
|
329
579
|
const inst = this.instanceFor(numericType);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
this.
|
|
333
|
-
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
580
|
+
// Parse with the instance for the requested numericType: constant nodes take
|
|
581
|
+
// their numeric type at parse time.
|
|
582
|
+
const { tree, functionValues } = this.parseExpression(expression, inst, ctx);
|
|
583
|
+
// Evaluation, result inspection, and formatting all run inside the timeout, so
|
|
584
|
+
// no result reaches formatting unchecked and no step runs unbounded (#31).
|
|
585
|
+
return this.runWithTimeout(() => {
|
|
586
|
+
// Evaluate against a copy: an assignment in the expression (`(y = 2) + x`)
|
|
587
|
+
// writes into the scope it is given, and the caller's object must not change (#34).
|
|
588
|
+
const variables = Object.entries(scope ?? {}).map(([name, value]) => [name, inst.scopeValue(value)]);
|
|
589
|
+
const raw = tree.compile().evaluate(new Map(variables));
|
|
590
|
+
const resultType = inst.typeOf(raw);
|
|
591
|
+
this.validateResultType(resultType, ctx);
|
|
592
|
+
this.validateResultValue(raw, ctx, numericType);
|
|
593
|
+
// Match JS Number.toString thresholds — math.js defaults to exp ≥ 5,
|
|
594
|
+
// which would render 83810205 as "8.3810205e+7".
|
|
595
|
+
const result = inst.format(raw, {
|
|
596
|
+
lowerExp: -6,
|
|
597
|
+
upperExp: 21,
|
|
598
|
+
...(precision != null && { precision }),
|
|
599
|
+
});
|
|
600
|
+
this.validateResultSize(result, ctx);
|
|
601
|
+
return { result, resultType };
|
|
602
|
+
}, ctx, {
|
|
603
|
+
operation: 'evaluate',
|
|
604
|
+
numericType,
|
|
605
|
+
// A scope value shadows the function of the same name.
|
|
606
|
+
functionValues: functionValues.filter((name) => !(scope && Object.hasOwn(scope, name))),
|
|
340
607
|
});
|
|
341
|
-
this.validateResultSize(result, ctx);
|
|
342
|
-
return { result, resultType };
|
|
343
608
|
}
|
|
344
609
|
/** Simplify an algebraic expression symbolically. */
|
|
345
610
|
simplifyExpression(expression, ctx) {
|
|
346
611
|
this.validateInput(expression, ctx);
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
let inputNormalized;
|
|
355
|
-
try {
|
|
356
|
-
inputNormalized = this.parse(normalized).toString();
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
// If the expression cannot be parsed, let the simplify step surface the error
|
|
360
|
-
// with full math.js context. Set to the raw input so the unchanged flag is
|
|
361
|
-
// false (we don't know, but a parse error is not a no-op simplification).
|
|
362
|
-
inputNormalized = '';
|
|
363
|
-
}
|
|
364
|
-
const simplified = this.runWithTimeout(() => this.simplify(normalized, this.simplifyRules), ctx);
|
|
365
|
-
const result = simplified.toString();
|
|
612
|
+
const { tree } = this.parseExpression(expression, this.defaultInstance, ctx);
|
|
613
|
+
// Capture the alias-normalized input before simplification so we can detect
|
|
614
|
+
// whether the simplifier made any progress. String comparison of the raw input
|
|
615
|
+
// is not sufficient — formatting-only changes like `x+1` vs `x + 1` should not
|
|
616
|
+
// count as progress — so both sides are compared as math.js renders them.
|
|
617
|
+
const inputNormalized = tree.toString();
|
|
618
|
+
const result = this.runWithTimeout(() => this.simplify(tree, this.simplifyRules).toString(), ctx, { operation: 'simplify' });
|
|
366
619
|
this.validateResultSize(result, ctx);
|
|
367
|
-
|
|
368
|
-
return { result, resultType: 'string', unchanged };
|
|
620
|
+
return { result, resultType: 'string', unchanged: inputNormalized === result };
|
|
369
621
|
}
|
|
370
622
|
/** Compute the symbolic derivative of an expression with respect to a variable. */
|
|
371
623
|
differentiateExpression(expression, variable, ctx) {
|
|
372
624
|
this.validateInput(expression, ctx);
|
|
373
|
-
const
|
|
374
|
-
this.
|
|
375
|
-
|
|
376
|
-
|
|
625
|
+
const { tree } = this.parseExpression(expression, this.defaultInstance, ctx);
|
|
626
|
+
const result = this.runWithTimeout(() => this.derivative(tree, variable).toString(), ctx, {
|
|
627
|
+
operation: 'derivative',
|
|
628
|
+
variable,
|
|
629
|
+
});
|
|
377
630
|
this.validateResultSize(result, ctx);
|
|
378
631
|
return { result, resultType: 'string' };
|
|
379
632
|
}
|
|
@@ -383,107 +636,273 @@ export class MathService {
|
|
|
383
636
|
}
|
|
384
637
|
validateInput(expression, ctx) {
|
|
385
638
|
if (!expression.trim()) {
|
|
386
|
-
throw
|
|
387
|
-
reason: 'empty_expression',
|
|
388
|
-
...ctx.recoveryFor('empty_expression'),
|
|
389
|
-
});
|
|
639
|
+
throw declared(ctx, 'empty_expression', 'Expression cannot be empty.');
|
|
390
640
|
}
|
|
391
641
|
if (expression.length > this.config.maxExpressionLength) {
|
|
392
|
-
throw
|
|
642
|
+
throw declared(ctx, 'expression_too_long', `Expression exceeds maximum length of ${this.config.maxExpressionLength} characters.`);
|
|
393
643
|
}
|
|
394
|
-
|
|
395
|
-
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Parse once and check the tree before anything evaluates. A parse failure is
|
|
647
|
+
* `parse_failed`. Multiple statements are detected structurally — math.js parses
|
|
648
|
+
* `a; b` and a top-level newline into a `BlockNode` — so `;` inside a matrix or
|
|
649
|
+
* inside either quote style (`"a;b"`, `'a;b'`) is data, transpose `A'` stays an
|
|
650
|
+
* operator, and a real separator is rejected before either statement runs (#32).
|
|
651
|
+
* math.js treats a bare carriage return as a syntax error rather than a
|
|
652
|
+
* separator; when it is what broke the parse, the input is re-parsed with line
|
|
653
|
+
* feeds purely to report `multiple_expressions` — the rewritten text is never
|
|
654
|
+
* evaluated. The same tree then gets the stringifier check, the
|
|
655
|
+
* operation-as-function check (#27), and the notation rename (#24), and yields
|
|
656
|
+
* the names it uses as values that resolve to functions (#38). Every operation
|
|
657
|
+
* starts here, so it also resets the instance's auto unit system (#37).
|
|
658
|
+
*/
|
|
659
|
+
parseExpression(expression, inst, ctx) {
|
|
660
|
+
inst.resetUnitSystem();
|
|
661
|
+
let tree;
|
|
662
|
+
try {
|
|
663
|
+
tree = inst.parse(expression);
|
|
396
664
|
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
if (expression.includes('\r') &&
|
|
667
|
+
this.parsesAsBlock(expression.replace(/\r\n?/g, '\n'), inst)) {
|
|
668
|
+
throw this.multipleExpressions(ctx);
|
|
669
|
+
}
|
|
670
|
+
throw declared(ctx, 'parse_failed', `Invalid expression: ${errorMessage(err)}`);
|
|
671
|
+
}
|
|
672
|
+
if (tree.type === 'BlockNode')
|
|
673
|
+
throw this.multipleExpressions(ctx);
|
|
674
|
+
const { stringifies, operationCall, valueNames } = inspectTree(tree);
|
|
675
|
+
// `.toString()` / `.toLocaleString()` on a function-valued identifier
|
|
676
|
+
// (`cos.toString()`) returns the function's source as a plain string, slipping
|
|
677
|
+
// past validateResultType, which only sees the value after stringification. No
|
|
678
|
+
// calculator expression needs either method, so the accessor is rejected on any
|
|
679
|
+
// operand; a string literal containing "toString" is a ConstantNode, not an
|
|
680
|
+
// accessor, and is unaffected.
|
|
681
|
+
if (stringifies) {
|
|
682
|
+
throw declared(ctx, 'disallowed_result_type', 'Converting a function to a string is not allowed — it would expose internal source.');
|
|
683
|
+
}
|
|
684
|
+
if (operationCall !== undefined) {
|
|
685
|
+
const variableHint = operationCall === 'derivative' ? ' and pass `variable`' : '';
|
|
686
|
+
throw declared(ctx, 'operation_as_function', `"${operationCall}" is an operation, not a function available inside expressions. Send the inner expression as \`expression\` with \`operation: "${operationCall}"\`${variableHint}.`);
|
|
687
|
+
}
|
|
688
|
+
return { tree, functionValues: [...valueNames].filter(inst.isFunction) };
|
|
689
|
+
}
|
|
690
|
+
/** Whether `expression` parses to multiple statements. */
|
|
691
|
+
parsesAsBlock(expression, inst) {
|
|
692
|
+
try {
|
|
693
|
+
return inst.parse(expression).type === 'BlockNode';
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
multipleExpressions(ctx) {
|
|
700
|
+
return declared(ctx, 'multiple_expressions', 'Multiple expressions are not allowed. Submit one expression per call.');
|
|
397
701
|
}
|
|
398
702
|
/** Reject scope keys that could pollute the object prototype chain. */
|
|
399
703
|
validateScope(scope, ctx) {
|
|
400
704
|
for (const key of Object.keys(scope)) {
|
|
401
705
|
if (BLOCKED_SCOPE_KEYS.has(key)) {
|
|
402
|
-
throw
|
|
706
|
+
throw declared(ctx, 'reserved_scope_key', `Scope key "${key}" is not allowed — it conflicts with a reserved property name.`);
|
|
403
707
|
}
|
|
404
708
|
}
|
|
405
709
|
}
|
|
406
710
|
/** Reject result types that leak internals (functions, parsers, multi-expression ResultSets). */
|
|
407
711
|
validateResultType(resultType, ctx) {
|
|
408
712
|
if (BLOCKED_RESULT_TYPES.has(resultType)) {
|
|
409
|
-
throw
|
|
713
|
+
throw declared(ctx, 'disallowed_result_type', `Expression produced a ${resultType}, which cannot be returned — the result must be a value, not a function.`);
|
|
410
714
|
}
|
|
411
715
|
}
|
|
412
|
-
/**
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
716
|
+
/**
|
|
717
|
+
* Inspect a result before it is formatted: reject a `help()` object (its
|
|
718
|
+
* rendering evaluates the documentation examples, #31), a collection too large
|
|
719
|
+
* to ever fit `maxResultLength` (checked before formatting, so it is never
|
|
720
|
+
* stringified), any non-finite number, scalar or nested (#21), and under
|
|
721
|
+
* numericType "Fraction", any float approximation, scalar or nested (#35).
|
|
722
|
+
*/
|
|
723
|
+
validateResultValue(raw, ctx, numericType) {
|
|
724
|
+
const { maxResultLength } = this.config;
|
|
725
|
+
if (typeof raw === 'string' && raw.length > maxResultLength) {
|
|
726
|
+
throw this.resultTooLarge(ctx);
|
|
416
727
|
}
|
|
728
|
+
const elementLimit = Math.floor(maxResultLength / MIN_CHARS_PER_ELEMENT);
|
|
729
|
+
const scan = {
|
|
730
|
+
elements: 0,
|
|
731
|
+
help: false,
|
|
732
|
+
nonFinite: false,
|
|
733
|
+
exactOnly: numericType === 'Fraction',
|
|
734
|
+
inexact: false,
|
|
735
|
+
};
|
|
736
|
+
scanResult(raw, elementLimit, scan);
|
|
737
|
+
if (scan.help) {
|
|
738
|
+
throw declared(ctx, 'disallowed_result_type', 'help() is not available inside expressions — read the calculator://help resource for the function reference.');
|
|
739
|
+
}
|
|
740
|
+
if (scan.elements > elementLimit)
|
|
741
|
+
throw this.resultTooLarge(ctx);
|
|
742
|
+
if (scan.nonFinite)
|
|
743
|
+
throw declared(ctx, 'undefined_result', UNDEFINED_RESULT_MESSAGE);
|
|
744
|
+
if (scan.inexact)
|
|
745
|
+
throw declared(ctx, 'fraction_unsupported', FRACTION_FLOAT_MESSAGE);
|
|
417
746
|
}
|
|
418
747
|
/** Reject results that exceed the configured maximum size. */
|
|
419
748
|
validateResultSize(result, ctx) {
|
|
420
|
-
if (result.length > this.config.maxResultLength)
|
|
421
|
-
throw
|
|
422
|
-
|
|
749
|
+
if (result.length > this.config.maxResultLength)
|
|
750
|
+
throw this.resultTooLarge(ctx);
|
|
751
|
+
}
|
|
752
|
+
resultTooLarge(ctx) {
|
|
753
|
+
return declared(ctx, 'result_too_large', `Result exceeds maximum size (${this.config.maxResultLength} characters). Reduce matrix dimensions or simplify the expression.`);
|
|
423
754
|
}
|
|
424
755
|
/**
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* the prototype would reject legitimate expressions. No real calculator expression
|
|
433
|
-
* needs `.toString()`/`.toLocaleString()`, so blocking the accessor outright (dot
|
|
434
|
-
* or bracket form, on any operand) is both sufficient and side-effect-free. String
|
|
435
|
-
* literals that merely contain the text "toString" parse as ConstantNodes, not
|
|
436
|
-
* accessors, so they are unaffected.
|
|
756
|
+
* Runs a synchronous function inside a vm sandbox with timeout protection and
|
|
757
|
+
* maps anything it throws to a declared reason via {@link classifyFailure}.
|
|
758
|
+
*
|
|
759
|
+
* The context exists only to carry the timeout — `fn` and everything it calls
|
|
760
|
+
* run in this realm — so one context is reused for every call and cleared
|
|
761
|
+
* afterwards. A fresh context per call costs ~100 µs and, under Bun, retains
|
|
762
|
+
* ~100 KB of memory per call.
|
|
437
763
|
*/
|
|
438
|
-
|
|
439
|
-
|
|
764
|
+
runWithTimeout(fn, ctx, stage) {
|
|
765
|
+
const { sandbox } = this;
|
|
766
|
+
sandbox.fn = fn;
|
|
767
|
+
beginEvaluation();
|
|
440
768
|
try {
|
|
441
|
-
|
|
769
|
+
TIMED_CALL.runInContext(sandbox, { timeout: this.config.evaluationTimeoutMs });
|
|
770
|
+
return sandbox.result;
|
|
442
771
|
}
|
|
443
|
-
catch {
|
|
444
|
-
|
|
445
|
-
return;
|
|
772
|
+
catch (err) {
|
|
773
|
+
throw this.classifyFailure(err, ctx, stage);
|
|
446
774
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const { dimensions } = node;
|
|
451
|
-
return dimensions.some((dim) => dim.type === 'ConstantNode' && STRINGIFYING_METHODS.has(String(dim.value)));
|
|
452
|
-
});
|
|
453
|
-
if (accessesStringifier.length > 0) {
|
|
454
|
-
throw validationError('Converting a function to a string is not allowed — it would expose internal source. Only numeric, string, matrix, complex, unit, and boolean results are allowed.', { reason: 'disallowed_result_type', ...ctx.recoveryFor('disallowed_result_type') });
|
|
775
|
+
finally {
|
|
776
|
+
sandbox.fn = undefined;
|
|
777
|
+
sandbox.result = undefined;
|
|
455
778
|
}
|
|
456
779
|
}
|
|
457
780
|
/**
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
781
|
+
* Classify a failure raised after the expression parsed (evaluation, result
|
|
782
|
+
* inspection, formatting, or a symbolic operation). First match wins:
|
|
783
|
+
*
|
|
784
|
+
* 1. Already-classified `McpError`s (result checks) pass through.
|
|
785
|
+
* 2. Timeout → `evaluation_timeout`.
|
|
786
|
+
* 3. A size guard (see size-guard.ts) → `result_too_large`.
|
|
787
|
+
* 4. Fraction mode calling a function with no Fraction implementation (sqrt,
|
|
788
|
+
* sin, log, factorial, …) → `fraction_unsupported` (#19); so does Fraction
|
|
789
|
+
* mode meeting a float with no equal Fraction (`pi * 2/3`, #35).
|
|
790
|
+
* 5. fraction.js `Division by Zero`, in any numericType → `undefined_result` (#21).
|
|
791
|
+
* 6. Name errors (undefined symbol/function/unit, disabled function, blocked
|
|
792
|
+
* property) → `parse_failed`.
|
|
793
|
+
* 7. A symbolic operation (simplify, derivative) that cannot process the parsed
|
|
794
|
+
* tree → `evaluation_failed`, with a hint about the symbolic engine rather
|
|
795
|
+
* than about argument values (see {@link symbolicFailure}).
|
|
796
|
+
* 8. Operand type/unit errors (typed-function `wrongType`, unit mismatch,
|
|
797
|
+
* string-to-number) → `type_mismatch`.
|
|
798
|
+
* 9. Anything else — arity, domain, singular matrix, dimensions, index →
|
|
799
|
+
* `evaluation_failed` (#29). Stage decides, not error class: a runtime
|
|
800
|
+
* `SyntaxError` such as `number("abc")` lands here, never in `parse_failed`.
|
|
462
801
|
*/
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
802
|
+
classifyFailure(err, ctx, stage) {
|
|
803
|
+
const { numericType } = stage;
|
|
804
|
+
if (err instanceof McpError)
|
|
805
|
+
return err;
|
|
806
|
+
const message = errorMessage(err);
|
|
807
|
+
// math.js's map/forEach/filter re-wrap a callback's error in a new one that
|
|
808
|
+
// embeds its message, so a timeout inside a callback loses the error code.
|
|
809
|
+
const timedOut = (err instanceof Error && 'code' in err && err.code === 'ERR_SCRIPT_EXECUTION_TIMEOUT') ||
|
|
810
|
+
VM_TIMEOUT_ERROR.test(message);
|
|
811
|
+
if (timedOut) {
|
|
812
|
+
return timeout(`Expression evaluation timed out after ${this.config.evaluationTimeoutMs / 1000} seconds. Simplify the expression or reduce matrix dimensions.`, { reason: 'evaluation_timeout', ...ctx.recoveryFor('evaluation_timeout') });
|
|
468
813
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
474
|
-
// Fraction mode cannot represent an irrational/transcendental result (sqrt, sin,
|
|
475
|
-
// log, …); math.js surfaces this as an implicit-conversion error. Remap to a
|
|
476
|
-
// dedicated, actionable error rather than the syntax-oriented parse_failed.
|
|
477
|
-
if (numericType === 'Fraction' && FRACTION_CONVERSION_ERROR.test(message)) {
|
|
478
|
-
throw validationError('This expression has no exact fractional value (irrational or transcendental result, e.g. sqrt, sin, log). Retry with numericType "number" or "BigNumber".', { reason: 'fraction_unsupported', ...ctx.recoveryFor('fraction_unsupported') });
|
|
479
|
-
}
|
|
480
|
-
// All other non-timeout errors from the VM are expression-related.
|
|
481
|
-
throw validationError(`Invalid expression: ${message}`, {
|
|
482
|
-
reason: 'parse_failed',
|
|
483
|
-
...ctx.recoveryFor('parse_failed'),
|
|
484
|
-
});
|
|
814
|
+
const limitError = sizeLimitErrorIn(err);
|
|
815
|
+
if (limitError) {
|
|
816
|
+
return declared(ctx, 'result_too_large', `Size limit exceeded: ${limitError.message}`);
|
|
485
817
|
}
|
|
818
|
+
if (numericType === 'Fraction' && FRACTION_CONVERSION_ERROR.test(message)) {
|
|
819
|
+
return declared(ctx, 'fraction_unsupported', FRACTION_FUNCTION_MESSAGE);
|
|
820
|
+
}
|
|
821
|
+
if (numericType === 'Fraction' && FLOAT_TO_FRACTION_ERROR.test(message)) {
|
|
822
|
+
return declared(ctx, 'fraction_unsupported', FRACTION_FLOAT_MESSAGE);
|
|
823
|
+
}
|
|
824
|
+
if (FRACTION_DIVISION_BY_ZERO.test(message)) {
|
|
825
|
+
return declared(ctx, 'undefined_result', UNDEFINED_RESULT_MESSAGE);
|
|
826
|
+
}
|
|
827
|
+
if (NAME_ERROR.test(message)) {
|
|
828
|
+
return declared(ctx, 'parse_failed', `Invalid expression: ${message}`);
|
|
829
|
+
}
|
|
830
|
+
if (stage.operation !== 'evaluate')
|
|
831
|
+
return symbolicFailure(stage, message);
|
|
832
|
+
const data = err?.data;
|
|
833
|
+
const category = data?.category;
|
|
834
|
+
if (category === 'wrongType' &&
|
|
835
|
+
Array.isArray(data?.actual) &&
|
|
836
|
+
data.actual.includes('function')) {
|
|
837
|
+
return functionAsValue(stage.functionValues, message);
|
|
838
|
+
}
|
|
839
|
+
if (category === 'wrongType' || TYPE_MISMATCH_ERROR.test(message)) {
|
|
840
|
+
return declared(ctx, 'type_mismatch', `Type mismatch: ${message}`);
|
|
841
|
+
}
|
|
842
|
+
return declared(ctx, 'evaluation_failed', `Evaluation failed: ${message}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* `evaluation_failed` for a symbolic operation that parsed but could not be
|
|
847
|
+
* carried out. The hint is written at the throw site: the contract hint is about
|
|
848
|
+
* fixing an argument, but here the syntax and arguments are fine — the symbolic
|
|
849
|
+
* engine has no rule for part of the expression (a function such as `floor` or
|
|
850
|
+
* an operator such as `==` in a derivative, or a matrix or conditional node).
|
|
851
|
+
*/
|
|
852
|
+
function symbolicFailure(stage, message) {
|
|
853
|
+
const { operation } = stage;
|
|
854
|
+
const noRule = operation === 'derivative' && NO_DERIVATIVE_RULE.exec(message);
|
|
855
|
+
if (noRule) {
|
|
856
|
+
const [, kind, name] = noRule;
|
|
857
|
+
return validationError(`The derivative operation has no rule for the ${kind} "${name}", so it cannot differentiate this expression symbolically.`, {
|
|
858
|
+
reason: 'evaluation_failed',
|
|
859
|
+
recovery: {
|
|
860
|
+
hint: `"${name}" has no symbolic derivative rule — rewrite the expression without it, or evaluate it numerically with operation "evaluate", passing values for its variables (such as ${stage.variable}) through scope.`,
|
|
861
|
+
},
|
|
862
|
+
});
|
|
486
863
|
}
|
|
864
|
+
return validationError(`The ${operation} operation cannot process this expression symbolically: ${message}`, {
|
|
865
|
+
reason: 'evaluation_failed',
|
|
866
|
+
recovery: {
|
|
867
|
+
hint: `The ${operation} engine cannot handle part of this expression (for example a matrix or conditional) — rewrite it with scalar functions and operators, or evaluate it numerically with operation "evaluate" and values passed through scope.`,
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* `type_mismatch` for a function where a value belongs: math.js reads a name or
|
|
873
|
+
* definition next to a value as multiplication (`5 min` is 5 times the minimum
|
|
874
|
+
* function, `(f(x) = x^2)(3)` is the function times 3). The contract hint is about
|
|
875
|
+
* units, so the hint is written here (#38), naming the functions the expression
|
|
876
|
+
* uses as values and the unit to write for one that shadows a unit name.
|
|
877
|
+
*/
|
|
878
|
+
function functionAsValue(names, detail) {
|
|
879
|
+
const described = names.map((name) => {
|
|
880
|
+
const unit = FUNCTION_SHADOWED_UNITS[name];
|
|
881
|
+
return unit ? `"${name}" is a function, not the unit "${unit}"` : `"${name}" is a function`;
|
|
882
|
+
});
|
|
883
|
+
const unitHints = names.flatMap((name) => {
|
|
884
|
+
const unit = FUNCTION_SHADOWED_UNITS[name];
|
|
885
|
+
return unit
|
|
886
|
+
? [`"${name}" names a function here; for the unit, write ${unit} (\`5 ${unit}\`).`]
|
|
887
|
+
: [];
|
|
888
|
+
});
|
|
889
|
+
return validationError(`Type mismatch: a function was used as a value${described.length ? ` (${described.join('; ')})` : ''}. ${detail}`, {
|
|
890
|
+
reason: 'type_mismatch',
|
|
891
|
+
recovery: {
|
|
892
|
+
hint: [
|
|
893
|
+
...unitHints,
|
|
894
|
+
'To call a function, use parentheses (`sin(1)`, `f(3)`); a function name or definition next to a value multiplies it instead.',
|
|
895
|
+
].join(' '),
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
/** A validation error carrying `reason` and the recovery hint its contract entry declares. */
|
|
900
|
+
function declared(ctx, reason, message) {
|
|
901
|
+
return validationError(message, { reason, ...ctx.recoveryFor(reason) });
|
|
902
|
+
}
|
|
903
|
+
/** Message of a thrown value, whatever its type. */
|
|
904
|
+
function errorMessage(err) {
|
|
905
|
+
return err instanceof Error ? err.message : String(err);
|
|
487
906
|
}
|
|
488
907
|
// --- Init/accessor pattern ---
|
|
489
908
|
let _service;
|
|
@@ -520,11 +939,13 @@ const HELP_CONTENT = `# Calculator Help
|
|
|
520
939
|
| e | 2.71828... | Euler's number |
|
|
521
940
|
| phi | 1.61803... | Golden ratio |
|
|
522
941
|
| i | sqrt(-1) | Imaginary unit |
|
|
523
|
-
| Infinity | Infinity | Positive infinity |
|
|
524
|
-
| NaN | NaN | Not a number |
|
|
942
|
+
| Infinity | Infinity | Positive infinity — intermediate values only |
|
|
943
|
+
| NaN | NaN | Not a number — intermediate values only |
|
|
525
944
|
| true | true | Boolean true |
|
|
526
945
|
| false | false | Boolean false |
|
|
527
946
|
|
|
947
|
+
\`Infinity\` and \`NaN\` work inside an expression (\`1/Infinity\` => \`0\`, \`isNaN(NaN)\` => \`true\`), but a result that is Infinity, -Infinity, or NaN anywhere in it fails with \`undefined_result\`: \`Infinity\` and \`NaN\` on their own fail.
|
|
948
|
+
|
|
528
949
|
## Functions
|
|
529
950
|
|
|
530
951
|
> **Standard notation accepted:** \`ln\` works as natural log (canonical \`log\`), and the \`arc*\` inverse-trig names (\`arcsin\`, \`arccos\`, \`arctan\`, \`arcsinh\`, …) work as their \`a*\` equivalents (\`asin\`, \`acos\`, \`atan\`, …). Both forms are valid across evaluate, simplify, and derivative.
|
|
@@ -533,11 +954,25 @@ const HELP_CONTENT = `# Calculator Help
|
|
|
533
954
|
abs, ceil, floor, round, sign, sqrt, cbrt, exp, expm1, log (also: ln), log2, log10, log1p, pow, mod, gcd, lcm, nthRoot, hypot, fix, cube, square, unaryMinus, unaryPlus
|
|
534
955
|
|
|
535
956
|
### Trigonometry
|
|
536
|
-
sin, cos, tan, asin (arcsin), acos (arccos), atan (arctan), atan2, sinh, cosh, tanh, asinh (arcsinh), acosh (arccosh), atanh (arctanh), sec, csc, cot, asec (arcsec), acsc (arccsc), acot (arccot), sech (arcsech),
|
|
957
|
+
sin, cos, tan, asin (arcsin), acos (arccos), atan (arctan), atan2, sinh, cosh, tanh, asinh (arcsinh), acosh (arccosh), atanh (arctanh), sec, csc, cot, asec (arcsec), acsc (arccsc), acot (arccot), sech, csch, coth, asech (arcsech), acsch (arccsch), acoth (arccoth)
|
|
537
958
|
|
|
538
959
|
### Statistics
|
|
539
960
|
mean (aliases: average, avg), median, mode, std (aliases: stdev, stddev), variance, min, max, sum, prod, quantileSeq, mad, count (aliases: length, len)
|
|
540
961
|
|
|
962
|
+
\`std\` and \`variance\` (and the \`stdev\`/\`stddev\` aliases) return the sample statistic by default. An optional normalization string after the array or matrix picks the divisor:
|
|
963
|
+
|
|
964
|
+
| Normalization | Divisor | \`std([2, 4, 6], …)\` | \`variance([2, 4, 6], …)\` |
|
|
965
|
+
|:--------------|:--------|:--------------------|:-------------------------|
|
|
966
|
+
| \`"unbiased"\` (default, sample) | n − 1 | \`2\` | \`4\` |
|
|
967
|
+
| \`"uncorrected"\` (population) | n | \`1.632993161855452\` | \`2.6666666666666665\` |
|
|
968
|
+
| \`"biased"\` | n + 1 | \`1.4142135623730951\` | \`2\` |
|
|
969
|
+
|
|
970
|
+
\`std([2, 4, 6], "uncorrected")\` => \`1.632993161855452\`. The normalization must follow an array or matrix: \`std(2, 4, 6, "uncorrected")\` fails.
|
|
971
|
+
|
|
972
|
+
- \`mad\` is the unscaled median absolute deviation (no 1.4826 factor): \`mad([1, 2, 3, 4, 100])\` => \`1\`
|
|
973
|
+
- \`quantileSeq\` interpolates linearly: \`quantileSeq([1, 2, 3, 4], 0.25)\` => \`1.75\`. A third argument \`true\` declares the data already sorted and skips sorting, so unsorted data then gives a wrong answer: \`quantileSeq([4, 3, 2, 1], 0.25, true)\` => \`3.25\`
|
|
974
|
+
- \`mode\` always returns an array, even with a single mode: \`mode([1, 2, 2, 3])\` => \`[2]\`
|
|
975
|
+
|
|
541
976
|
### Matrix
|
|
542
977
|
det, inv, transpose, trace, zeros, ones, identity, diag, size, reshape, flatten, concat, sort, cross, dot, eigs, expm, sqrtm, kron, pinv, range
|
|
543
978
|
|
|
@@ -556,55 +991,60 @@ equal, unequal, larger, largerEq, smaller, smallerEq, compare, deepEqual
|
|
|
556
991
|
### Unit Conversion
|
|
557
992
|
Syntax: \`value unit to targetUnit\`
|
|
558
993
|
|
|
559
|
-
Common units: m, cm, mm, km, inch, ft, yard, mile, lightyear (ly), kg, g, lb, oz, s,
|
|
994
|
+
Common units: m, cm, mm, km, inch, ft, yard, mile, lightyear (ly), kg, g, lb, oz, s, minute, hour, day, mph, knot (kt), celsius, fahrenheit, kelvin, liter, gallon, joule, watt, newton, Pa, bar, psi, radian, degree
|
|
995
|
+
|
|
996
|
+
\`min\` is the minimum function, not minutes, so write \`minute\`: \`5 minute to s\` => \`300 s\`. Pascals are \`Pa\`: \`1 bar to Pa\` => \`100000 Pa\`.
|
|
560
997
|
|
|
561
998
|
## Syntax Examples
|
|
562
999
|
|
|
1000
|
+
Each example shows the exact result string \`calculate\` returns.
|
|
1001
|
+
|
|
563
1002
|
### Basic arithmetic
|
|
564
|
-
\`2 + 3 * 4\` => 14
|
|
1003
|
+
\`2 + 3 * 4\` => \`14\`
|
|
565
1004
|
|
|
566
1005
|
### Functions
|
|
567
|
-
\`sqrt(144)\` => 12
|
|
568
|
-
\`sin(pi / 2)\` => 1
|
|
569
|
-
\`log(1000, 10)\` => 3
|
|
1006
|
+
\`sqrt(144)\` => \`12\`
|
|
1007
|
+
\`sin(pi / 2)\` => \`1\`
|
|
1008
|
+
\`log(1000, 10)\` => \`2.9999999999999996\` (floating-point rounding); with \`precision: 10\`, \`log(1000, 10)\` => \`3\`
|
|
570
1009
|
|
|
571
|
-
### Variables (
|
|
1010
|
+
### Variables (scope parameter, evaluate only)
|
|
572
1011
|
Provide scope: { "x": 5, "y": 3 }
|
|
573
|
-
Expression: \`x^2 + y\` => 28
|
|
1012
|
+
Expression: \`x^2 + y\` => \`28\`
|
|
574
1013
|
|
|
575
1014
|
### Matrices
|
|
576
|
-
\`[1, 2; 3, 4]\` — 2x2 matrix
|
|
577
|
-
\`det([1, 2; 3, 4])\` =>
|
|
578
|
-
\`inv([1, 2; 3, 4])\` => [[-2, 1], [1.5, -0.5]]
|
|
1015
|
+
\`[1, 2; 3, 4]\` — 2x2 matrix (\`;\` separates rows)
|
|
1016
|
+
\`det([1, 2; 3, 4])\` => \`-2\`
|
|
1017
|
+
\`inv([1, 2; 3, 4])\` => \`[[-2, 1], [1.5, -0.5]]\`
|
|
579
1018
|
|
|
580
1019
|
### Complex numbers
|
|
581
|
-
\`2 + 3i\` => 2 + 3i
|
|
582
|
-
\`sqrt(-4)\` => 2i
|
|
583
|
-
\`abs(3 + 4i)\` => 5
|
|
1020
|
+
\`2 + 3i\` => \`2 + 3i\`
|
|
1021
|
+
\`sqrt(-4)\` => \`2i\`
|
|
1022
|
+
\`abs(3 + 4i)\` => \`5\`
|
|
1023
|
+
\`log(-1)\` => \`3.141592653589793i\` — the log of a negative number is complex, not undefined
|
|
584
1024
|
|
|
585
1025
|
### Unit conversion
|
|
586
|
-
\`5 kg to lbs\` => 11.
|
|
587
|
-
\`100 celsius to fahrenheit\` =>
|
|
588
|
-
\`1 mile to km\` => 1.
|
|
1026
|
+
\`5 kg to lbs\` => \`11.023113109243878 lbs\`
|
|
1027
|
+
\`100 celsius to fahrenheit\` => \`211.99999999999997 fahrenheit\`
|
|
1028
|
+
\`1 mile to km\` => \`1.609344 km\`
|
|
589
1029
|
|
|
590
|
-
> **Note:** Unit conversions use IEEE 754 floating-point arithmetic
|
|
1030
|
+
> **Note:** Unit conversions use IEEE 754 floating-point arithmetic, so results can carry rounding artifacts like the one above. Use the \`precision\` parameter to round: with \`precision: 6\`, \`100 celsius to fahrenheit\` => \`212 fahrenheit\`.
|
|
591
1031
|
|
|
592
1032
|
### Precision
|
|
593
|
-
|
|
1033
|
+
The \`precision\` parameter (1\u201316 significant digits) rounds numeric results: with \`precision: 4\`, \`1 / 3\` => \`0.3333\`. Fraction results always print exactly, and symbolic operations ignore it.
|
|
594
1034
|
|
|
595
1035
|
### Operations
|
|
596
1036
|
|
|
597
|
-
- **evaluate** (default): Compute a numeric result.
|
|
598
|
-
- \`"number"\` (default): 64-bit IEEE 754 float — fastest.
|
|
599
|
-
- \`"BigNumber"\`:
|
|
600
|
-
- \`"Fraction"\`:
|
|
1037
|
+
- **evaluate** (default): Compute a numeric result. The \`numericType\` parameter picks the number representation:
|
|
1038
|
+
- \`"number"\` (default): 64-bit IEEE 754 float — fastest, about 16 significant digits. A value past about 1.8e308 overflows to Infinity and fails with \`undefined_result\` (\`171!\`, \`2^1024\`, \`exp(1000)\`).
|
|
1039
|
+
- \`"BigNumber"\`: decimal with 64 significant digits and a much wider exponent range; slower than \`"number"\`. Use it when a value overflows the float range: with \`numericType: "BigNumber"\`, \`2^2000\` => \`1.148130695274254524232833201177681984022317702088695200477642737e+602\`. Results are 64-digit approximations, not exact: \`10000! / 9999!\` => \`9999.999999999999999999999999999999999999999999999999999999999996\`, and adding \`precision: 16\` rounds it, \`10000! / 9999!\` => \`10000\`.
|
|
1040
|
+
- \`"Fraction"\`: exact rational arithmetic — \`0.1 + 0.2\` => \`3/10\`, \`1/3 + 1/6\` => \`1/2\`. An expression fails with \`fraction_unsupported\` when its result has no exact rational value (\`sqrt(2)\`, \`sin(1)\`, \`log(3)\`), when it calls a function Fraction mode cannot compute, even for a rational result (\`sqrt(4)\`, \`5!\`, \`combinations(5, 2)\`), or when it uses a value Fraction mode holds only as a rounded 64-bit float (\`pi\`, \`e\`, \`2^(1/2)\`, \`sin(pi)\`, a complex number with a non-integer part, \`number(x)\`, or \`random()\`); use \`"number"\` or \`"BigNumber"\` for those. A unit conversion with an irrational factor returns a close rational approximation, not an exact value: \`30 deg to rad\` => \`191068/364913 rad\`. Whole-number indexes and dimensions work as in \`"number"\`: \`[1, 2, 3][2]\` => \`2/1\`, \`sum([1, 2; 3, 4], 1)\` => \`[4/1, 6/1]\`.
|
|
601
1041
|
|
|
602
|
-
|
|
1042
|
+
An \`undefined_result\` caused by overflow (large powers, factorials, \`exp\`) can be retried with \`numericType: "BigNumber"\`. Division by zero, \`0/0\`, and \`log(0)\` are undefined in every numeric type, and BigNumber does not change that.
|
|
603
1043
|
|
|
604
|
-
- **simplify**: Reduce algebraic
|
|
1044
|
+
- **simplify**: Reduce an algebraic expression symbolically: \`2x + 3x\` => \`5 * x\`, \`sin(x)^2 + cos(x)^2\` => \`1\`. Supports algebraic rules and common trigonometric identities (Pythagorean, double-angle, tan/sec/csc/cot relationships). \`scope\`, \`precision\`, and \`numericType\` are ignored.
|
|
605
1045
|
|
|
606
|
-
**Known limits:** The built-in simplifier does not perform polynomial factoring or rational cancellation
|
|
1046
|
+
**Known limits:** The built-in simplifier does not perform polynomial factoring or rational cancellation: \`(x^2 - 1) / (x - 1)\` => \`(x ^ 2 - 1) / (x - 1)\`, returned with \`unchanged: true\`. For these cases, rewrite by hand or use \`evaluate\` with a numeric scope.
|
|
607
1047
|
|
|
608
|
-
- **derivative**: Compute symbolic derivative
|
|
1048
|
+
- **derivative**: Compute a symbolic derivative; requires the \`variable\` parameter. With \`variable: "x"\`, \`x^2\` => \`2 * x\`. \`scope\`, \`precision\`, and \`numericType\` are ignored.
|
|
609
1049
|
`;
|
|
610
1050
|
//# sourceMappingURL=math-service.js.map
|