Do not report a name the expression does not contain (#989) - #1000
Merged
Conversation
ConditionalSet.DirectChildren is its predicate with the bound name renamed
to a fresh one, so that two builders differing only in that name compare
and hash alike. Every property that reports names by walking DirectChildren
returned that invented name.
"{ k : k > 0 }".ToEntity().FreeVariables was: { %1 } now: { }
"{ k : k > 0 }".ToEntity().Vars was: { %1 } now: { k }
"{ k : k > a }".ToEntity().Vars was: { %1, a } now: { k, a }
%1 is neither answer on anybody's definition: not the bound name, not in
the expression the caller wrote, different for a different predicate, and
not typeable -- the parser has no %. It broke Vars' own promise too, which
is the variables that occur; k occurs and was missing, %1 does not occur
and was there. The issue reports this for FreeVariables only, but the leak
is one channel and reaches all three.
A set builder binds the name it declares exactly as a lambda binds its
parameter, so all three now answer for { k : ... } what they already
answered for lambda(k, ...).
Only the reporting changed. DirectChildren still publishes the renamed
predicate, because that is what makes two alpha-equivalent builders equal,
and Replace and Substitute never used it -- both override and work from Var
and Predicate directly. { x : x > 0 } = { y : y > 0 } is still True, with a
test saying so.
The second half of the issue -- whether sum, integral, limit and derivative
should bind their variable here too -- is a documented choice rather than a
leak, and is left open.
Suite 7376 passed, 0 failed. Corpus unchanged at 116/119 with 0 wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKcbJP266A3EGyQ5kq7Ru
This was referenced Aug 22, 2026
Collaborator
Author
|
Cross-reference: #1001 (the SymPy exporter, #985) needs this one to finish its job, and says so on its own thread. Measured on a local integration branch of the two: 45 of 45 generated programs run under |
This was referenced Aug 22, 2026
…laceholder-989 # Conflicts: # BREAKING-CHANGES.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes §1 of #989. Leaves §2 open — see the end.
The defect
ConditionalSet.DirectChildrenis its predicate with the bound name renamed to a fresh one, so that two builders differing only in that name compare and hash alike. Every property that reports names by walkingDirectChildrenpicked that invented name up and returned it.%1is neither answer on anybody's definition. It is not the bound name, it is not in the expression the caller wrote,Variable.CreateTempinvents a different one for a different predicate, and it cannot be typed — the parser has no%.Wider than the issue says
I filed #989 against
FreeVariablesand wrote there thatVars"promises less: it is documented as the variables that occur, and an occurrence is what it counts". That was wrong, and measuring it is what showed it: the leak is one channel —DirectChildren— andVarsandVarsAndConstsgo through it too.%1does not occur either, andk, which does, was missing. SoVarswas not being lenient, it was breaking its own promise in both directions.Fixing only the property the issue named would have left the same invented name in the more widely used one.
The fix
A set builder binds the name it declares exactly as a lambda binds its parameter, so all three properties now answer for
{ k : ... }what they already answered forlambda(k, ...):lambda(k, k > a){ k : k > a }was{ k : k > a }nowFreeVariables{ a }{ %1, a }{ a }Vars{ k, a }{ %1, a }{ k, a }Two cases added, next to the
Lambdacase that was already there.What is deliberately untouched
DirectChildrenstill publishes the renamed predicate. I checked whether the rename could simply be dropped at the source — it is what gives alpha-invariance toSortHash,EqualsImpreciselyand pattern matching, and{ x : x > 0 } = { y : y > 0 }isTruebecause of it. There is a test asserting that still holds.It was worth checking because the two obvious rewriting consumers do not use it:
ConditionalSet.Replaceisfunc(New(Var, Predicate.Replace(func)))andConditionalSet.Substitutedoes its own capture-avoiding rename, both fromVarandPredicatedirectly. So only the reporting properties ever saw%1, and only they change.§2 is not answered here
Whether
sum,integral,limitandderivativeshould bind their variable inFreeVariablestoo is the other half of #989. It is a documented choice — the XML doc says "a parameter of some outer lambda" — and a breaking change for anyone reading the property as "occurring variables", so I would rather have an answer than assume one. That issue stays open.Measured
BREAKING-CHANGES.mdentry with both values measured on a build of each arm.Against the other open PRs
Derived with
git merge-tree --write-tree:fix/bound-name-must-be-symbolicBREAKING-CHANGES.mdonlyconstant-node-984BREAKING-CHANGES.mdonlyfix/special-set-membership-995BREAKING-CHANGES.mdonlyfix/determinant-division-free-992BREAKING-CHANGES.mdonlyNo source conflicts with any of them. Whichever merges last takes the mechanical round and I will do it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KbKcbJP266A3EGyQ5kq7Ru