exprify 1.0.1 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exprify",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A powerful math expression parser and evaluator with runtime data-type checking",
5
5
  "type": "module",
6
6
  "main": "dist/exprify.cjs.js",
@@ -10,6 +10,7 @@ import { globalUnits } from "../utils/globalUnits.js";
10
10
  import { createVarStore } from "../variables/store.js";
11
11
  import { createFunctionRegistry } from "../function/registry.js";
12
12
  import { internalFunctions } from "../function/internal.js";
13
+ import { isDenseMatrixWrapper, serializeExprifyValue, wrapDenseMatrix } from "../utils/matrix.js";
13
14
 
14
15
  import { buildAST } from "../parser/astBuild.js";
15
16
 
@@ -42,6 +43,18 @@ const formatComplex = (value) => {
42
43
  return `${real} ${sign} ${imagPart}`;
43
44
  };
44
45
 
46
+ const formatScalar = (value) => {
47
+ if (typeof value !== "number") {
48
+ return String(value);
49
+ }
50
+
51
+ if (Number.isInteger(value)) {
52
+ return String(value);
53
+ }
54
+
55
+ return Number(value.toFixed(14)).toString();
56
+ };
57
+
45
58
  const formatResult = (value) => {
46
59
  if (isComplex(value)) {
47
60
  return formatComplex(value);
@@ -51,12 +64,20 @@ const formatResult = (value) => {
51
64
  return `${value.value} ${value.unit}`;
52
65
  }
53
66
 
67
+ if (isDenseMatrixWrapper(value)) {
68
+ return serializeExprifyValue(value);
69
+ }
70
+
54
71
  if (isMatrix(value)) {
55
- return value.map((row) => row.join("\t")).join("\n");
72
+ return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
56
73
  }
57
74
 
58
75
  if (Array.isArray(value)) {
59
- return value.join("\n");
76
+ return JSON.stringify(value);
77
+ }
78
+
79
+ if (value && typeof value === "object") {
80
+ return serializeExprifyValue(value);
60
81
  }
61
82
 
62
83
  return value;
@@ -70,6 +91,214 @@ class exprify {
70
91
  this.functions = createFunctionRegistry(internalFunctions);
71
92
  this.variables = createVarStore();
72
93
  this._cache = new Map();
94
+ this.variables.set("pi", Math.PI);
95
+ this.variables.set("e", Math.E);
96
+ this.addFunction("parse", (expression) => {
97
+ if (typeof expression !== "string") {
98
+ throw new Error("parse() expects an expression string");
99
+ }
100
+ return expression;
101
+ });
102
+ this.addFunction("leafCount", (value) => {
103
+ const countLeafTokens = (expression) => {
104
+ const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, "$1");
105
+ const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
106
+ return matches ? matches.length : 0;
107
+ };
108
+
109
+ let ast = value;
110
+ if (typeof value === "string") {
111
+ try {
112
+ ast = this.parse(value).ast;
113
+ } catch {
114
+ return countLeafTokens(value);
115
+ }
116
+ }
117
+
118
+ const countLeaves = (node) => {
119
+ if (!node || typeof node !== "object") return 0;
120
+
121
+ switch (node.type) {
122
+ case "Literal":
123
+ case "ImaginaryLiteral":
124
+ case "UnitLiteral":
125
+ case "Identifier":
126
+ return 1;
127
+ default:
128
+ return Object.values(node).reduce((sum, child) => {
129
+ if (Array.isArray(child)) {
130
+ return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
131
+ }
132
+
133
+ return sum + countLeaves(child);
134
+ }, 0);
135
+ }
136
+ };
137
+
138
+ return countLeaves(ast);
139
+ });
140
+ this.addFunction("matrix", (value) => wrapDenseMatrix(value));
141
+ this.addFunction("sparse", (value) => wrapDenseMatrix(value));
142
+ this.addFunction("rationalize", (expression, withDetails = false) => {
143
+ if (typeof expression !== "string") {
144
+ throw new Error("rationalize() expects an expression string");
145
+ }
146
+
147
+ const normalizedExpression = expression
148
+ .replace(/\s+/g, "")
149
+ .replace(/(\d)([a-zA-Z(])/g, "$1*$2")
150
+ .replace(/([a-zA-Z)])(\d)/g, "$1*$2");
151
+
152
+ const polyKey = (powers) => JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
153
+ const keyToPowers = (key) => Object.fromEntries(JSON.parse(key));
154
+ const makePoly = (terms = new Map()) => terms;
155
+ const constPoly = (value) => new Map([[polyKey({}), value]]);
156
+ const varPoly = (name) => new Map([[polyKey({ [name]: 1 }), 1]]);
157
+ const cleanPoly = (poly) => new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
158
+ const addPoly = (a, b, sign = 1) => {
159
+ const result = new Map(a);
160
+ for (const [key, coeff] of b.entries()) {
161
+ result.set(key, (result.get(key) || 0) + (sign * coeff));
162
+ }
163
+ return cleanPoly(result);
164
+ };
165
+ const multiplyPoly = (a, b) => {
166
+ const result = new Map();
167
+ for (const [keyA, coeffA] of a.entries()) {
168
+ const powersA = keyToPowers(keyA);
169
+ for (const [keyB, coeffB] of b.entries()) {
170
+ const powersB = keyToPowers(keyB);
171
+ const merged = { ...powersA };
172
+ for (const [name, power] of Object.entries(powersB)) {
173
+ merged[name] = (merged[name] || 0) + power;
174
+ }
175
+ const key = polyKey(merged);
176
+ result.set(key, (result.get(key) || 0) + (coeffA * coeffB));
177
+ }
178
+ }
179
+ return cleanPoly(result);
180
+ };
181
+ const powPoly = (poly, exponent) => {
182
+ let result = constPoly(1);
183
+ for (let index = 0; index < exponent; index++) {
184
+ result = multiplyPoly(result, poly);
185
+ }
186
+ return result;
187
+ };
188
+ const rational = (num, den = constPoly(1)) => ({ num, den });
189
+ const addRat = (a, b, sign = 1) => rational(
190
+ addPoly(
191
+ multiplyPoly(a.num, b.den),
192
+ multiplyPoly(b.num, a.den),
193
+ sign
194
+ ),
195
+ multiplyPoly(a.den, b.den)
196
+ );
197
+ const mulRat = (a, b) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
198
+ const divRat = (a, b) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
199
+ const negRat = (value) => rational(addPoly(new Map(), value.num, -1), value.den);
200
+ const astToRat = (node) => {
201
+ switch (node.type) {
202
+ case "Literal":
203
+ return rational(constPoly(node.value));
204
+ case "Identifier":
205
+ return rational(varPoly(node.name));
206
+ case "UnaryExpression":
207
+ if (node.operator === "-") return negRat(astToRat(node.argument));
208
+ throw new Error("Unsupported unary operator");
209
+ case "BinaryExpression": {
210
+ const left = astToRat(node.left);
211
+ const right = astToRat(node.right);
212
+ switch (node.operator) {
213
+ case "+": return addRat(left, right);
214
+ case "-": return addRat(left, right, -1);
215
+ case "*": return mulRat(left, right);
216
+ case "/": return divRat(left, right);
217
+ case "^": {
218
+ if (node.right.type !== "Literal" || !Number.isInteger(node.right.value) || node.right.value < 0) {
219
+ throw new Error("Unsupported exponent");
220
+ }
221
+ return rational(
222
+ powPoly(left.num, node.right.value),
223
+ powPoly(left.den, node.right.value)
224
+ );
225
+ }
226
+ default:
227
+ throw new Error("Unsupported operator in rationalize()");
228
+ }
229
+ }
230
+ default:
231
+ throw new Error("Unsupported expression in rationalize()");
232
+ }
233
+ };
234
+ const formatPoly = (poly) => {
235
+ const entries = [...poly.entries()]
236
+ .filter(([, coeff]) => coeff !== 0)
237
+ .sort(([keyA], [keyB]) => {
238
+ const powersA = keyToPowers(keyA);
239
+ const powersB = keyToPowers(keyB);
240
+ const firstVarA = Object.keys(powersA).sort()[0] || "";
241
+ const firstVarB = Object.keys(powersB).sort()[0] || "";
242
+
243
+ if (firstVarA !== firstVarB) {
244
+ return firstVarA.localeCompare(firstVarB);
245
+ }
246
+
247
+ const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
248
+ const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
249
+ return degreeB - degreeA;
250
+ });
251
+
252
+ if (!entries.length) return "0";
253
+
254
+ return entries.map(([key, coeff], index) => {
255
+ const powers = keyToPowers(key);
256
+ const absCoeff = Math.abs(coeff);
257
+ const variablePart = Object.entries(powers)
258
+ .map(([name, power]) => power === 1 ? name : `${name} ^ ${power}`)
259
+ .join(" * ");
260
+ let body = variablePart;
261
+
262
+ if (!body) {
263
+ body = `${absCoeff}`;
264
+ } else if (absCoeff !== 1) {
265
+ body = `${absCoeff} * ${body}`;
266
+ }
267
+
268
+ if (index === 0) {
269
+ return coeff < 0 ? `- ${body}`.replace("- ", "-") : body;
270
+ }
271
+
272
+ return coeff < 0 ? `- ${body}` : `+ ${body}`;
273
+ }).join(" ");
274
+ };
275
+
276
+ const ast = this.parse(normalizedExpression).ast;
277
+ const result = astToRat(ast);
278
+ const numerator = formatPoly(result.num);
279
+ const denominator = formatPoly(result.den);
280
+ const variableSet = new Set();
281
+
282
+ for (const poly of [result.num, result.den]) {
283
+ for (const key of poly.keys()) {
284
+ for (const name of Object.keys(keyToPowers(key))) {
285
+ variableSet.add(name);
286
+ }
287
+ }
288
+ }
289
+
290
+ if (!withDetails) {
291
+ return `(${numerator}) / (${denominator})`;
292
+ }
293
+
294
+ return {
295
+ numerator,
296
+ denominator,
297
+ coefficients: [],
298
+ variables: [...variableSet].sort(),
299
+ expression: `(${numerator}) / (${denominator})`
300
+ };
301
+ });
73
302
  }
74
303
 
75
304
  setVariable(name, value) {
@@ -1,4 +1,7 @@
1
+ import { unwrapDenseMatrix, wrapDenseMatrix } from "../utils/matrix.js";
2
+
1
3
  function validateSquareMatrix(matrix) {
4
+ matrix = unwrapDenseMatrix(matrix);
2
5
  if (!Array.isArray(matrix) || matrix.length === 0) {
3
6
  throw new Error("det() expects a non-empty matrix");
4
7
  }
@@ -22,6 +25,7 @@ function validateSquareMatrix(matrix) {
22
25
  }
23
26
 
24
27
  function determinant(matrix) {
28
+ matrix = unwrapDenseMatrix(matrix);
25
29
  validateSquareMatrix(matrix);
26
30
 
27
31
  if (matrix.length === 1) {
@@ -41,6 +45,339 @@ function determinant(matrix) {
41
45
  }, 0);
42
46
  }
43
47
 
48
+ function toLinearArray(value) {
49
+ const unwrapped = unwrapDenseMatrix(value);
50
+ return Array.isArray(unwrapped) ? unwrapped : value;
51
+ }
52
+
53
+ function asMatrixData(value) {
54
+ const data = unwrapDenseMatrix(value);
55
+ if (!Array.isArray(data)) {
56
+ throw new Error("Expected matrix data");
57
+ }
58
+ return data;
59
+ }
60
+
61
+ function solveLinearSystem(coefficients, constants) {
62
+ const n = coefficients.length;
63
+ const augmented = coefficients.map((row, rowIndex) => [...row, constants[rowIndex]]);
64
+
65
+ for (let pivot = 0; pivot < n; pivot++) {
66
+ let maxRow = pivot;
67
+ let maxValue = Math.abs(augmented[pivot][pivot]);
68
+
69
+ for (let row = pivot + 1; row < n; row++) {
70
+ const current = Math.abs(augmented[row][pivot]);
71
+ if (current > maxValue) {
72
+ maxValue = current;
73
+ maxRow = row;
74
+ }
75
+ }
76
+
77
+ if (maxValue === 0) {
78
+ throw new Error("Linear system is singular");
79
+ }
80
+
81
+ if (maxRow !== pivot) {
82
+ [augmented[pivot], augmented[maxRow]] = [augmented[maxRow], augmented[pivot]];
83
+ }
84
+
85
+ const pivotValue = augmented[pivot][pivot];
86
+ for (let col = pivot; col <= n; col++) {
87
+ augmented[pivot][col] /= pivotValue;
88
+ }
89
+
90
+ for (let row = 0; row < n; row++) {
91
+ if (row === pivot) continue;
92
+ const factor = augmented[row][pivot];
93
+ for (let col = pivot; col <= n; col++) {
94
+ augmented[row][col] -= factor * augmented[pivot][col];
95
+ }
96
+ }
97
+ }
98
+
99
+ return augmented.map((row) => row[n]);
100
+ }
101
+
102
+ function lupDecomposition(input) {
103
+ const matrix = asMatrixData(input).map((row) => [...row]);
104
+ validateSquareMatrix(matrix);
105
+
106
+ const n = matrix.length;
107
+ const permutation = Array.from({ length: n }, (_, index) => index);
108
+
109
+ for (let pivot = 0; pivot < n; pivot++) {
110
+ let maxRow = pivot;
111
+ let maxValue = Math.abs(matrix[pivot][pivot]);
112
+
113
+ for (let row = pivot + 1; row < n; row++) {
114
+ const current = Math.abs(matrix[row][pivot]);
115
+ if (current > maxValue) {
116
+ maxValue = current;
117
+ maxRow = row;
118
+ }
119
+ }
120
+
121
+ if (maxValue === 0) {
122
+ throw new Error("Matrix is singular");
123
+ }
124
+
125
+ if (maxRow !== pivot) {
126
+ [matrix[pivot], matrix[maxRow]] = [matrix[maxRow], matrix[pivot]];
127
+ [permutation[pivot], permutation[maxRow]] = [permutation[maxRow], permutation[pivot]];
128
+ }
129
+
130
+ for (let row = pivot + 1; row < n; row++) {
131
+ matrix[row][pivot] /= matrix[pivot][pivot];
132
+ for (let col = pivot + 1; col < n; col++) {
133
+ matrix[row][col] -= matrix[row][pivot] * matrix[pivot][col];
134
+ }
135
+ }
136
+ }
137
+
138
+ const L = matrix.map((row, rowIndex) =>
139
+ row.map((value, colIndex) => {
140
+ if (rowIndex === colIndex) return 1;
141
+ if (rowIndex > colIndex) return value;
142
+ return 0;
143
+ })
144
+ );
145
+
146
+ const U = matrix.map((row, rowIndex) =>
147
+ row.map((value, colIndex) => (rowIndex <= colIndex ? value : 0))
148
+ );
149
+
150
+ return {
151
+ L: wrapDenseMatrix(L),
152
+ U: wrapDenseMatrix(U),
153
+ p: permutation
154
+ };
155
+ }
156
+
157
+ function linearSolve(aInput, bInput) {
158
+ const { L, U, p } = lupDecomposition(aInput);
159
+ const a = asMatrixData(aInput);
160
+ const bData = asMatrixData(bInput);
161
+ const bVector = Array.isArray(bData[0]) ? bData.map((row) => row[0]) : bData;
162
+
163
+ if (a.length !== bVector.length) {
164
+ throw new Error("Right-hand side dimension mismatch");
165
+ }
166
+
167
+ const permutedB = p.map((index) => bVector[index]);
168
+ const y = new Array(a.length).fill(0);
169
+
170
+ for (let row = 0; row < a.length; row++) {
171
+ y[row] = permutedB[row];
172
+ for (let col = 0; col < row; col++) {
173
+ y[row] -= L.data[row][col] * y[col];
174
+ }
175
+ }
176
+
177
+ const x = new Array(a.length).fill(0);
178
+ for (let row = a.length - 1; row >= 0; row--) {
179
+ x[row] = y[row];
180
+ for (let col = row + 1; col < a.length; col++) {
181
+ x[row] -= U.data[row][col] * x[col];
182
+ }
183
+ x[row] /= U.data[row][row];
184
+ }
185
+
186
+ return wrapDenseMatrix(x.map((value) => [value]));
187
+ }
188
+
189
+ function solveLyapunov(aInput, qInput) {
190
+ const A = asMatrixData(aInput).map((row) => [...row]);
191
+ const Q = asMatrixData(qInput).map((row) => [...row]);
192
+ validateSquareMatrix(A);
193
+ validateSquareMatrix(Q);
194
+
195
+ const n = A.length;
196
+ if (Q.length !== n) {
197
+ throw new Error("A and Q must have the same dimensions");
198
+ }
199
+
200
+ const coefficients = [];
201
+ const constants = [];
202
+
203
+ for (let row = 0; row < n; row++) {
204
+ for (let col = 0; col < n; col++) {
205
+ const equation = new Array(n * n).fill(0);
206
+
207
+ for (let k = 0; k < n; k++) {
208
+ equation[k * n + col] += A[row][k];
209
+ equation[row * n + k] += A[col][k];
210
+ }
211
+
212
+ coefficients.push(equation);
213
+ constants.push(-Q[row][col]);
214
+ }
215
+ }
216
+
217
+ const solution = solveLinearSystem(coefficients, constants);
218
+ const X = [];
219
+
220
+ for (let row = 0; row < n; row++) {
221
+ X.push(solution.slice(row * n, (row + 1) * n));
222
+ }
223
+
224
+ return wrapDenseMatrix(X);
225
+ }
226
+
227
+ function evaluatePolynomial(coefficients, x) {
228
+ return coefficients.reduce((sum, coefficient, index) => sum + (coefficient * (x ** index)), 0);
229
+ }
230
+
231
+ function syntheticDivide(coefficients, root) {
232
+ const descending = [...coefficients].reverse();
233
+ const quotient = [descending[0]];
234
+
235
+ for (let index = 1; index < descending.length - 1; index++) {
236
+ quotient.push(descending[index] + (quotient[index - 1] * root));
237
+ }
238
+
239
+ const remainder = descending[descending.length - 1] + (quotient[quotient.length - 1] * root);
240
+ return {
241
+ quotient: quotient.reverse(),
242
+ remainder
243
+ };
244
+ }
245
+
246
+ function solveQuadratic(coefficients) {
247
+ const [c, b, a] = coefficients;
248
+ const discriminant = (b ** 2) - (4 * a * c);
249
+ if (discriminant < 0) {
250
+ throw new Error("Only real roots are supported");
251
+ }
252
+
253
+ const sqrtDisc = Math.sqrt(discriminant);
254
+ return [
255
+ (-b + sqrtDisc) / (2 * a),
256
+ (-b - sqrtDisc) / (2 * a)
257
+ ];
258
+ }
259
+
260
+ function polynomialRoots(...coefficients) {
261
+ while (coefficients.length > 1 && coefficients[coefficients.length - 1] === 0) {
262
+ coefficients.pop();
263
+ }
264
+
265
+ const degree = coefficients.length - 1;
266
+ if (degree < 1) {
267
+ throw new Error("polynomialRoot() expects at least a linear polynomial");
268
+ }
269
+
270
+ if (degree === 1) {
271
+ const [b, a] = coefficients;
272
+ return [-b / a];
273
+ }
274
+
275
+ if (degree === 2) {
276
+ return solveQuadratic(coefficients);
277
+ }
278
+
279
+ if (degree === 3) {
280
+ const constant = coefficients[0];
281
+ const leading = coefficients[3];
282
+ const candidates = [];
283
+ const limit = Math.abs(constant);
284
+
285
+ for (let divisor = 1; divisor <= Math.max(1, limit); divisor++) {
286
+ if (limit % divisor === 0) {
287
+ candidates.push(divisor, -divisor);
288
+ }
289
+ }
290
+
291
+ for (const candidate of candidates) {
292
+ if (evaluatePolynomial(coefficients, candidate) === 0) {
293
+ const reduced = syntheticDivide(coefficients, candidate);
294
+ const remainingRoots = solveQuadratic(reduced.quotient);
295
+ return [candidate, ...remainingRoots];
296
+ }
297
+ }
298
+ }
299
+
300
+ throw new Error("polynomialRoot() currently supports degree up to 3");
301
+ }
302
+
303
+ function dotProduct(a, b) {
304
+ return a.reduce((sum, value, index) => sum + (value * b[index]), 0);
305
+ }
306
+
307
+ function vectorNorm(vector) {
308
+ return Math.sqrt(dotProduct(vector, vector));
309
+ }
310
+
311
+ function scaleVector(vector, scalar) {
312
+ return vector.map((value) => value * scalar);
313
+ }
314
+
315
+ function subtractVectors(a, b) {
316
+ return a.map((value, index) => value - b[index]);
317
+ }
318
+
319
+ function transpose(matrix) {
320
+ return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
321
+ }
322
+
323
+ function qrDecomposition(input) {
324
+ const A = asMatrixData(input).map((row) => [...row]);
325
+ if (!A.length || !A.every((row) => row.length === A[0].length)) {
326
+ throw new Error("qr() expects a rectangular matrix");
327
+ }
328
+
329
+ const rowCount = A.length;
330
+ const colCount = A[0].length;
331
+ const columns = transpose(A);
332
+ const qColumns = [];
333
+
334
+ for (let col = 0; col < colCount; col++) {
335
+ let vector = [...columns[col]];
336
+
337
+ for (let existing = 0; existing < qColumns.length; existing++) {
338
+ const projection = dotProduct(qColumns[existing], columns[col]);
339
+ vector = subtractVectors(vector, scaleVector(qColumns[existing], projection));
340
+ }
341
+
342
+ const norm = vectorNorm(vector);
343
+ if (norm === 0) {
344
+ throw new Error("qr() requires linearly independent columns");
345
+ }
346
+
347
+ qColumns.push(scaleVector(vector, 1 / norm));
348
+ }
349
+
350
+ for (let basisIndex = 0; qColumns.length < rowCount && basisIndex < rowCount; basisIndex++) {
351
+ let candidate = Array.from({ length: rowCount }, (_, index) => (index === basisIndex ? 1 : 0));
352
+
353
+ for (const column of qColumns) {
354
+ const projection = dotProduct(column, candidate);
355
+ candidate = subtractVectors(candidate, scaleVector(column, projection));
356
+ }
357
+
358
+ const norm = vectorNorm(candidate);
359
+ if (norm > 1e-10) {
360
+ qColumns.push(scaleVector(candidate, 1 / norm));
361
+ }
362
+ }
363
+
364
+ const Q = Array.from({ length: rowCount }, (_, rowIndex) =>
365
+ qColumns.map((column) => column[rowIndex])
366
+ );
367
+
368
+ const fullR = Array.from({ length: rowCount }, () => Array(colCount).fill(0));
369
+ for (let row = 0; row < rowCount; row++) {
370
+ for (let col = 0; col < colCount; col++) {
371
+ fullR[row][col] = dotProduct(qColumns[row], columns[col]);
372
+ }
373
+ }
374
+
375
+ return {
376
+ Q: wrapDenseMatrix(Q),
377
+ R: wrapDenseMatrix(fullR)
378
+ };
379
+ }
380
+
44
381
  function splitTerms(expression) {
45
382
  const normalized = expression.replace(/\s+/g, "");
46
383
  if (!normalized) {
@@ -177,6 +514,11 @@ export const internalFunctions = {
177
514
 
178
515
  pow: (a, b) => a ** b,
179
516
  det: (matrix) => determinant(matrix),
517
+ polynomialRoot: (...coefficients) => polynomialRoots(...coefficients),
518
+ lsolve: (a, b) => linearSolve(a, b),
519
+ lup: (matrix) => lupDecomposition(matrix),
520
+ lyap: (a, q) => solveLyapunov(a, q),
521
+ qr: (matrix) => qrDecomposition(matrix),
180
522
  simplify: (expression) => {
181
523
  if (typeof expression !== "string") {
182
524
  throw new Error("simplify() expects an expression string");
@@ -65,7 +65,7 @@ export function buildAST(tokens) {
65
65
  case "Identifier":
66
66
  return { type: "Identifier", name: token.name };
67
67
 
68
- case "Function": // 🔥 ADD THIS
68
+ case "Function":
69
69
  return {
70
70
  type: "Identifier",
71
71
  name: token.name
@@ -470,6 +470,29 @@ export function buildAST(tokens) {
470
470
  ) {
471
471
  const operator = tokens[current - 1].value;
472
472
 
473
+ if (left.type === "CallExpression") {
474
+ const isFunctionTarget =
475
+ left.callee?.type === "Identifier" &&
476
+ left.arguments.every((arg) => arg.type === "Identifier");
477
+
478
+ if (!isFunctionTarget) {
479
+ throw new Error("Invalid function definition");
480
+ }
481
+
482
+ const right = parseAssignment();
483
+
484
+ return {
485
+ type: "FunctionAssignmentExpression",
486
+ operator,
487
+ left: {
488
+ type: "Identifier",
489
+ name: left.callee.name
490
+ },
491
+ params: left.arguments.map((arg) => arg.name),
492
+ right
493
+ };
494
+ }
495
+
473
496
  if (
474
497
  left.type !== "Identifier" &&
475
498
  left.type !== "MemberExpression" &&