exprify 1.0.3 → 1.0.6

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.
@@ -1,55 +1,67 @@
1
- import { unwrapDenseMatrix, wrapDenseMatrix } from "../utils/matrix.js";
2
-
1
+ import { unwrapDenseMatrix, wrapDenseMatrix, isDenseMatrixWrapper } from '../utils/matrix.js';
2
+ import {
3
+ isFraction,
4
+ fraction as createFrac,
5
+ addFrac,
6
+ subFrac,
7
+ mulFrac,
8
+ divFrac,
9
+ powFrac,
10
+ } from '../math/fraction.js';
11
+ import { isBigNumber, bigNumber as createBN } from '../math/bignumber.js';
12
+
13
+ /** @param {any } node*/
3
14
  export function evaluateAST(node, context = {}) {
4
-
5
15
  const vars = context.variables;
6
16
  const fns = context.functions;
7
17
  const units = context.units;
8
18
 
9
-
10
- const isUnitObj = (v) =>
11
- v && typeof v === "object" && "value" in v && "unit" in v;
12
-
13
- const isComplex = (v) =>
14
- v && typeof v === "object" && "re" in v && "im" in v;
15
-
16
- const isSliceNode = (v) =>
17
- v && typeof v === "object" && v.type === "SliceExpression";
18
-
19
- const isMatrix = (v) =>
19
+ const isUnitObj = (/** @type {any} */ v) =>
20
+ v && typeof v === 'object' && 'value' in v && 'unit' in v;
21
+ const isComplex = (/** @type {any} */ v) => v && typeof v === 'object' && 're' in v && 'im' in v;
22
+ const isSliceNode = (/** @type {any} */ v) =>
23
+ v && typeof v === 'object' && v.type === 'SliceExpression';
24
+ const isMatrix = (/** @type {any[]} */ v) =>
20
25
  Array.isArray(v) && v.length > 0 && v.every(Array.isArray);
26
+ const isMatrixLike = (/** @type {any} */ v) => isMatrix(v) || isDenseMatrixWrapper(v);
21
27
 
22
- const cloneValue = (value) => {
23
- if (Array.isArray(value)) {
24
- return value.map(cloneValue);
28
+ const normalizeMatrix = (/** @type {any[]} */ value) => {
29
+ value = unwrapDenseMatrix(value);
30
+ if (isMatrix(value)) {
31
+ return value.map((/** @type {any} */ row) => [...row]);
25
32
  }
26
-
27
- if (value && typeof value === "object") {
28
- return { ...value };
33
+ if (Array.isArray(value)) {
34
+ return [value];
29
35
  }
30
-
31
- return value;
36
+ throw new Error('Expected matrix-compatible value');
32
37
  };
33
38
 
34
- const normalizeMatrix = (value) => {
35
- value = unwrapDenseMatrix(value);
36
- if (isMatrix(value)) return value.map((row) => [...row]);
37
- if (Array.isArray(value)) return [value];
38
- throw new Error("Expected matrix-compatible value");
39
+ const nodeError = (/** @type {string} */ msg) => {
40
+ const pos = node && node.pos !== undefined ? ` at position ${node.pos}` : '';
41
+ return new Error(`${msg}${pos}`);
39
42
  };
40
43
 
41
- const toOneBasedIndex = (value) => {
42
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
43
- throw new Error("Matrix indices must be positive integers");
44
+ const toOneBasedIndex = (/** @type {unknown} */ value) => {
45
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
46
+ throw new Error('Matrix indices must be positive integers');
44
47
  }
45
48
 
46
49
  return value - 1;
47
50
  };
48
51
 
49
- const resolveSelector = (selector, contextLength) => {
52
+ const resolveSelector = (
53
+ /** @type {{ start: null | undefined; end: null | undefined; }} */ selector,
54
+ /** @type {number} */ contextLength
55
+ ) => {
50
56
  if (isSliceNode(selector)) {
51
- const startValue = selector.start == null ? 1 : evaluateAST(selector.start, context);
52
- const endValue = selector.end == null ? contextLength : evaluateAST(selector.end, context);
57
+ const startValue =
58
+ selector.start === null || selector.start === undefined
59
+ ? 1
60
+ : evaluateAST(selector.start, context);
61
+ const endValue =
62
+ selector.end === null || selector.end === undefined
63
+ ? contextLength
64
+ : evaluateAST(selector.end, context);
53
65
  const start = toOneBasedIndex(startValue);
54
66
  const end = toOneBasedIndex(endValue);
55
67
 
@@ -67,14 +79,14 @@ export function evaluateAST(node, context = {}) {
67
79
  return [toOneBasedIndex(evaluateAST(selector, context))];
68
80
  };
69
81
 
70
- const indexMatrix = (matrix, selectors) => {
82
+ const indexMatrix = (/** @type {any} */ matrix, /** @type {string | any[]} */ selectors) => {
71
83
  const target = normalizeMatrix(matrix);
72
84
 
73
85
  if (selectors.length === 1) {
74
86
  const rowIndexes = resolveSelector(selectors[0], target.length);
75
- const rows = rowIndexes.map((rowIndex) => {
87
+ const rows = rowIndexes.map((/** @type {number} */ rowIndex) => {
76
88
  if (rowIndex >= target.length) {
77
- throw new Error("Row index out of range");
89
+ throw new Error('Row index out of range');
78
90
  }
79
91
  return [...target[rowIndex]];
80
92
  });
@@ -85,14 +97,14 @@ export function evaluateAST(node, context = {}) {
85
97
  const rowIndexes = resolveSelector(selectors[0], target.length);
86
98
  const colIndexes = resolveSelector(selectors[1], target[0]?.length || 0);
87
99
 
88
- const values = rowIndexes.map((rowIndex) => {
100
+ const values = rowIndexes.map((/** @type {number} */ rowIndex) => {
89
101
  if (rowIndex >= target.length) {
90
- throw new Error("Row index out of range");
102
+ throw new Error('Row index out of range');
91
103
  }
92
104
 
93
- return colIndexes.map((colIndex) => {
105
+ return colIndexes.map((/** @type {number} */ colIndex) => {
94
106
  if (colIndex >= target[rowIndex].length) {
95
- throw new Error("Column index out of range");
107
+ throw new Error('Column index out of range');
96
108
  }
97
109
  return target[rowIndex][colIndex];
98
110
  });
@@ -107,15 +119,19 @@ export function evaluateAST(node, context = {}) {
107
119
  }
108
120
 
109
121
  if (colIndexes.length === 1) {
110
- return values.map((row) => [row[0]]);
122
+ return values.map((/** @type {any[]} */ row) => [row[0]]);
111
123
  }
112
124
 
113
125
  return values;
114
126
  };
115
127
 
116
- const assignMatrixIndex = (matrix, selectors, value) => {
128
+ const assignMatrixIndex = (
129
+ /** @type {any[]} */ matrix,
130
+ /** @type {string | any[]} */ selectors,
131
+ /** @type {any} */ value
132
+ ) => {
117
133
  const target = isMatrix(matrix)
118
- ? matrix.map((row) => [...row])
134
+ ? matrix.map((/** @type {any} */ row) => [...row])
119
135
  : Array.isArray(matrix)
120
136
  ? [matrix.slice()]
121
137
  : [];
@@ -124,7 +140,7 @@ export function evaluateAST(node, context = {}) {
124
140
  const colSelector = selectors[1];
125
141
 
126
142
  if (!rowSelector) {
127
- throw new Error("Matrix assignment requires at least one index");
143
+ throw new Error('Matrix assignment requires at least one index');
128
144
  }
129
145
 
130
146
  const rowContextLength = Math.max(target.length, 1);
@@ -134,111 +150,205 @@ export function evaluateAST(node, context = {}) {
134
150
  const rowsValue = isMatrix(value) ? value : normalizeMatrix(value);
135
151
 
136
152
  if (rowsValue.length !== rowIndexes.length) {
137
- throw new Error("Assigned row count does not match slice");
153
+ throw new Error('Assigned row count does not match slice');
138
154
  }
139
155
 
140
- rowIndexes.forEach((rowIndex, index) => {
141
- target[rowIndex] = [...rowsValue[index]];
142
- });
156
+ rowIndexes.forEach(
157
+ (/** @type {string | number} */ rowIndex, /** @type {string | number} */ index) => {
158
+ target[rowIndex] = [...rowsValue[index]];
159
+ }
160
+ );
143
161
 
144
162
  return {
145
163
  updatedMatrix: target,
146
- selectionResult: rowIndexes.length === 1 ? [target[rowIndexes[0]]] : rowIndexes.map((rowIndex) => [target[rowIndex]])
164
+ selectionResult:
165
+ rowIndexes.length === 1
166
+ ? [target[rowIndexes[0]]]
167
+ : rowIndexes.map((/** @type {string | number} */ rowIndex) => [target[rowIndex]]),
147
168
  };
148
169
  }
149
170
 
150
- const maxCols = Math.max(...target.map((row) => row.length), 0, 1);
171
+ const maxCols = Math.max(
172
+ ...target.map((/** @type {string | any[]} */ row) => row.length),
173
+ 0,
174
+ 1
175
+ );
151
176
  const colIndexes = resolveSelector(colSelector, maxCols);
152
177
  const normalizedValue = normalizeMatrix(value);
153
178
 
154
179
  if (normalizedValue.length !== rowIndexes.length) {
155
- throw new Error("Assigned row count does not match matrix slice");
180
+ throw new Error('Assigned row count does not match matrix slice');
156
181
  }
157
182
 
158
- normalizedValue.forEach((row, rowOffset) => {
183
+ normalizedValue.forEach((/** @type {string | any[]} */ row, /** @type {any} */ _rowOffset) => {
159
184
  if (row.length !== colIndexes.length) {
160
- throw new Error("Assigned column count does not match matrix slice");
185
+ throw new Error('Assigned column count does not match matrix slice');
161
186
  }
162
187
  });
163
188
 
164
- rowIndexes.forEach((rowIndex, rowOffset) => {
165
- if (!target[rowIndex]) {
166
- target[rowIndex] = [];
167
- }
189
+ rowIndexes.forEach(
190
+ (/** @type {string | number} */ rowIndex, /** @type {string | number} */ rowOffset) => {
191
+ if (!target[rowIndex]) {
192
+ target[rowIndex] = [];
193
+ }
168
194
 
169
- colIndexes.forEach((colIndex, colOffset) => {
170
- target[rowIndex][colIndex] = normalizedValue[rowOffset][colOffset];
171
- });
172
- });
195
+ colIndexes.forEach(
196
+ (/** @type {string | number} */ colIndex, /** @type {string | number} */ colOffset) => {
197
+ target[rowIndex][colIndex] = normalizedValue[rowOffset][colOffset];
198
+ }
199
+ );
200
+ }
201
+ );
173
202
 
174
203
  return {
175
204
  updatedMatrix: target,
176
- selectionResult: rowIndexes.length === 1
177
- ? [colIndexes.map((colIndex) => target[rowIndexes[0]][colIndex])]
178
- : rowIndexes.map((rowIndex) => colIndexes.map((colIndex) => target[rowIndex][colIndex]))
205
+ selectionResult:
206
+ rowIndexes.length === 1
207
+ ? [
208
+ colIndexes.map(
209
+ (/** @type {string | number} */ colIndex) => target[rowIndexes[0]][colIndex]
210
+ ),
211
+ ]
212
+ : rowIndexes.map((/** @type {string | number} */ rowIndex) =>
213
+ colIndexes.map(
214
+ (/** @type {string | number} */ colIndex) => target[rowIndex][colIndex]
215
+ )
216
+ ),
179
217
  };
180
218
  };
181
219
 
182
- const multiplyMatrices = (left, right) => {
220
+ const isScalar = (/** @type {any} */ v) => typeof v === 'number' || typeof v === 'bigint';
221
+
222
+ const multiplyMatrices = (/** @type {any} */ left, /** @type {any} */ right) => {
223
+ if (isScalar(left)) {
224
+ const b = normalizeMatrix(right);
225
+ return b.map((/** @type {any[]} */ row) =>
226
+ row.map((/** @type {number} */ v) => Number(left) * v)
227
+ );
228
+ }
229
+ if (isScalar(right)) {
230
+ const a = normalizeMatrix(left);
231
+ return a.map((/** @type {any[]} */ row) =>
232
+ row.map((/** @type {number} */ v) => v * Number(right))
233
+ );
234
+ }
183
235
  const a = normalizeMatrix(left);
184
236
  const b = normalizeMatrix(right);
185
237
 
186
238
  if (a[0].length !== b.length) {
187
- throw new Error("Matrix dimensions do not allow multiplication");
239
+ throw new Error('Matrix dimensions do not allow multiplication');
188
240
  }
189
241
 
190
- return a.map((row) =>
191
- b[0].map((_, colIndex) =>
192
- row.reduce((sum, value, rowIndex) => sum + (value * b[rowIndex][colIndex]), 0)
242
+ return a.map((/** @type {any[]} */ row) =>
243
+ b[0].map((/** @type {any} */ _, /** @type {string | number} */ colIndex) =>
244
+ row.reduce(
245
+ (
246
+ /** @type {number} */ sum,
247
+ /** @type {number} */ value,
248
+ /** @type {string | number} */ rowIndex
249
+ ) => sum + value * b[rowIndex][colIndex],
250
+ 0
251
+ )
193
252
  )
194
253
  );
195
254
  };
196
255
 
197
- const toComplex = (value) => {
198
- if (isComplex(value)) return value;
199
- if (typeof value === "number") return { re: value, im: 0 };
200
- throw new Error("Complex arithmetic only supports numbers");
256
+ const addMatrices = (/** @type {any} */ left, /** @type {any} */ right) => {
257
+ const a = normalizeMatrix(left);
258
+ const b = normalizeMatrix(right);
259
+ if (a.length !== b.length || a[0].length !== b[0].length) {
260
+ throw new Error('Matrix dimensions must match for addition');
261
+ }
262
+ return a.map((/** @type {any[]} */ row, /** @type {string | number} */ i) =>
263
+ row.map((/** @type {any} */ v, /** @type {string | number} */ j) => v + b[i][j])
264
+ );
265
+ };
266
+
267
+ const subtractMatrices = (/** @type {any} */ left, /** @type {any} */ right) => {
268
+ const a = normalizeMatrix(left);
269
+ const b = normalizeMatrix(right);
270
+ if (a.length !== b.length || a[0].length !== b[0].length) {
271
+ throw new Error('Matrix dimensions must match for subtraction');
272
+ }
273
+ return a.map((/** @type {any[]} */ row, /** @type {string | number} */ i) =>
274
+ row.map((/** @type {number} */ v, /** @type {string | number} */ j) => v - b[i][j])
275
+ );
276
+ };
277
+
278
+ const identityMatrix = (/** @type {any} */ n) =>
279
+ Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 1 : 0)));
280
+
281
+ const powerMatrix = (/** @type {any} */ left, /** @type {any} */ right) => {
282
+ const a = normalizeMatrix(left);
283
+ if (a.length !== a[0].length) {
284
+ throw new Error('Matrix power requires a square matrix');
285
+ }
286
+ if (!Number.isInteger(right) || right < 0) {
287
+ throw new Error('Matrix power requires a non-negative integer exponent');
288
+ }
289
+ if (right === 0) {
290
+ return identityMatrix(a.length);
291
+ }
292
+ let result = a;
293
+ for (let i = 1; i < right; i++) {
294
+ result = multiplyMatrices(result, a);
295
+ }
296
+ return result;
297
+ };
298
+
299
+ const toComplex = (/** @type {any} */ value) => {
300
+ if (isComplex(value)) {
301
+ return value;
302
+ }
303
+ if (typeof value === 'number') {
304
+ return { re: value, im: 0 };
305
+ }
306
+ throw new Error('Complex arithmetic only supports numbers');
201
307
  };
202
308
 
203
- const fromImaginary = (value) => ({ re: 0, im: value });
309
+ const fromImaginary = (/** @type {any} */ value) => ({ re: 0, im: value });
204
310
 
205
- const simplifyComplex = (value) =>
311
+ const simplifyComplex = (/** @type {{ re: any; im: any; }} */ value) =>
206
312
  value.im === 0 ? value.re : value;
207
313
 
208
- const createFunctionScope = (params, args) => {
314
+ const createFunctionScope = (/** @type {any[]} */ params, /** @type {any[]} */ args) => {
209
315
  const scopedValues = {};
210
316
 
211
- params.forEach((param, index) => {
317
+ params.forEach((/** @type {string | number} */ param, /** @type {string | number} */ index) => {
212
318
  scopedValues[param] = args[index];
213
319
  });
214
320
 
215
321
  return scopedValues;
216
322
  };
217
323
 
218
- const evalComplexBinary = (operator, left, right) => {
324
+ const evalComplexBinary = (
325
+ /** @type {any} */ operator,
326
+ /** @type {any} */ left,
327
+ /** @type {any} */ right
328
+ ) => {
219
329
  const a = toComplex(left);
220
330
  const b = toComplex(right);
221
331
 
222
332
  switch (operator) {
223
- case "+":
333
+ case '+':
224
334
  return simplifyComplex({ re: a.re + b.re, im: a.im + b.im });
225
- case "-":
335
+ case '-':
226
336
  return simplifyComplex({ re: a.re - b.re, im: a.im - b.im });
227
- case "*":
337
+ case '*':
228
338
  return simplifyComplex({
229
- re: (a.re * b.re) - (a.im * b.im),
230
- im: (a.re * b.im) + (a.im * b.re)
339
+ re: a.re * b.re - a.im * b.im,
340
+ im: a.re * b.im + a.im * b.re,
231
341
  });
232
- case "/": {
233
- const denominator = (b.re ** 2) + (b.im ** 2);
342
+ case '/': {
343
+ const denominator = b.re ** 2 + b.im ** 2;
234
344
 
235
345
  if (denominator === 0) {
236
- throw new Error("Division by zero");
346
+ throw new Error('Division by zero');
237
347
  }
238
348
 
239
349
  return simplifyComplex({
240
- re: ((a.re * b.re) + (a.im * b.im)) / denominator,
241
- im: ((a.im * b.re) - (a.re * b.im)) / denominator
350
+ re: (a.re * b.re + a.im * b.im) / denominator,
351
+ im: (a.im * b.re - a.re * b.im) / denominator,
242
352
  });
243
353
  }
244
354
  default:
@@ -246,52 +356,76 @@ export function evaluateAST(node, context = {}) {
246
356
  }
247
357
  };
248
358
 
249
- /* ================= EVALUATOR ================= */
250
-
359
+ // EVALUATOR
251
360
  switch (node.type) {
252
-
253
- /* ===== LITERAL ===== */
254
- case "Literal":
361
+ case 'Literal':
255
362
  return node.value;
256
363
 
257
- case "ImaginaryLiteral":
364
+ case 'ImaginaryLiteral':
258
365
  return fromImaginary(node.value);
259
366
 
260
- case "UnitLiteral":
367
+ case 'UnitLiteral':
261
368
  return { value: node.value, unit: node.unit };
262
369
 
263
- /* ===== VARIABLE ===== */
264
- case "Identifier":
370
+ // VARIABLE
371
+ case 'Identifier':
265
372
  return vars.get(node.name);
266
373
 
267
- /* ===== ASSIGNMENT ===== */
268
- case "AssignmentExpression": {
269
- const value = evaluateAST(node.right, context);
374
+ // Assignment with optional compound operator (+=, -=, *=, /=): read current, apply, write
375
+ case 'AssignmentExpression': {
376
+ let value;
377
+ if (node.operator !== '=') {
378
+ const current = vars.get(node.left.name);
379
+ const right = evaluateAST(node.right, context);
380
+ const op = node.operator.slice(0, -1);
381
+ switch (op) {
382
+ case '+':
383
+ value = current + right;
384
+ break;
385
+ case '-':
386
+ value = current - right;
387
+ break;
388
+ case '*':
389
+ value = current * right;
390
+ break;
391
+ case '/':
392
+ value = current / right;
393
+ break;
394
+ case '%':
395
+ value = current % right;
396
+ break;
397
+ default:
398
+ throw nodeError(`Unknown compound operator ${node.operator}`);
399
+ }
400
+ } else {
401
+ value = evaluateAST(node.right, context);
402
+ }
270
403
 
271
- if (node.left.type === "Identifier") {
404
+ if (node.left.type === 'Identifier') {
272
405
  vars.set(node.left.name, value);
273
- if (node.right.type === "ArrayExpression") {
406
+ if (node.right.type === 'ArrayExpression') {
274
407
  return wrapDenseMatrix(unwrapDenseMatrix(value));
275
408
  }
276
409
  return value;
277
410
  }
278
411
 
279
- if (node.left.type === "IndexExpression" && node.left.object.type === "Identifier") {
412
+ if (node.left.type === 'IndexExpression' && node.left.object.type === 'Identifier') {
280
413
  const currentValue = vars.get(node.left.object.name);
281
414
  const assigned = assignMatrixIndex(currentValue, node.left.selectors, value);
282
415
  vars.set(node.left.object.name, assigned.updatedMatrix);
283
416
  return assigned.selectionResult;
284
417
  }
285
418
 
286
- throw new Error("Invalid assignment target");
419
+ throw nodeError('Invalid assignment target');
287
420
  }
288
421
 
289
- case "FunctionAssignmentExpression": {
290
- if (node.operator !== "=") {
291
- throw new Error(`Operator ${node.operator} is not supported for function definitions`);
422
+ // User-defined function via f(a,b)=expr: closure evaluates body in a new scope with params bound
423
+ case 'FunctionAssignmentExpression': {
424
+ if (node.operator !== '=') {
425
+ throw nodeError(`Operator ${node.operator} is not supported for function definitions`);
292
426
  }
293
427
 
294
- const fn = (...args) => {
428
+ const fn = (/** @type {any} */ ...args) => {
295
429
  const scopedContext = context.withScope(createFunctionScope(node.params, args));
296
430
  return evaluateAST(node.right, scopedContext);
297
431
  };
@@ -300,36 +434,112 @@ export function evaluateAST(node, context = {}) {
300
434
  return fn;
301
435
  }
302
436
 
303
- /* ===== UNARY ===== */
304
- case "UnaryExpression": {
437
+ // UNARY
438
+ case 'UnaryExpression': {
305
439
  const val = evaluateAST(node.argument, context);
306
440
 
307
441
  switch (node.operator) {
308
- case "-":
309
- return isComplex(val)
310
- ? simplifyComplex({ re: -val.re, im: -val.im })
311
- : -val;
312
- case "!": return !val;
442
+ case '-':
443
+ if (isBigNumber(val)) {
444
+ return val.negated();
445
+ }
446
+ if (isComplex(val)) {
447
+ return simplifyComplex({ re: -val.re, im: -val.im });
448
+ }
449
+ return -val;
450
+ case '!':
451
+ return !val;
313
452
  }
314
453
 
315
- throw new Error(`Unknown unary operator ${node.operator}`);
454
+ throw nodeError(`Unknown unary operator ${node.operator}`);
316
455
  }
317
456
 
318
- /* ===== BINARY ===== */
319
- case "BinaryExpression": {
320
- let left = evaluateAST(node.left, context);
321
- let right = evaluateAST(node.right, context);
457
+ // Dispatch order: unit arithmetic -> matrix arithmetic -> complex arithmetic -> scalar arithmetic
458
+ case 'BinaryExpression': {
459
+ const left = evaluateAST(node.left, context);
460
+ const right = evaluateAST(node.right, context);
322
461
 
323
462
  // UNIT handling
324
463
  if (isUnitObj(left) || isUnitObj(right)) {
325
-
326
- if (!units) throw new Error("Unit system not available");
464
+ if (!units) {
465
+ throw new Error('Unit system not available');
466
+ }
327
467
 
328
468
  return units.compute(node.operator, left, right);
329
469
  }
330
470
 
331
- if (node.operator === "*" && (Array.isArray(left) || Array.isArray(right))) {
332
- return multiplyMatrices(left, right);
471
+ if (
472
+ isMatrixLike(left) ||
473
+ isMatrixLike(right) ||
474
+ (node.operator === '*' && (Array.isArray(left) || Array.isArray(right)))
475
+ ) {
476
+ switch (node.operator) {
477
+ case '+':
478
+ return addMatrices(left, right);
479
+ case '-':
480
+ return subtractMatrices(left, right);
481
+ case '*':
482
+ return multiplyMatrices(left, right);
483
+ case '^':
484
+ return powerMatrix(left, right);
485
+ default:
486
+ throw nodeError(`Operator ${node.operator} not supported for matrices`);
487
+ }
488
+ }
489
+
490
+ if (isFraction(left) || isFraction(right)) {
491
+ const a = isFraction(left) ? left : createFrac(left, 1);
492
+ const b = isFraction(right) ? right : createFrac(right, 1);
493
+ switch (node.operator) {
494
+ case '+':
495
+ return addFrac(a, b);
496
+ case '-':
497
+ return subFrac(a, b);
498
+ case '*':
499
+ return mulFrac(a, b);
500
+ case '/':
501
+ return divFrac(a, b);
502
+ case '^': {
503
+ const p = powFrac(a, right);
504
+ if (p) {
505
+ return p;
506
+ }
507
+ throw nodeError('Fraction power requires non-negative integer exponent');
508
+ }
509
+ default:
510
+ throw nodeError(`Operator ${node.operator} not supported for fractions`);
511
+ }
512
+ }
513
+
514
+ if (isBigNumber(left) || isBigNumber(right)) {
515
+ const a = isBigNumber(left) ? left : createBN(left);
516
+ const b = isBigNumber(right) ? right : createBN(right);
517
+ switch (node.operator) {
518
+ case '+':
519
+ return a.plus(b);
520
+ case '-':
521
+ return a.minus(b);
522
+ case '*':
523
+ return a.times(b);
524
+ case '/':
525
+ return a.div(b);
526
+ case '%':
527
+ return a.mod(b);
528
+ case '^':
529
+ return a.pow(b);
530
+ case '>':
531
+ return a.gt(b);
532
+ case '<':
533
+ return a.lt(b);
534
+ case '>=':
535
+ return a.gte(b);
536
+ case '<=':
537
+ return a.lte(b);
538
+ case '==':
539
+ return a.eq(b);
540
+ default:
541
+ throw nodeError(`Operator ${node.operator} not supported for BigNumber`);
542
+ }
333
543
  }
334
544
 
335
545
  if (isComplex(left) || isComplex(right)) {
@@ -337,124 +547,174 @@ export function evaluateAST(node, context = {}) {
337
547
  }
338
548
 
339
549
  switch (node.operator) {
340
- case "+": return left + right;
341
- case "-": return left - right;
342
- case "*": return left * right;
343
- case "/": return left / right;
344
- case "%": return left % right;
345
- case "^": return left ** right;
346
-
347
- case ">": return left > right;
348
- case "<": return left < right;
349
- case ">=": return left >= right;
350
- case "<=": return left <= right;
351
- case "==": return left === right;
550
+ case '+':
551
+ return left + right;
552
+ case '-':
553
+ return left - right;
554
+ case '*':
555
+ return left * right;
556
+ case '/':
557
+ return left / right;
558
+ case '%':
559
+ return left % right;
560
+ case '^':
561
+ return left ** right;
562
+
563
+ case '>':
564
+ return left > right;
565
+ case '<':
566
+ return left < right;
567
+ case '>=':
568
+ return left >= right;
569
+ case '<=':
570
+ return left <= right;
571
+ case '==':
572
+ return left === right;
352
573
  }
353
574
 
354
- throw new Error(`Unknown operator ${node.operator}`);
575
+ throw nodeError(`Unknown operator ${node.operator}`);
355
576
  }
356
577
 
357
- /* ===== LOGICAL ===== */
358
- case "LogicalExpression": {
578
+ // Short-circuit: && returns first falsy, || returns first truthy, ?? returns first non-nullish
579
+ case 'LogicalExpression': {
359
580
  const left = evaluateAST(node.left, context);
360
581
 
361
- if (node.operator === "&&") {
582
+ if (node.operator === '&&') {
362
583
  return left && evaluateAST(node.right, context);
363
584
  }
364
585
 
365
- if (node.operator === "||") {
586
+ if (node.operator === '||') {
366
587
  return left || evaluateAST(node.right, context);
367
588
  }
368
589
 
369
- if (node.operator === "??") {
590
+ if (node.operator === '??') {
370
591
  return left ?? evaluateAST(node.right, context);
371
592
  }
372
593
 
373
- throw new Error(`Unknown logical operator ${node.operator}`);
594
+ throw nodeError(`Unknown logical operator ${node.operator}`);
595
+ }
596
+
597
+ // Range [start..end] inclusive: returns array of integers from floor(start) to floor(end)
598
+ case 'RangeExpression': {
599
+ const start = evaluateAST(node.start, context);
600
+ const end = evaluateAST(node.end, context);
601
+ if (typeof start !== 'number' || typeof end !== 'number') {
602
+ throw nodeError('Range requires numeric bounds');
603
+ }
604
+ const result = [];
605
+ for (let i = Math.floor(start); i <= Math.floor(end); i++) {
606
+ result.push(i);
607
+ }
608
+ return result;
609
+ }
610
+
611
+ // Lambda: return a callable function evaluating the body with params bound in a new scope
612
+ case 'ArrowFunctionExpression': {
613
+ const fn = (/** @type {any[]} */ ...args) => {
614
+ const scopedContext = context.withScope(createFunctionScope(node.params, args));
615
+ return evaluateAST(node.body, scopedContext);
616
+ };
617
+ return fn;
374
618
  }
375
619
 
376
- /* ===== FUNCTION CALL ===== */
377
- case "CallExpression": {
620
+ // Function call: flatten spread (...array) arguments, then invoke
621
+ case 'CallExpression': {
378
622
  const fnName = node.callee.name;
379
623
  const fn = fns.get(fnName);
380
624
 
381
- const args = node.arguments.map(arg =>
382
- evaluateAST(arg, context)
383
- );
625
+ const rawArgs = node.arguments.map((/** @type {{ type: string; argument: any; }} */ arg) => {
626
+ if (arg.type === 'SpreadElement') {
627
+ const val = evaluateAST(arg.argument, context);
628
+ if (!Array.isArray(val)) {
629
+ throw new Error('Spread operator requires an array');
630
+ }
631
+ return { spread: true, values: val };
632
+ }
633
+ return { spread: false, value: evaluateAST(arg, context) };
634
+ });
635
+ const args = [];
636
+ for (const arg of rawArgs) {
637
+ if (arg.spread) {
638
+ args.push(...arg.values);
639
+ } else {
640
+ args.push(arg.value);
641
+ }
642
+ }
384
643
 
385
644
  return fn(...args);
386
645
  }
387
646
 
388
- /* ===== PIPELINE ===== */
389
- case "PipelineExpression": {
647
+ // Pipeline: left value is passed as first argument to the right function/expression
648
+ case 'PipelineExpression': {
390
649
  const leftVal = evaluateAST(node.left, context);
391
650
 
392
651
  // right must be function
393
- if (node.right.type === "CallExpression") {
652
+ if (node.right.type === 'CallExpression') {
394
653
  const fnName = node.right.callee.name;
395
654
  const fn = fns.get(fnName);
396
655
 
397
656
  const args = [
398
657
  leftVal,
399
- ...node.right.arguments.map(arg =>
400
- evaluateAST(arg, context)
401
- )
658
+ ...node.right.arguments.map((/** @type {any} */ arg) => evaluateAST(arg, context)),
402
659
  ];
403
660
 
404
661
  return fn(...args);
405
662
  }
406
663
 
407
- if (node.right.type === "Identifier") {
664
+ if (node.right.type === 'Identifier') {
408
665
  const fn = fns.get(node.right.name);
409
666
  return fn(leftVal);
410
667
  }
411
668
 
412
- throw new Error("Invalid pipeline target");
669
+ throw nodeError('Invalid pipeline target');
413
670
  }
414
671
 
415
- /* ===== UNIT CONVERSION ===== */
416
- case "UnitConversion": {
672
+ // Unit conversion: value fromUnit -> toUnit
673
+ case 'UnitConversion': {
417
674
  const from = evaluateAST(node.from, context);
418
675
 
419
676
  if (!isUnitObj(from)) {
420
- throw new Error("Left side must be a unit value");
677
+ throw nodeError('Left side must be a unit value');
421
678
  }
422
679
 
423
680
  if (!units) {
424
- throw new Error("Unit system not available");
681
+ throw nodeError('Unit system not available');
425
682
  }
426
683
 
427
684
  return units.convert(from.value, from.unit, node.to);
428
685
  }
429
686
 
430
- /* ===== ARRAY ===== */
431
- case "ArrayExpression":
432
- return node.elements.map(el => evaluateAST(el, context));
687
+ // ARRAY
688
+ case 'ArrayExpression':
689
+ return node.elements.map((/** @type {any} */ el) => evaluateAST(el, context));
433
690
 
434
- case "IndexExpression": {
691
+ // Matrix/array indexing: target[selector1, selector2] with 1-based and slice support
692
+ case 'IndexExpression': {
435
693
  const target = evaluateAST(node.object, context);
436
694
  return indexMatrix(target, node.selectors);
437
695
  }
438
696
 
439
- /* ===== OBJECT ===== */
440
- case "ObjectExpression": {
697
+ // OBJECT
698
+ case 'ObjectExpression': {
441
699
  const obj = {};
442
- for (let p of node.properties) {
700
+ for (const p of node.properties) {
443
701
  obj[p.key] = evaluateAST(p.value, context);
444
702
  }
445
703
  return obj;
446
704
  }
447
705
 
448
- /* ===== MEMBER ===== */
449
- case "MemberExpression": {
706
+ // Property access: obj.prop; optional chaining (?.) returns undefined if obj is null/undefined
707
+ case 'MemberExpression': {
450
708
  const obj = evaluateAST(node.object, context);
451
709
 
452
- if (node.optional && obj == null) return undefined;
710
+ if (node.optional && (obj === null || obj === undefined)) {
711
+ return undefined;
712
+ }
453
713
 
454
714
  return obj[node.property.name];
455
715
  }
456
716
 
457
717
  default:
458
- throw new Error(`Unknown AST node type: ${node.type}`);
718
+ throw nodeError(`Unknown AST node type: ${node.type}`);
459
719
  }
460
720
  }