@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.
@@ -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