@cyanheads/calculator-mcp-server 0.4.3 → 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 +4 -4
- package/CLAUDE.md +4 -4
- package/README.md +4 -3
- package/changelog/0.5.x/0.5.0.md +44 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/calculate.tool.d.ts +28 -10
- package/dist/mcp-server/tools/definitions/calculate.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/calculate.tool.js +81 -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 +3 -3
- package/server.json +3 -3
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Size limits for expression evaluation. The evaluation timeout
|
|
3
|
+
* bounds time; these bound memory. Two layers:
|
|
4
|
+
*
|
|
5
|
+
* - Per-call limits on math.js functions whose result size is set by an argument
|
|
6
|
+
* — a size vector, a count, a product or broadcast of inputs, a repetition of
|
|
7
|
+
* inputs (`concat(A, A, A)`), an index that reads or grows a matrix, or a
|
|
8
|
+
* formatting precision — rather than by the size of any one input. Each guard
|
|
9
|
+
* computes the size the call is about to build and throws
|
|
10
|
+
* {@link SizeLimitError} before allocating.
|
|
11
|
+
* - A per-evaluation element budget for what many individually allowed results
|
|
12
|
+
* add up to — repeated copies, or a loop (`map`, `forEach`, `filter`, a
|
|
13
|
+
* user-defined function) that builds a collection per iteration. Every guarded
|
|
14
|
+
* call and every value-producing node of the expression tree charges the
|
|
15
|
+
* elements (or characters) it builds; the count is deterministic, so the
|
|
16
|
+
* verdict never depends on when the garbage collector runs.
|
|
17
|
+
*
|
|
18
|
+
* The rebuilt size functions also accept whole-number Fraction sizes and counts,
|
|
19
|
+
* so `zeros(2)` works under numericType "Fraction" (see {@link WHOLE_NUMBER_ARGS}).
|
|
20
|
+
* A Fraction instance additionally gets the same conversion on its index
|
|
21
|
+
* functions (`index`, `row`, `column`) and on the dimension argument of its
|
|
22
|
+
* reductions and `concat`, so `[1, 2, 3][2]` and `sum(A, 1)` work there too.
|
|
23
|
+
* @module services/math/size-guard
|
|
24
|
+
*/
|
|
25
|
+
import { factory, isBigNumber, isFraction, isMatrix, isUnit, } from 'mathjs';
|
|
26
|
+
/**
|
|
27
|
+
* Largest collection, in elements, a guarded function may build in one call. One
|
|
28
|
+
* million keeps work such as `sum(range(1, 1e6))` available while refusing sizes
|
|
29
|
+
* whose allocation alone runs to gigabytes. It sits well above the largest
|
|
30
|
+
* collection that can be returned: a formatted element takes at least three
|
|
31
|
+
* characters, so `CALC_MAX_RESULT_LENGTH` (≤ 1,000,000) caps a returned
|
|
32
|
+
* collection near 333,000 elements.
|
|
33
|
+
*/
|
|
34
|
+
export const MAX_MATRIX_ELEMENTS = 1_000_000;
|
|
35
|
+
/**
|
|
36
|
+
* Longest string a guarded formatting function (`format`, `print`, `bin`, `oct`,
|
|
37
|
+
* `hex`) may build. Equal to the largest `CALC_MAX_RESULT_LENGTH`, so no
|
|
38
|
+
* returnable result is refused.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_STRING_LENGTH = 1_000_000;
|
|
41
|
+
/**
|
|
42
|
+
* Elements (string characters count as elements) one evaluation may build in
|
|
43
|
+
* total. A value is charged by the guarded call that builds it and again by the
|
|
44
|
+
* expression node that returns it, so this allows nine chained elementwise
|
|
45
|
+
* operations on a full-size (MAX_MATRIX_ELEMENTS) range, while a loop or a list
|
|
46
|
+
* of copies that keeps building collections stops within a few hundred
|
|
47
|
+
* megabytes. Scalars are not charged, so a long scalar loop is bounded only by
|
|
48
|
+
* the timeout.
|
|
49
|
+
*/
|
|
50
|
+
export const MAX_EVALUATION_ELEMENTS = 20 * MAX_MATRIX_ELEMENTS;
|
|
51
|
+
/** Thrown when an evaluation would exceed a size limit. */
|
|
52
|
+
export class SizeLimitError extends Error {
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'SizeLimitError';
|
|
56
|
+
}
|
|
57
|
+
/** A guarded function's output would exceed its per-call limit. */
|
|
58
|
+
static forCall(fn, size, limit, unit) {
|
|
59
|
+
const amount = Number.isFinite(size) ? Math.ceil(size).toLocaleString('en-US') : 'unbounded';
|
|
60
|
+
return new SizeLimitError(`${fn}() would build ${amount} ${unit}, over the limit of ${limit.toLocaleString('en-US')}.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Per-evaluation state. Evaluation is synchronous, so one module-level record
|
|
65
|
+
* serves every math.js instance; {@link beginEvaluation} resets it.
|
|
66
|
+
*/
|
|
67
|
+
const evaluation = {
|
|
68
|
+
/** Elements charged so far against {@link MAX_EVALUATION_ELEMENTS}. */
|
|
69
|
+
elements: 0,
|
|
70
|
+
/** The size-limit error raised during this evaluation, if any. */
|
|
71
|
+
limitError: undefined,
|
|
72
|
+
};
|
|
73
|
+
/** Start a new evaluation's element budget and forget any earlier limit error. */
|
|
74
|
+
export function beginEvaluation() {
|
|
75
|
+
evaluation.elements = 0;
|
|
76
|
+
evaluation.limitError = undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The size-limit error behind a failure, if one was raised during this
|
|
80
|
+
* evaluation: the error itself, or a math.js error that re-wrapped it — `map`,
|
|
81
|
+
* `forEach`, and `filter` rethrow a callback's error as a new error whose
|
|
82
|
+
* message embeds the original.
|
|
83
|
+
*/
|
|
84
|
+
export function sizeLimitErrorIn(err) {
|
|
85
|
+
if (err instanceof SizeLimitError)
|
|
86
|
+
return err;
|
|
87
|
+
const { limitError } = evaluation;
|
|
88
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
89
|
+
return limitError && message.includes(limitError.message) ? limitError : undefined;
|
|
90
|
+
}
|
|
91
|
+
function raise(error) {
|
|
92
|
+
evaluation.limitError = error;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
/** Charge `count` elements to this evaluation; throw once it passes {@link MAX_EVALUATION_ELEMENTS}. */
|
|
96
|
+
function charge(count) {
|
|
97
|
+
evaluation.elements += count;
|
|
98
|
+
if (evaluation.elements > MAX_EVALUATION_ELEMENTS) {
|
|
99
|
+
raise(new SizeLimitError(`The expression built over ${MAX_EVALUATION_ELEMENTS.toLocaleString('en-US')} elements in total while evaluating.`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Meter a node so each evaluation of it charges the size of the value it returns
|
|
104
|
+
* (see {@link sizeOf}). Wraps the node instance's own `_compile`, which its parent
|
|
105
|
+
* — or a lazy transform such as `map`, for its callback — calls when compiling,
|
|
106
|
+
* so a node inside a loop body is charged on every iteration.
|
|
107
|
+
*/
|
|
108
|
+
export function meterNode(node) {
|
|
109
|
+
const target = node;
|
|
110
|
+
const compile = target._compile.bind(target);
|
|
111
|
+
target._compile = (math, argNames) => {
|
|
112
|
+
const evaluate = compile(math, argNames);
|
|
113
|
+
return (...args) => {
|
|
114
|
+
const value = evaluate(...args);
|
|
115
|
+
charge(sizeOf(value));
|
|
116
|
+
return value;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Reject a call whose output would exceed the per-call element limit, then
|
|
122
|
+
* charge what it newly allocates — the whole output unless `allocated` says
|
|
123
|
+
* less (an in-place growth allocates only the added elements; a read, nothing
|
|
124
|
+
* the expression node returning it will not charge).
|
|
125
|
+
*/
|
|
126
|
+
function assertElements(fn, count, allocated = count) {
|
|
127
|
+
if (count > MAX_MATRIX_ELEMENTS) {
|
|
128
|
+
raise(SizeLimitError.forCall(fn, count, MAX_MATRIX_ELEMENTS, 'elements'));
|
|
129
|
+
}
|
|
130
|
+
charge(allocated);
|
|
131
|
+
}
|
|
132
|
+
function assertCharacters(fn, length) {
|
|
133
|
+
if (length > MAX_STRING_LENGTH) {
|
|
134
|
+
raise(SizeLimitError.forCall(fn, length, MAX_STRING_LENGTH, 'characters'));
|
|
135
|
+
}
|
|
136
|
+
charge(length);
|
|
137
|
+
}
|
|
138
|
+
/** Product of a shape's dimensions (1 for a scalar's empty shape). */
|
|
139
|
+
function countOf(shape) {
|
|
140
|
+
let n = 1;
|
|
141
|
+
for (const d of shape)
|
|
142
|
+
n *= d;
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
/** Shape of an Array or Matrix, or `null` for anything else. */
|
|
146
|
+
function shapeOf(value) {
|
|
147
|
+
if (isMatrix(value))
|
|
148
|
+
return value.size();
|
|
149
|
+
if (!Array.isArray(value))
|
|
150
|
+
return null;
|
|
151
|
+
const shape = [];
|
|
152
|
+
let level = value;
|
|
153
|
+
while (Array.isArray(level)) {
|
|
154
|
+
shape.push(level.length);
|
|
155
|
+
level = level[0];
|
|
156
|
+
}
|
|
157
|
+
return shape;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* What a value holds, for the evaluation budget: a collection's elements, a
|
|
161
|
+
* string's characters, or an object literal's values (and what they hold).
|
|
162
|
+
* Scalars — numbers, BigNumbers, Fractions, Units, booleans — count as zero.
|
|
163
|
+
*/
|
|
164
|
+
function sizeOf(value) {
|
|
165
|
+
if (typeof value === 'string')
|
|
166
|
+
return value.length;
|
|
167
|
+
const shape = shapeOf(value);
|
|
168
|
+
if (shape)
|
|
169
|
+
return countOf(shape);
|
|
170
|
+
if (!isPlainObject(value))
|
|
171
|
+
return 0;
|
|
172
|
+
let total = 0;
|
|
173
|
+
for (const item of Object.values(value))
|
|
174
|
+
total += 1 + sizeOf(item);
|
|
175
|
+
return total;
|
|
176
|
+
}
|
|
177
|
+
/** Plain object — the value an object literal (`{a: 1}`) evaluates to. */
|
|
178
|
+
export function isPlainObject(value) {
|
|
179
|
+
if (value === null || typeof value !== 'object')
|
|
180
|
+
return false;
|
|
181
|
+
const proto = Object.getPrototypeOf(value);
|
|
182
|
+
return proto === Object.prototype || proto === null;
|
|
183
|
+
}
|
|
184
|
+
/** Elements (characters, for strings) across an argument list — what joining them builds. */
|
|
185
|
+
function joinedSize(args) {
|
|
186
|
+
return args.reduce((total, arg) => total + sizeOf(arg), 0);
|
|
187
|
+
}
|
|
188
|
+
/** Numeric value of a size-like argument (number, bigint, BigNumber, Fraction, numeric string, Unit); NaN otherwise. */
|
|
189
|
+
function toNumber(value) {
|
|
190
|
+
if (typeof value === 'number')
|
|
191
|
+
return value;
|
|
192
|
+
if (typeof value === 'bigint' || typeof value === 'string' || typeof value === 'boolean') {
|
|
193
|
+
return Number(value);
|
|
194
|
+
}
|
|
195
|
+
if (isUnit(value))
|
|
196
|
+
return toNumber(value.value);
|
|
197
|
+
if (value !== null && typeof value === 'object') {
|
|
198
|
+
return Number(value.valueOf());
|
|
199
|
+
}
|
|
200
|
+
return Number.NaN;
|
|
201
|
+
}
|
|
202
|
+
/** Numeric entries of an argument list, with Array/Matrix arguments flattened one level. */
|
|
203
|
+
function sizeEntries(args) {
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const arg of args) {
|
|
206
|
+
if (typeof arg === 'string')
|
|
207
|
+
continue; // storage format ("dense" / "sparse")
|
|
208
|
+
const entries = isMatrix(arg) ? arg.valueOf() : arg;
|
|
209
|
+
if (Array.isArray(entries))
|
|
210
|
+
out.push(...entries.map(toNumber));
|
|
211
|
+
else
|
|
212
|
+
out.push(toNumber(entries));
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/** Product of the entries of a size-vector argument (Array or Matrix); 1 for a scalar. */
|
|
217
|
+
function sizeProduct(arg) {
|
|
218
|
+
return shapeOf(arg) ? countOf(sizeEntries([arg])) : 1;
|
|
219
|
+
}
|
|
220
|
+
/** Shape two operands broadcast to (math.js elementwise rules), or `null` when both are scalars. */
|
|
221
|
+
function broadcastShape(a, b) {
|
|
222
|
+
if (!a || !b)
|
|
223
|
+
return a ?? b;
|
|
224
|
+
const n = Math.max(a.length, b.length);
|
|
225
|
+
const out = [];
|
|
226
|
+
for (let i = 0; i < n; i++) {
|
|
227
|
+
out.push(Math.max(a[a.length - n + i] ?? 1, b[b.length - n + i] ?? 1));
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
/** Shape of a matrix product (`multiply`), or `null` when both operands are scalars. */
|
|
232
|
+
function productShape(a, b) {
|
|
233
|
+
if (!a || !b)
|
|
234
|
+
return a ?? b;
|
|
235
|
+
const rows = a.length === 2 ? [a[0]] : [];
|
|
236
|
+
const cols = b.length === 2 ? [b[1]] : [];
|
|
237
|
+
return [...rows, ...cols];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Fold an operand list with a shape combiner, checking and charging each shape
|
|
241
|
+
* the fold builds (the first operand already exists).
|
|
242
|
+
*/
|
|
243
|
+
function assertFoldedShape(fn, args, combine) {
|
|
244
|
+
let shape = shapeOf(args[0]);
|
|
245
|
+
for (const arg of args.slice(1)) {
|
|
246
|
+
shape = combine(shape, shapeOf(arg));
|
|
247
|
+
if (shape)
|
|
248
|
+
assertElements(fn, countOf(shape));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Element count `range(start, end, step)` produces, from numeric or `"start:end"` / `"start:step:end"` arguments. */
|
|
252
|
+
function rangeCount(args) {
|
|
253
|
+
let start;
|
|
254
|
+
let end;
|
|
255
|
+
let step = 1;
|
|
256
|
+
if (typeof args[0] === 'string') {
|
|
257
|
+
const parts = args[0].split(':').map(Number);
|
|
258
|
+
if (parts.length === 3)
|
|
259
|
+
[start, step, end] = parts;
|
|
260
|
+
else
|
|
261
|
+
[start, end] = parts;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
start = toNumber(args[0]);
|
|
265
|
+
end = toNumber(args[1]);
|
|
266
|
+
if (args.length > 2 && typeof args[2] !== 'boolean')
|
|
267
|
+
step = toNumber(args[2]);
|
|
268
|
+
}
|
|
269
|
+
return Math.floor(Math.abs((end - start) / step)) + 1;
|
|
270
|
+
}
|
|
271
|
+
/** Shape a matrix grows to when an index reaches past its current size. */
|
|
272
|
+
function grownShape(current, maxIndex) {
|
|
273
|
+
const n = Math.max(current.length, maxIndex.length);
|
|
274
|
+
const out = [];
|
|
275
|
+
for (let i = 0; i < n; i++) {
|
|
276
|
+
out.push(Math.max(current[i] ?? 1, (maxIndex[i] ?? 0) + 1));
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
/** Characters per decimal digit of magnitude for each positional notation. */
|
|
281
|
+
const DIGITS_PER_DECIMAL_DIGIT = {
|
|
282
|
+
fixed: 1,
|
|
283
|
+
bin: Math.log2(10),
|
|
284
|
+
oct: Math.log2(10) / 3,
|
|
285
|
+
hex: Math.log2(10) / 4,
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Largest decimal exponent of any BigNumber in a value (walking matrices, arrays,
|
|
289
|
+
* units, and object values). Only BigNumbers matter: a `number` is at most 309
|
|
290
|
+
* digits, but a BigNumber exponent reaches 9e15, and positional notation writes
|
|
291
|
+
* every digit.
|
|
292
|
+
*/
|
|
293
|
+
function maxBigNumberExponent(value) {
|
|
294
|
+
if (isBigNumber(value))
|
|
295
|
+
return Math.abs(value.e);
|
|
296
|
+
if (isUnit(value))
|
|
297
|
+
return maxBigNumberExponent(value.value);
|
|
298
|
+
if (isMatrix(value))
|
|
299
|
+
return maxBigNumberExponent(value.valueOf());
|
|
300
|
+
if (Array.isArray(value))
|
|
301
|
+
return value.reduce((m, v) => Math.max(m, maxBigNumberExponent(v)), 0);
|
|
302
|
+
if (isPlainObject(value))
|
|
303
|
+
return maxBigNumberExponent(Object.values(value));
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Reject formatting options that would build an oversized string: a precision
|
|
308
|
+
* past the limit, or positional notation (`fixed`, `bin`, `oct`, `hex`) of a
|
|
309
|
+
* BigNumber whose exponent alone spells out more digits than the limit.
|
|
310
|
+
*/
|
|
311
|
+
function assertFormatSize(fn, value, options) {
|
|
312
|
+
const isOptionsObject = options !== null && typeof options === 'object' && !isBigNumber(options) && !isMatrix(options);
|
|
313
|
+
const precision = isOptionsObject
|
|
314
|
+
? toNumber(options.precision)
|
|
315
|
+
: toNumber(options);
|
|
316
|
+
const notation = isOptionsObject ? options.notation : undefined;
|
|
317
|
+
const factor = typeof notation === 'string' ? DIGITS_PER_DECIMAL_DIGIT[notation] : undefined;
|
|
318
|
+
const digits = factor === undefined ? 0 : maxBigNumberExponent(value) * factor;
|
|
319
|
+
const length = digits + (precision > 0 ? precision : 0);
|
|
320
|
+
if (length > 0)
|
|
321
|
+
assertCharacters(fn, length);
|
|
322
|
+
}
|
|
323
|
+
/** Per-function checks for functions whose output size is an argument. */
|
|
324
|
+
const SIZE_GUARDS = {
|
|
325
|
+
zeros: (args) => assertElements('zeros', countOf(sizeEntries(args))),
|
|
326
|
+
ones: (args) => assertElements('ones', countOf(sizeEntries(args))),
|
|
327
|
+
identity: (args) => {
|
|
328
|
+
const dims = sizeEntries(args);
|
|
329
|
+
// identity(n) and identity([n]) are square.
|
|
330
|
+
assertElements('identity', dims.length === 1 ? dims[0] ** 2 : countOf(dims));
|
|
331
|
+
},
|
|
332
|
+
range: (args) => assertElements('range', rangeCount(args)),
|
|
333
|
+
random: (args) => assertElements('random', sizeProduct(args[0])),
|
|
334
|
+
randomInt: (args) => assertElements('randomInt', sizeProduct(args[0])),
|
|
335
|
+
matrixFromFunction: (args) => assertElements('matrixFromFunction', sizeProduct(args[0])),
|
|
336
|
+
resize: (args) => assertElements('resize', sizeProduct(args[1])),
|
|
337
|
+
pickRandom: (args) => {
|
|
338
|
+
for (const arg of args.slice(1)) {
|
|
339
|
+
if (shapeOf(arg))
|
|
340
|
+
continue; // weights
|
|
341
|
+
const count = arg !== null && typeof arg === 'object' && 'number' in arg
|
|
342
|
+
? toNumber(arg.number)
|
|
343
|
+
: toNumber(arg);
|
|
344
|
+
assertElements('pickRandom', count);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
nthRoots: (args) => assertElements('nthRoots', args.length > 1 ? toNumber(args[1]) : 2),
|
|
348
|
+
freqz: (args) => {
|
|
349
|
+
// A scalar third argument is the number of frequency points; the result holds two arrays of it.
|
|
350
|
+
if (args.length > 2 && !shapeOf(args[2]))
|
|
351
|
+
assertElements('freqz', 2 * toNumber(args[2]));
|
|
352
|
+
},
|
|
353
|
+
quantileSeq: (args) => {
|
|
354
|
+
// A scalar second argument above 1 is a count of evenly spaced quantiles.
|
|
355
|
+
if (args.length > 1 && !shapeOf(args[1]) && toNumber(args[1]) > 1) {
|
|
356
|
+
assertElements('quantileSeq', toNumber(args[1]));
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
diag: (args) => {
|
|
360
|
+
const shape = shapeOf(args[0]);
|
|
361
|
+
if (shape?.length !== 1)
|
|
362
|
+
return; // a matrix argument yields its diagonal vector
|
|
363
|
+
const offset = args.slice(1).find((a) => typeof a !== 'string');
|
|
364
|
+
const k = offset === undefined ? 0 : Math.abs(toNumber(offset));
|
|
365
|
+
assertElements('diag', (shape[0] + k) ** 2);
|
|
366
|
+
},
|
|
367
|
+
kron: (args) => assertElements('kron', countOf(shapeOf(args[0]) ?? []) * countOf(shapeOf(args[1]) ?? [])),
|
|
368
|
+
setCartesian: (args) => assertElements('setCartesian', 2 * countOf(shapeOf(args[0]) ?? []) * countOf(shapeOf(args[1]) ?? [])),
|
|
369
|
+
setPowerset: (args) => assertElements('setPowerset', 2 ** countOf(shapeOf(args[0]) ?? [])),
|
|
370
|
+
multiply: (args) => assertFoldedShape('multiply', args, productShape),
|
|
371
|
+
// Joining functions build the sum of their inputs, and one input may repeat (`concat(A, A, A)`).
|
|
372
|
+
concat: (args) => assertElements('concat', joinedSize(args)),
|
|
373
|
+
matrixFromRows: (args) => assertElements('matrixFromRows', joinedSize(args)),
|
|
374
|
+
matrixFromColumns: (args) => assertElements('matrixFromColumns', joinedSize(args)),
|
|
375
|
+
format: (args) => assertFormatSize('format', args[0], args[1]),
|
|
376
|
+
print: (args) => assertFormatSize('print', args[1], args[2]),
|
|
377
|
+
bin: (args) => assertFormatSize('bin', args[0], { notation: 'bin' }),
|
|
378
|
+
oct: (args) => assertFormatSize('oct', args[0], { notation: 'oct' }),
|
|
379
|
+
hex: (args) => assertFormatSize('hex', args[0], { notation: 'hex' }),
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* Elementwise functions that broadcast their operands (`[1;2;3] + [1,2,3]` is
|
|
383
|
+
* 3×3), so two vectors of n elements produce n² — guarded on the broadcast shape.
|
|
384
|
+
*/
|
|
385
|
+
const BROADCASTING_FUNCTIONS = [
|
|
386
|
+
'add',
|
|
387
|
+
'subtract',
|
|
388
|
+
'dotMultiply',
|
|
389
|
+
'dotDivide',
|
|
390
|
+
'dotPow',
|
|
391
|
+
'mod',
|
|
392
|
+
'equal',
|
|
393
|
+
'unequal',
|
|
394
|
+
'larger',
|
|
395
|
+
'largerEq',
|
|
396
|
+
'smaller',
|
|
397
|
+
'smallerEq',
|
|
398
|
+
'compare',
|
|
399
|
+
'compareText',
|
|
400
|
+
'atan2',
|
|
401
|
+
'gcd',
|
|
402
|
+
'lcm',
|
|
403
|
+
'nthRoot',
|
|
404
|
+
'bitXor',
|
|
405
|
+
'xor',
|
|
406
|
+
'leftShift',
|
|
407
|
+
'rightArithShift',
|
|
408
|
+
'rightLogShift',
|
|
409
|
+
'to',
|
|
410
|
+
];
|
|
411
|
+
/**
|
|
412
|
+
* Broadcasting functions whose expression form is a lazy (raw-argument)
|
|
413
|
+
* transform: the transform evaluates its operands itself and calls an internal
|
|
414
|
+
* copy of the function, so the guard wraps the transform. Each entry counts the
|
|
415
|
+
* leading arguments that broadcast — both operands of `and`, `or`, `&`, `|`, and
|
|
416
|
+
* `??`, and every collection before the callback of `map(A, B, …, callback)`.
|
|
417
|
+
*/
|
|
418
|
+
const LAZY_BROADCASTING_TRANSFORMS = {
|
|
419
|
+
and: () => 2,
|
|
420
|
+
or: () => 2,
|
|
421
|
+
bitAnd: () => 2,
|
|
422
|
+
bitOr: () => 2,
|
|
423
|
+
nullish: () => 2,
|
|
424
|
+
map: (args) => args.length - 1,
|
|
425
|
+
};
|
|
426
|
+
/**
|
|
427
|
+
* Signatures that can take a collection. Broadcast and product guards wrap only
|
|
428
|
+
* these, so scalar dispatch — the hot path inside math.js's own loops — is
|
|
429
|
+
* untouched.
|
|
430
|
+
*/
|
|
431
|
+
const COLLECTION_SIGNATURE = /Array|Matrix|any/;
|
|
432
|
+
const EVERY_POSITION = () => true;
|
|
433
|
+
const SIZES = 'sizes and counts';
|
|
434
|
+
/**
|
|
435
|
+
* The dimension of a reduction, `fn(A, dim)` — second, after the collection, in
|
|
436
|
+
* both the typed function and its expression transform (which moves it to
|
|
437
|
+
* zero-based once it is a number). `std` and `variance` keep it second when a
|
|
438
|
+
* normalization string follows.
|
|
439
|
+
*/
|
|
440
|
+
const DIMENSION = {
|
|
441
|
+
noun: 'dimensions',
|
|
442
|
+
scalar: (i) => i === 1,
|
|
443
|
+
afterCollection: true,
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Whole-number arguments per function. The size functions are guarded on every
|
|
447
|
+
* instance; the index and dimension functions are rebuilt on a Fraction instance
|
|
448
|
+
* only (see {@link FRACTION_WHOLE_NUMBER_FUNCTIONS}).
|
|
449
|
+
*/
|
|
450
|
+
const WHOLE_NUMBER_ARGS = {
|
|
451
|
+
zeros: { noun: SIZES, scalar: EVERY_POSITION, vector: EVERY_POSITION },
|
|
452
|
+
ones: { noun: SIZES, scalar: EVERY_POSITION, vector: EVERY_POSITION },
|
|
453
|
+
identity: { noun: SIZES, scalar: EVERY_POSITION, vector: EVERY_POSITION },
|
|
454
|
+
resize: { noun: SIZES, vector: (i) => i === 1 },
|
|
455
|
+
random: { noun: SIZES, vector: (i) => i === 0 },
|
|
456
|
+
randomInt: { noun: SIZES, scalar: EVERY_POSITION, vector: (i) => i === 0 },
|
|
457
|
+
matrixFromFunction: { noun: SIZES, vector: (i) => i === 0 },
|
|
458
|
+
diag: { noun: SIZES, scalar: (i) => i === 1 },
|
|
459
|
+
nthRoots: { noun: SIZES, scalar: (i) => i === 1 },
|
|
460
|
+
pickRandom: { noun: SIZES, scalar: (i) => i > 0 },
|
|
461
|
+
// `index` covers bracket indexing (`A[2]`, `A[2, 1] = v`) and explicit `index(2)`.
|
|
462
|
+
index: { noun: 'indexes', scalar: EVERY_POSITION, vector: EVERY_POSITION },
|
|
463
|
+
row: { noun: 'indexes', scalar: (i) => i === 1 },
|
|
464
|
+
column: { noun: 'indexes', scalar: (i) => i === 1 },
|
|
465
|
+
sum: DIMENSION,
|
|
466
|
+
max: DIMENSION,
|
|
467
|
+
min: DIMENSION,
|
|
468
|
+
mean: DIMENSION,
|
|
469
|
+
median: DIMENSION,
|
|
470
|
+
prod: DIMENSION,
|
|
471
|
+
std: DIMENSION,
|
|
472
|
+
variance: DIMENSION,
|
|
473
|
+
cumsum: DIMENSION,
|
|
474
|
+
// concat's dimension is its last argument, after at least one matrix; every other
|
|
475
|
+
// argument is an Array or Matrix, so a scalar Fraction past the first is the
|
|
476
|
+
// dimension. The typed function takes it inside a rest parameter
|
|
477
|
+
// (`...Array|Matrix|number|BigNumber`), which cannot gain a trailing-Fraction
|
|
478
|
+
// counterpart, so only the expression transform — the path expressions call —
|
|
479
|
+
// converts it; `i > 0` keeps position 0 of that rest signature unchanged.
|
|
480
|
+
concat: { noun: 'dimensions', scalar: (i) => i > 0 },
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* Functions that take an index or a dimension, rebuilt on a Fraction instance
|
|
484
|
+
* only; their arguments are converted but not size-checked. `concat` is missing
|
|
485
|
+
* because it is size-guarded on every instance, which converts it too.
|
|
486
|
+
*/
|
|
487
|
+
const FRACTION_WHOLE_NUMBER_FUNCTIONS = [
|
|
488
|
+
'index',
|
|
489
|
+
'row',
|
|
490
|
+
'column',
|
|
491
|
+
'sum',
|
|
492
|
+
'max',
|
|
493
|
+
'min',
|
|
494
|
+
'mean',
|
|
495
|
+
'median',
|
|
496
|
+
'prod',
|
|
497
|
+
'std',
|
|
498
|
+
'variance',
|
|
499
|
+
'cumsum',
|
|
500
|
+
];
|
|
501
|
+
/** A whole-number Fraction as a number; a non-integer one is rejected with a clear message. */
|
|
502
|
+
function wholeNumber(fn, noun, value) {
|
|
503
|
+
if (!isFraction(value))
|
|
504
|
+
return value;
|
|
505
|
+
const fraction = value;
|
|
506
|
+
if (Number(fraction.d) !== 1) {
|
|
507
|
+
throw new Error(`${fn}() needs whole-number ${noun}; got ${fraction.toFraction()}.`);
|
|
508
|
+
}
|
|
509
|
+
return Number(value.valueOf());
|
|
510
|
+
}
|
|
511
|
+
/** Convert the whole-number Fractions in a (spread-out) argument list to numbers. */
|
|
512
|
+
function toWholeNumberArgs(fn, spec, args) {
|
|
513
|
+
if (spec.afterCollection && !Array.isArray(args[0]) && !isMatrix(args[0]))
|
|
514
|
+
return args;
|
|
515
|
+
const convert = (value) => wholeNumber(fn, spec.noun, value);
|
|
516
|
+
return args.map((arg, i) => {
|
|
517
|
+
if (spec.scalar?.(i) && isFraction(arg))
|
|
518
|
+
return convert(arg);
|
|
519
|
+
if (!spec.vector?.(i))
|
|
520
|
+
return arg;
|
|
521
|
+
if (Array.isArray(arg))
|
|
522
|
+
return arg.map(convert);
|
|
523
|
+
if (isMatrix(arg)) {
|
|
524
|
+
return arg.map((entry) => convert(entry));
|
|
525
|
+
}
|
|
526
|
+
return arg;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* The Fraction counterparts of a signature: each scalar size/count parameter that
|
|
531
|
+
* accepts `number` accepts `Fraction` instead (`number,string` → `Fraction,string`).
|
|
532
|
+
* A rest parameter must start with a Fraction and keeps its other types after it
|
|
533
|
+
* (`...number|string` → `Fraction` and `Fraction,...Fraction|string`), so no
|
|
534
|
+
* counterpart matches the same arguments as the original. Empty when nothing changes.
|
|
535
|
+
*/
|
|
536
|
+
function fractionSignatures(params, spec) {
|
|
537
|
+
let changed = false;
|
|
538
|
+
let restTail;
|
|
539
|
+
const fixed = params.map((param, i) => {
|
|
540
|
+
const rest = param.startsWith('...');
|
|
541
|
+
const types = (rest ? param.slice(3) : param).split('|');
|
|
542
|
+
if (!spec.scalar?.(i) || !types.includes('number'))
|
|
543
|
+
return param;
|
|
544
|
+
changed = true;
|
|
545
|
+
if (rest)
|
|
546
|
+
restTail = `...${types.map((type) => (type === 'number' ? 'Fraction' : type)).join('|')}`;
|
|
547
|
+
return 'Fraction';
|
|
548
|
+
});
|
|
549
|
+
if (!changed)
|
|
550
|
+
return [];
|
|
551
|
+
const single = fixed.join(',');
|
|
552
|
+
return restTail === undefined ? [single] : [single, `${single},${restTail}`];
|
|
553
|
+
}
|
|
554
|
+
/** Spread a rest parameter's array (at `restAt`) back into the argument list. */
|
|
555
|
+
function spreadRest(args, restAt) {
|
|
556
|
+
return restAt < 0 ? args : [...args.slice(0, restAt), ...args[restAt]];
|
|
557
|
+
}
|
|
558
|
+
/** Position of a signature's rest parameter, or -1. */
|
|
559
|
+
function restPosition(signature) {
|
|
560
|
+
return signature.split(',').findIndex((param) => param.startsWith('...'));
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Wrap a function with `guard`. A typed function is rebuilt from its signatures
|
|
564
|
+
* (guarding those `selectSignature` picks) so it stays a typed function for
|
|
565
|
+
* math.js's callback and dispatch machinery; a rest parameter (`...number`) is
|
|
566
|
+
* spread out for the guard. A plain function gets a plain wrapper. When the
|
|
567
|
+
* function has whole-number parameters, Fraction values there are converted to
|
|
568
|
+
* numbers first, and each signature gains a Fraction counterpart. Without a
|
|
569
|
+
* `guard`, the wrapper only converts.
|
|
570
|
+
*/
|
|
571
|
+
function guardFunction(typed, name, fn, guard, selectSignature) {
|
|
572
|
+
const wholeArgs = WHOLE_NUMBER_ARGS[name];
|
|
573
|
+
const prepare = (args) => {
|
|
574
|
+
const prepared = wholeArgs ? toWholeNumberArgs(name, wholeArgs, args) : args;
|
|
575
|
+
guard?.(prepared);
|
|
576
|
+
return prepared;
|
|
577
|
+
};
|
|
578
|
+
if (!typed.isTypedFunction(fn) || !fn.signatures) {
|
|
579
|
+
return (...args) => fn(...prepare(args));
|
|
580
|
+
}
|
|
581
|
+
const signatures = {};
|
|
582
|
+
const variants = {};
|
|
583
|
+
for (const [signature, impl] of Object.entries(fn.signatures)) {
|
|
584
|
+
if (!selectSignature(signature)) {
|
|
585
|
+
signatures[signature] = impl;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const restAt = restPosition(signature);
|
|
589
|
+
const gather = (flat) => restAt < 0 ? flat : [...flat.slice(0, restAt), flat.slice(restAt)];
|
|
590
|
+
// `inputRestAt` is where the calling signature's rest parameter sits; the
|
|
591
|
+
// implementation always receives the arguments shaped for `signature`.
|
|
592
|
+
const guardedFrom = (inputRestAt) => (...args) => impl(...gather(prepare(spreadRest(args, inputRestAt))));
|
|
593
|
+
signatures[signature] = guardedFrom(restAt);
|
|
594
|
+
for (const variant of wholeArgs ? fractionSignatures(signature.split(','), wholeArgs) : []) {
|
|
595
|
+
if (!(variant in fn.signatures))
|
|
596
|
+
variants[variant] ??= guardedFrom(restPosition(variant));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return typed(name, { ...signatures, ...variants });
|
|
600
|
+
}
|
|
601
|
+
/** A factory that builds `original`'s function and returns it wrapped by `wrap`. */
|
|
602
|
+
function wrappedFactory(original, wrap) {
|
|
603
|
+
return factory(original.fn, original.dependencies, (deps) => wrap(original(deps)), original.meta);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Wrap a lazy transform. It is handed shims for its broadcasting operands that
|
|
607
|
+
* record each value and, once the last is evaluated, check the broadcast shape
|
|
608
|
+
* before the transform combines them — the transform keeps its evaluation and
|
|
609
|
+
* short-circuit order, and the remaining arguments (a callback) pass unchanged.
|
|
610
|
+
*/
|
|
611
|
+
function guardLazyTransform(name, original, operandCount) {
|
|
612
|
+
const guarded = (args, math, scope) => {
|
|
613
|
+
const count = operandCount(args);
|
|
614
|
+
if (count < 2 || args.length < count)
|
|
615
|
+
return original(args, math, scope);
|
|
616
|
+
const values = [];
|
|
617
|
+
const shims = args.slice(0, count).map((operand, i) => ({
|
|
618
|
+
compile: () => ({
|
|
619
|
+
evaluate: (s) => {
|
|
620
|
+
values[i] = operand.compile().evaluate(s);
|
|
621
|
+
if (i === count - 1)
|
|
622
|
+
assertFoldedShape(name, values, broadcastShape);
|
|
623
|
+
return values[i];
|
|
624
|
+
},
|
|
625
|
+
}),
|
|
626
|
+
}));
|
|
627
|
+
return original([...shims, ...args.slice(count)], math, scope);
|
|
628
|
+
};
|
|
629
|
+
guarded.rawArgs = true;
|
|
630
|
+
return guarded;
|
|
631
|
+
}
|
|
632
|
+
/** Check a matrix growing from `current` to `grown` elements, charging only the added ones. */
|
|
633
|
+
function assertGrowth(fn, current, grown) {
|
|
634
|
+
assertElements(fn, grown, Math.max(0, grown - current));
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Guard the matrix methods that read or grow a matrix by index — `resize(size)`,
|
|
638
|
+
* `subset(index)` (indexed reads, `A[i, j]`, whose index vectors multiply:
|
|
639
|
+
* `A[ones(1e4), ones(1e4)]` reads 1e8 elements), `subset(index, replacement)`
|
|
640
|
+
* (indexed assignment, `A[i, j] = v`, which grows the matrix to fit), and
|
|
641
|
+
* `set(index, value)` — on one matrix class. Classes are per math.js instance,
|
|
642
|
+
* so each is wrapped once.
|
|
643
|
+
*/
|
|
644
|
+
function guardMatrixMethods(matrixClass, className) {
|
|
645
|
+
const proto = matrixClass.prototype;
|
|
646
|
+
const { resize, subset, set } = proto;
|
|
647
|
+
proto.resize = function (...args) {
|
|
648
|
+
assertGrowth(`${className}.resize`, countOf(this.size()), sizeProduct(args[0]));
|
|
649
|
+
return resize.apply(this, args);
|
|
650
|
+
};
|
|
651
|
+
proto.subset = function (...args) {
|
|
652
|
+
const index = args[0];
|
|
653
|
+
if (args.length > 1 && typeof index?.max === 'function') {
|
|
654
|
+
const current = this.size();
|
|
655
|
+
const grown = grownShape(current, index.max().map(toNumber));
|
|
656
|
+
assertGrowth('subset', countOf(current), countOf(grown));
|
|
657
|
+
}
|
|
658
|
+
else if (typeof index?.size === 'function') {
|
|
659
|
+
// The node returning the read value charges it.
|
|
660
|
+
assertElements('subset', countOf(index.size()), 0);
|
|
661
|
+
}
|
|
662
|
+
return subset.apply(this, args);
|
|
663
|
+
};
|
|
664
|
+
proto.set = function (...args) {
|
|
665
|
+
if (Array.isArray(args[0])) {
|
|
666
|
+
const size = this.size();
|
|
667
|
+
const index = args[0].map(toNumber);
|
|
668
|
+
if (index.some((i, d) => i >= (size[d] ?? 1))) {
|
|
669
|
+
assertGrowth(`${className}.set`, countOf(size), countOf(grownShape(size, index)));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return set.apply(this, args);
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
/** math.js factory names that are not the function name capitalized (`createCumSum`). */
|
|
676
|
+
const FACTORY_BASE_NAMES = { cumsum: 'CumSum' };
|
|
677
|
+
/**
|
|
678
|
+
* Install the size guards on a math.js instance. Must run before the instance's
|
|
679
|
+
* `import` is disabled; `mathImport` is the captured original and `factories` is
|
|
680
|
+
* the factory map the instance was created from. Functions are re-imported as
|
|
681
|
+
* lazy factories, so nothing is instantiated until an expression first uses it.
|
|
682
|
+
* On a Fraction instance, the index and dimension functions are rebuilt too, to
|
|
683
|
+
* accept whole-number Fraction indexes and dimensions.
|
|
684
|
+
*/
|
|
685
|
+
export function installSizeGuards(math, mathImport, factories) {
|
|
686
|
+
const typed = math.typed;
|
|
687
|
+
const transformNamespace = math.expression
|
|
688
|
+
.transform;
|
|
689
|
+
const factoryFor = (name, suffix = '') => {
|
|
690
|
+
const base = FACTORY_BASE_NAMES[name] ?? `${name[0]?.toUpperCase()}${name.slice(1)}`;
|
|
691
|
+
const found = factories[`create${base}${suffix}`];
|
|
692
|
+
if (!found)
|
|
693
|
+
throw new Error(`No math.js factory create${base}${suffix} for "${name}".`);
|
|
694
|
+
return found;
|
|
695
|
+
};
|
|
696
|
+
const functionFactories = [];
|
|
697
|
+
const transformFactories = [];
|
|
698
|
+
const addGuard = (name, guard, select) => {
|
|
699
|
+
const wrap = (fn) => guardFunction(typed, name, fn, guard, select);
|
|
700
|
+
functionFactories.push(wrappedFactory(factoryFor(name), wrap));
|
|
701
|
+
// Overriding a function drops its expression transform; re-install it guarded
|
|
702
|
+
// too. Clearing the transform first keeps the import lazy — importing over an
|
|
703
|
+
// existing transform makes math.js resolve the new function immediately.
|
|
704
|
+
if (name in transformNamespace) {
|
|
705
|
+
delete transformNamespace[name];
|
|
706
|
+
transformFactories.push(wrappedFactory(factoryFor(name, 'Transform'), wrap));
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
const selectAll = () => true;
|
|
710
|
+
const selectCollections = (signature) => COLLECTION_SIGNATURE.test(signature);
|
|
711
|
+
for (const [name, guard] of Object.entries(SIZE_GUARDS)) {
|
|
712
|
+
addGuard(name, guard, name === 'multiply' ? selectCollections : selectAll);
|
|
713
|
+
}
|
|
714
|
+
for (const name of BROADCASTING_FUNCTIONS) {
|
|
715
|
+
addGuard(name, (args) => assertFoldedShape(name, args, broadcastShape), selectCollections);
|
|
716
|
+
}
|
|
717
|
+
// An empty options object reads the config without changing it.
|
|
718
|
+
if (math.config({}).number === 'Fraction') {
|
|
719
|
+
for (const name of FRACTION_WHOLE_NUMBER_FUNCTIONS)
|
|
720
|
+
addGuard(name, undefined, selectAll);
|
|
721
|
+
}
|
|
722
|
+
for (const [name, operandCount] of Object.entries(LAZY_BROADCASTING_TRANSFORMS)) {
|
|
723
|
+
// Importing over an existing transform deletes it rather than replacing it, so clear it first.
|
|
724
|
+
delete transformNamespace[name];
|
|
725
|
+
transformFactories.push(wrappedFactory(factoryFor(name, 'Transform'), (fn) => guardLazyTransform(name, fn, operandCount)));
|
|
726
|
+
}
|
|
727
|
+
mathImport(functionFactories, { override: true });
|
|
728
|
+
mathImport(transformFactories, { override: true });
|
|
729
|
+
const classes = math;
|
|
730
|
+
guardMatrixMethods(classes.DenseMatrix, 'DenseMatrix');
|
|
731
|
+
guardMatrixMethods(classes.SparseMatrix, 'SparseMatrix');
|
|
732
|
+
}
|
|
733
|
+
//# sourceMappingURL=size-guard.js.map
|