Skip to content

Commit 5566971

Browse files
committed
fix: update inline snapshots in arithmetic, calculus, derivatives, and logic tests for accuracy
- Corrected inline snapshots for sum evaluations in arithmetic tests. - Adjusted inline snapshots for indefinite integration in calculus tests. - Fixed derivative results for hyperbolic functions and other expressions in derivatives tests. - Simplified quantifier evaluations in logic tests by removing unnecessary array structures. - Enhanced inline snapshots for LaTeX syntax tests, ensuring clarity in expected outputs. - Updated polynomial division regression tests to reflect accurate simplifications.
1 parent 5bc172d commit 5566971

18 files changed

Lines changed: 1087 additions & 392 deletions

src/compute-engine/boxed-expression/ascii-math.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ const FUNCTIONS: Record<
201201
string | ((expr: BoxedExpression, serialize: AsciiMathSerializer) => string)
202202
> = {
203203
Abs: (expr: BoxedExpression, serialize) => `|${serialize(expr.op1)}|`,
204+
Norm: (expr: BoxedExpression, serialize) => `||${serialize(expr.op1)}||`,
204205

205206
// Trigonometric functions
206207
Sin: 'sin',

src/compute-engine/boxed-expression/box.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -532,13 +532,15 @@ function makeCanonicalFunction(
532532
result = new BoxedFunction(
533533
ce,
534534
name,
535-
validateArguments(
536-
ce,
537-
xs,
538-
opDef.signature.type,
539-
opDef.lazy,
540-
opDef.broadcastable
541-
) ?? xs,
535+
opDef.inferredSignature
536+
? xs
537+
: validateArguments(
538+
ce,
539+
xs,
540+
opDef.signature.type,
541+
opDef.lazy,
542+
opDef.broadcastable
543+
) ?? xs,
542544
{ metadata, canonical: true, scope }
543545
);
544546
return result;
@@ -583,13 +585,18 @@ function makeCanonicalFunction(
583585
opDef.associative ? name : undefined
584586
);
585587

586-
const adjustedArgs = validateArguments(
587-
ce,
588-
args,
589-
opDef.signature.type,
590-
opDef.lazy,
591-
opDef.broadcastable
592-
);
588+
// Skip validation for function literals with inferred signatures.
589+
// These will be validated during evaluation by the lambda function,
590+
// which handles currying and partial application.
591+
const adjustedArgs = opDef.inferredSignature
592+
? null
593+
: validateArguments(
594+
ce,
595+
args,
596+
opDef.signature.type,
597+
opDef.lazy,
598+
opDef.broadcastable
599+
);
593600

594601
// If we have some adjusted arguments, the arguments did not
595602
// match the parameters of the signature. We're done.

src/compute-engine/boxed-expression/boxed-function.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,19 @@ export class BoxedFunction extends _BoxedExpression {
174174
this.engine._typeResolver
175175
);
176176
} else if (isSignatureType(def.signature.type)) {
177+
// Preserve the argument information when updating the result type
178+
const oldSig = def.signature.type;
177179
def.signature = new BoxedType(
178180
{
179181
kind: 'signature',
182+
args: oldSig.args,
183+
optArgs: oldSig.optArgs,
184+
variadicArg: oldSig.variadicArg,
185+
variadicMin: oldSig.variadicMin,
180186
result:
181187
inferenceMode === 'narrow'
182-
? narrow(def.signature.type.result, t)
183-
: widen(def.signature.type.result, t),
188+
? narrow(oldSig.result, t)
189+
: widen(oldSig.result, t),
184190
},
185191
this.engine._typeResolver
186192
);

src/compute-engine/boxed-expression/boxed-operator-definition.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,20 @@ export class _BoxedOperatorDefinition implements BoxedOperatorDefinition {
320320
if (!boxedFn.isValid)
321321
throw Error(`Invalid function ${boxedFn.toString()}`);
322322

323+
// If no explicit signature was provided and the evaluate handler is a
324+
// Function expression, infer the signature from the function parameters
325+
// and body type.
326+
if (this.inferredSignature && boxedFn.operator === 'Function') {
327+
const body = boxedFn.ops![0];
328+
const params = boxedFn.ops!.slice(1);
329+
const bodyType = body.type.toString();
330+
const paramTypes = params.map(() => 'unknown').join(', ');
331+
this.signature = new BoxedType(
332+
`(${paramTypes}) -> ${bodyType}`,
333+
this.engine._typeResolver
334+
);
335+
}
336+
323337
const fn = applicable(boxedFn);
324338
evaluate = (xs) => fn(xs);
325339
Object.defineProperty(evaluate, 'toString', {

src/compute-engine/boxed-expression/simplify.ts

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,59 @@ type InternalSimplifyOptions = SimplifyOptions & {
1313
useVariations: boolean;
1414
};
1515

16+
const BASIC_ARITHMETIC = [
17+
'Add',
18+
'Subtract',
19+
'Multiply',
20+
'Divide',
21+
'Negate',
22+
'Power',
23+
'Rational',
24+
];
25+
26+
// Trig functions with constructible special values
27+
const CONSTRUCTIBLE_TRIG = ['Sin', 'Cos', 'Tan', 'Csc', 'Sec', 'Cot'];
28+
29+
/**
30+
* Check if an expression contains a constructible trig function somewhere
31+
* in its subexpressions. Used to determine if we need to recursively
32+
* simplify an operand to get constructible value simplification.
33+
*/
34+
function containsConstructibleTrig(expr: BoxedExpression): boolean {
35+
if (CONSTRUCTIBLE_TRIG.includes(expr.operator)) return true;
36+
if (!expr.ops) return false;
37+
return expr.ops.some((op) => containsConstructibleTrig(op));
38+
}
39+
40+
/**
41+
* Recursively evaluate purely numeric subexpressions without full simplification.
42+
* This handles cases like Power(x, Add(1,2)) where Add(1,2) should become 3.
43+
* Unlike full simplification, this won't expand polynomial factors.
44+
*/
45+
function evaluateNumericSubexpressions(expr: BoxedExpression): BoxedExpression {
46+
// Number literals are already simplified
47+
if (expr.isNumberLiteral) return expr;
48+
49+
// No ops means symbol or other atomic - return as is
50+
if (!expr.ops) return expr;
51+
52+
// If purely numeric (no unknowns), evaluate the whole expression
53+
if (expr.unknowns.length === 0 && BASIC_ARITHMETIC.includes(expr.operator)) {
54+
const evaluated = expr.evaluate();
55+
if (evaluated.isNumberLiteral) return evaluated;
56+
}
57+
58+
// Otherwise, recursively process operands
59+
const newOps = expr.ops.map((op) => evaluateNumericSubexpressions(op));
60+
61+
// Check if anything changed
62+
const changed = newOps.some((op, i) => op !== expr.ops![i]);
63+
if (!changed) return expr;
64+
65+
// Reconstruct with _fn to avoid re-canonicalization
66+
return expr.engine._fn(expr.operator, newOps);
67+
}
68+
1669
export function simplify(
1770
expr: BoxedExpression,
1871
options?: Partial<InternalSimplifyOptions>,
@@ -120,33 +173,75 @@ function simplifyOperands(
120173

121174
const def = expr.operatorDefinition;
122175

123-
// For scoped functions (Sum, Product), use holdMap but simplify non-body operands
176+
// For scoped functions (Sum, Product, D), use holdMap but simplify non-body operands
124177
if (def?.scoped === true) {
125178
const simplifiedOps = expr.ops.map((x, i) => {
126179
// Don't simplify the body (first operand) to allow pattern-matching rules to work
127180
if (i === 0) return x;
128181
// Simplify other operands (like Limits)
129182
return simplify(x, options).at(-1)!.value;
130183
});
131-
return expr.engine.function(expr.operator, simplifiedOps);
184+
// Use _fn() to bypass canonicalization - operands are already canonical.
185+
// This avoids triggering handlers like D's canonicalFunctionLiteralArguments
186+
// which would add extra Function wrappers.
187+
return expr.engine._fn(expr.operator, simplifiedOps);
132188
}
133189

134-
// For lazy but non-scoped functions (Multiply, Add), we need a balanced approach:
135-
// - Respect holdMap for evaluation semantics
136-
// - But also simplify Sum/Product operands that result from other simplification rules
190+
// For non-scoped functions, we need to balance simplification with holdMap semantics
137191

138192
// First get the operands through holdMap
139193
const ops = holdMap(expr, (x) => x);
140194

141-
// Then simplify any Sum/Product operands specifically
195+
// For lazy functions (Multiply, Add), only simplify Sum/Product operands
196+
// and expressions containing constructible trig functions
197+
// to avoid interfering with their special handling in simplify-rules
198+
if (def?.lazy) {
199+
const simplifiedOps = ops.map((x) => {
200+
if (
201+
x.operator === 'Sum' ||
202+
x.operator === 'Product' ||
203+
containsConstructibleTrig(x)
204+
) {
205+
return simplify(x, options).at(-1)!.value;
206+
}
207+
return x;
208+
});
209+
return expr.engine.function(expr.operator, simplifiedOps);
210+
}
211+
212+
// For non-lazy, non-scoped functions (e.g., Factorial2, Sqrt, Degrees),
213+
// recursively simplify operands. This ensures expressions like Factorial2(-1 + 2*3)
214+
// become Factorial2(5) and Degrees(tan(90-0.000001)) becomes Degrees(tan(89.999999)).
215+
//
216+
// EXCEPTION: For Divide expressions, only evaluate purely numeric subexpressions
217+
// but don't do full recursive simplification. This preserves factored polynomial
218+
// structure for the cancelCommonFactors rule.
219+
// e.g., (x-1)(x+2)/((x-1)(x+3)) should cancel to (x+2)/(x+3), not expand first.
220+
// But x^(1+2)/(1+2) should still simplify to x^3/3.
221+
if (expr.operator === 'Divide') {
222+
const simplifiedOps = ops.map((x) => evaluateNumericSubexpressions(x));
223+
const changed = simplifiedOps.some((op, i) => op !== ops[i]);
224+
if (!changed) return expr;
225+
return expr.engine._fn(expr.operator, simplifiedOps);
226+
}
227+
142228
const simplifiedOps = ops.map((x) => {
143-
if (x.operator === 'Sum' || x.operator === 'Product') {
229+
// For purely numeric basic arithmetic expressions, evaluate directly
230+
// to get simpler results like √(1+2) → √3
231+
if (!x.isNumberLiteral && x.ops && x.unknowns.length === 0) {
232+
if (BASIC_ARITHMETIC.includes(x.operator)) {
233+
const evaluated = x.evaluate();
234+
if (evaluated.isNumberLiteral) return evaluated;
235+
}
236+
}
237+
// For other expressions with ops (like Tan, Sqrt, etc.), recursively simplify
238+
if (x.ops) {
144239
return simplify(x, options).at(-1)!.value;
145240
}
146241
return x;
147242
});
148-
149-
return expr.engine.function(expr.operator, simplifiedOps);
243+
// Use _fn() since operands are already canonical (simplified above)
244+
return expr.engine._fn(expr.operator, simplifiedOps);
150245
}
151246

152247
function simplifyExpression(

src/compute-engine/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,8 @@ function assignValueAsValue(
20772077
if (typeof value === 'number' || typeof value === 'bigint')
20782078
return ce.number(value);
20792079
const expr = ce.box(value);
2080+
// Explicit function expressions should always be treated as operator definitions
2081+
if (expr.operator === 'Function') return undefined;
20802082
if (expr.unknowns.some((s) => s.startsWith('_'))) {
20812083
// If the expression has wildcards, it should be treated as a function
20822084
// E.g. ["Add", "_", 1] or ["Add", "_x", 1]
@@ -2099,7 +2101,10 @@ function assignValueAsOperatorDef(
20992101
const body = canonicalFunctionLiteral(ce.box(value));
21002102
if (body === undefined) return undefined;
21012103

2102-
return { evaluate: body, signature: 'function' };
2104+
// Don't set an explicit signature - let it be inferred from the body.
2105+
// This ensures inferredSignature = true, which allows the return type
2106+
// to be properly narrowed during type checking (e.g., in Add operands).
2107+
return { evaluate: body };
21032108
}
21042109

21052110
function defToString(

src/compute-engine/library/logic-utils.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,63 @@ import { asSmallInteger } from '../boxed-expression/numerics';
66
* Extracted from logic.ts for better code organization.
77
*/
88

9+
/**
10+
* Check if an And expression is a contradiction (contains A and Not(A)).
11+
* Non-recursive to avoid infinite loops.
12+
*/
13+
function isContradiction(
14+
args: ReadonlyArray<BoxedExpression>
15+
): boolean {
16+
for (let i = 0; i < args.length; i++) {
17+
const arg = args[i];
18+
for (let j = i + 1; j < args.length; j++) {
19+
const other = args[j];
20+
if (
21+
(arg.operator === 'Not' && arg.op1.isSame(other)) ||
22+
(other.operator === 'Not' && other.op1.isSame(arg))
23+
) {
24+
return true;
25+
}
26+
}
27+
}
28+
return false;
29+
}
30+
31+
/**
32+
* Check if an Or expression is a tautology (contains A and Not(A)).
33+
* Non-recursive to avoid infinite loops.
34+
*/
35+
function isTautologyCheck(
36+
args: ReadonlyArray<BoxedExpression>
37+
): boolean {
38+
for (let i = 0; i < args.length; i++) {
39+
const arg = args[i];
40+
for (let j = i + 1; j < args.length; j++) {
41+
const other = args[j];
42+
if (
43+
(arg.operator === 'Not' && arg.op1.isSame(other)) ||
44+
(other.operator === 'Not' && other.op1.isSame(arg))
45+
) {
46+
return true;
47+
}
48+
}
49+
}
50+
return false;
51+
}
52+
953
export function evaluateAnd(
1054
args: ReadonlyArray<BoxedExpression>,
1155
{ engine: ce }: { engine: ComputeEngine }
1256
): BoxedExpression | undefined {
1357
if (args.length === 0) return ce.True;
1458
const ops: BoxedExpression[] = [];
15-
for (const arg of args) {
59+
for (let arg of args) {
60+
// Check if an Or operand is a tautology (contains A and Not(A))
61+
// For example: Or(A, Not(A)) -> True, and And(..., True, ...) simplifies
62+
if (arg.operator === 'Or' && isTautologyCheck(arg.ops!)) {
63+
arg = ce.True;
64+
}
65+
1666
// ['And', ... , 'False', ...] -> 'False'
1767
if (arg.symbol === 'False') return ce.False;
1868
if (arg.symbol !== 'True') {
@@ -46,7 +96,13 @@ export function evaluateOr(
4696
): BoxedExpression | undefined {
4797
if (args.length === 0) return ce.True;
4898
const ops: BoxedExpression[] = [];
49-
for (const arg of args) {
99+
for (let arg of args) {
100+
// Check if an And operand is a contradiction (contains A and Not(A))
101+
// For example: And(A, Not(A)) -> False, and Or(..., False, ...) is removed
102+
if (arg.operator === 'And' && isContradiction(arg.ops!)) {
103+
arg = ce.False;
104+
}
105+
50106
// ['Or', ... , 'True', ...] -> 'True'
51107
if (arg.symbol === 'True') return ce.True;
52108
if (arg.symbol !== 'False') {

src/compute-engine/library/sets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,10 @@ export const SETS_LIBRARY: SymbolDefinitions = {
483483
complexity: 11200,
484484
// EL-3: Extended signature to support optional condition for filtered iteration
485485
// The condition is used by Sum/Product to filter values when iterating
486-
signature: '(value, collection, any?) -> boolean',
486+
signature: '(value, collection, boolean?) -> boolean',
487487
description:
488488
'Test whether a value is an element of a collection. ' +
489-
'Optional third argument is a condition for filtered iteration in Sum/Product.',
489+
'Optional third argument is a boolean expression (condition) for filtered iteration in Sum/Product.',
490490
evaluate: ([value, collection, _condition], { engine: ce }) => {
491491
// Note: condition is only used during Sum/Product iteration,
492492
// not for standalone Element evaluation

0 commit comments

Comments
 (0)