Skip to content

Factor: a cache hit returns what the first call returned - #1571

Merged
axkr merged 1 commit into
masterfrom
fix/factor-cache-hit-same-result
Oct 23, 2026
Merged

axkr merged 1 commit into
masterfrom
fix/factor-cache-hit-same-result

Conversation

@tranleduy2000

Copy link
Copy Markdown
Collaborator

The bug

Two Factor calls with the same argument on one EvalEngine give different answers, and so does a
Factor after a Simplify of the same expression:

{Factor(6/(2-x)), Factor(6/(2-x))}                    {-6/(-2+x),6/(2-x)}
{Factor((-a+b)^3), Factor((-a+b)^3)}                  {-(a-b)^3,(-a+b)^3}
{Simplify(Cos(y)^2/(2-x)), Factor(Cos(y)^2/(2-x))}    {Cos(y)^2/(2-x),Cos(y)^2/(2-x)}
{Simplify(6/(2-x)+Cos(y)), Factor(6/(2-x)+Cos(y))}    {-6/(-2+x)+Cos(y),6/(2-x)+Cos(y)}

Factor(e) alone gives -6/(-2+x), -(a-b)^3, -Cos(y)^2/(-2+x) and
(-6-2*Cos(y)+x*Cos(y))/(-2+x). The engine cache under the key Factor(e) holds something else
than what Factor(e) returns:

  • AlgebraUtil.factor(IAST ast, ...) puts its result into the cache before Factor.evaluate
    applies factorSigns to it (0d84662), so a cache hit returns the form before factorSigns.
  • AlgebraUtil.factor(IExpr, EvalEngine) (withHomogenization=false, called by
    TrigSimplifyFu.tr0, which Simplify and FullSimplify run) and togetherPlus (trig=true)
    pass F.Factor(e) as that ast, and so write the same key with results computed with other
    settings. After Simplify(6/(2-x)+Cos(y)) the entry is F.NIL, and Factor returns its
    argument.

More inputs and the history are in #1570.

The fix

Factor.evaluate caches the result it returns, after factorSigns, and
AlgebraUtil.factor(IAST ast, ...) writes nothing to the cache. Factor.evaluate is the only
reader of that cache, so the other callers lose nothing. A first call returns what it returned
before; only cache hits change. With the Factor.evaluate change alone, a Factor after
TrigSimplifyFu, Simplify or FullSimplify still returns the result they cached.

Tests

AlgebraTest#testFactorIssue1570 checks the first two inputs above, the equation
Factor((x+2)/(x-5)+3==6/(2-x)) twice, and Factor(e) after TrigSimplifyFu(e), Simplify(e) and
FullSimplify(e) for four inputs.

With fix Without fix
AlgebraTest#testFactorIssue1570 pass expected: <{-6/(-2+x),-6/(-2+x)}> but was: <{-6/(-2+x),6/(2-x)}> at the first check
the other 7769 tests of the fast tier 7767 pass, 2 skipped 7767 pass, 2 skipped

The fast tier is mvn test -pl matheclipse-core -am; compared test by test, none of the other 7769
changed its result. Verified on 9a50f18, against a build with the fix and a build without it.

Fixes #1570

🤖 Generated with Claude Code

AlgebraUtil.factor(IAST ast, ...) put its result into the engine cache
under ast before Factor.evaluate applied factorSigns to it, so since
0d84662 a second Factor of the same argument returned the form
before factorSigns: {Factor(6/(2-x)), Factor(6/(2-x))} gave
{-6/(-2+x),6/(2-x)}. AlgebraUtil.factor(IExpr, EvalEngine), which
TrigSimplifyFu.tr0 calls for Simplify and FullSimplify, and togetherPlus
passed F.Factor(e) as that ast, and so wrote the same key with results
computed with other settings: after Simplify(6/(2-x)+Cos(y)) the entry
was F.NIL, and Factor returned its argument.

Factor.evaluate now caches the result it returns, after factorSigns,
and AlgebraUtil.factor writes nothing. Factor.evaluate is the only
reader of that cache, so the other callers lose nothing.
@tranleduy2000
tranleduy2000 requested a review from axkr October 23, 2026 14:35
@axkr
axkr merged commit 5a840ff into master Oct 23, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants