Skip to content

Do not differentiate over a name that cannot vary (#993) - #1004

Merged
Rafael-SOWNet merged 2 commits into
masterfrom
fix/differentiate-over-constant-993
Aug 22, 2026
Merged

Do not differentiate over a name that cannot vary (#993)#1004
Rafael-SOWNet merged 2 commits into
masterfrom
fix/differentiate-over-constant-993

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Closes #993, taking the 0 option rather than the refusal I argued for on the issue — see Why not refuse.

The defect

Entity.Differentiate(Variable) takes a Variable, and MathS.pi and MathS.e are ones, so they could be handed to it and it differentiated as though they varied.

"pi ^ 2".ToEntity().Differentiate(MathS.pi)        // was: 2 * pi                now: 0
"sin(pi)".ToEntity().Differentiate(MathS.pi)       // was: -1                    now: 0
"x * pi".ToEntity().Differentiate(MathS.pi)        // was: x                     now: 0
"sin(x * pi)".ToEntity().Differentiate(MathS.pi)   // was: cos(x * pi) * x       now: 0
"x!".ToEntity().Differentiate(MathS.pi)            // was: derivative(x!, pi)    now: 0
"e ^ 2".ToEntity().Differentiate(MathS.e)          // was: 2 * e                 now: 0

sin(pi) is 0, and its derivative with respect to anything is 0. -1 is cos(pi).

Why not refuse

I wrote on the issue that refusing was the option that "survives the family", because ∫ f dc has no zero-shaped answer and answering one with a value while refusing the other would be incoherent. That was wrong, and the two cases are not the same:

  • d(sin(pi))/d(pi) is settled — decide pi is constant and the expression does not vary, so the answer is 0. Refusing it would be declining something the library can answer.
  • ∫ f dpi has nothing to solve. Leaving it unevaluated is the existing "I could not settle this" doing its job, not a refusal.

So Integrate and Limit are untouched here, and there is no exception, no new API and no signature change — which is also what makes this a one-guard PR rather than a change across four public methods and the native ABI.

x! is the case worth calling out: the library cannot take that derivative at all, and it is still 0 here, because the variable settles it whatever the expression is. That is why the guard sits on the public overloads rather than deeper — a node that builds an unresolved Derivativef never reaches the chain rule's base case.

The test is the value, not the spelling

variable.Evaled is Number, not "is it named pi". A name a binder declares can vary even when it is spelled pi, and it evaluates to itself. That is what keeps this compatible with #984/#991, and I measured it on both branches before writing the guard.

Scope: the node form belongs to #984

On this branch "derivative(x * pi, pi)".InnerSimplified also goes from x to 0, because the node asks the same public method. That is not this PR's answer to keep. With #991 in, the parser binds pi there and it becomes 2 * pi_1 — a derivative over the variable the binder holds — which is #984's intended behaviour and supersedes it.

I found this by building the integration branch, not by reasoning: my first version guarded Derivativef.InnerSimplify directly and five tests failed on the composition, because it was answering a question #991 had already re-scoped. #993's own text says as much — "this call has no binder in it" — and I had over-reached past it. The guard is now on the direct API only, where the constant genuinely arrives in the position that says what varies.

Measured

  • Suite: 7383 passed, 0 failed, 14 skipped.
  • Corpus: 116/119, 0 wrong, 0 error, 0 timeout — compared row by row, no verdict or answer changed.
  • work/crashcheck: 1834 cases, 0 crashed.
  • 15 new cases; controlled, 9 of 18 fail with the guard reverted and the 9 that pass are the ordinary-variable controls.
  • BREAKING-CHANGES.md entry with both values measured on a build of each arm.

Against the other open PRs

against conflicts
#991 constant-node-984 BREAKING-CHANGES.md + Differentiation.cs (the same one line — both guards are wanted)
#1003 fix/differentiate-power-simplifies-1002 BREAKING-CHANGES.md + DerivativeTest.cs (both additive)
#990, #997, #998, #1000, #1001 BREAKING-CHANGES.md only

Resolved all of them on a local integration branch of #993 + #990 + #991 + #1003 and ran it: 7484 passed, 0 failed, with each PR's own behaviour intact —

sin(pi) by MathS.pi        0            <- #993
derivative(pi ^ 2, pi)     2 * pi_1     <- #984/#991
derivative(x ^ 3, 3)       declined     <- #964/#990
"x ^ 4".Differentiate(x,3) 2 * x * 3 * 4  <- #1002/#1003

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbKcbJP266A3EGyQ5kq7Ru

Entity.Differentiate(Variable) takes a Variable, and MathS.pi and MathS.e
are ones, so they could be handed to it and it differentiated as though
they varied.

  "sin(pi)".Differentiate(MathS.pi)   was: -1   now: 0

sin(pi) is 0, and its derivative with respect to anything is 0. -1 is
cos(pi): the chain rule run over a symbol that cannot change.

The test is whether the name evaluates to a number, not whether it is
spelled like a constant. A name a binder declares can vary even when it is
spelled pi, and it evaluates to itself -- which is what keeps this
compatible with #984, whose answer to derivative(pi ^ 2, pi) is over the
variable the binder holds. This call has no binder in it, and the constant
arrives directly in the position that says what varies.

The guard is on the two public overloads rather than deeper, so that x! --
whose derivative the library cannot take at all -- is answered too: the
variable settles it whatever the expression is. Variable.InnerDifferentiate
carries it as well, for the internal callers that reach the chain rule
without going through Differentiate.

Integrate and Limit over a constant are not changed. They have no value to
give, so leaving them unevaluated is the existing "I could not settle
this", not a refusal of something settled. The derivative is settled.

Suite 7383 passed, 0 failed. Corpus unchanged at 116/119 with 0 wrong.
crashcheck 1834 cases, 0 crashed. Composed with #990, #991 and #1003 on a
local integration branch: 7484 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKcbJP266A3EGyQ5kq7Ru
…ver-constant-993

# Conflicts:
#	BREAKING-CHANGES.md
#	Sources/AngouriMath/Functions/Continuous/Differentiation.cs
#	Sources/Tests/UnitTests/Calculus/DerivativeTest.cs
@Rafael-SOWNet
Rafael-SOWNet merged commit 1beb257 into master Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Differentiating over a constant answers as though it varies: sin(pi) differentiated by pi is -1

1 participant