@@ -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+
1669export 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
152247function simplifyExpression (
0 commit comments