Skip to content

Emit SymPy code that runs (#985) - #1001

Merged
Rafael-SOWNet merged 2 commits into
masterfrom
fix/sympy-export-runs-985
Aug 22, 2026
Merged

Rafael-SOWNet merged 2 commits into
masterfrom
fix/sympy-export-runs-985

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

Closes #985 — and the two defects it names turned out to be four, across every set export.

The defect

MathS.ToSympyCode is documented as producing code you can run in SymPy. For every set, every lambda, every piecewise and every non-vector matrix, it did not. Run against SymPy 1.14:

expr = FiniteSet(1, 2)                        # NameError: name 'FiniteSet' is not defined
expr = Interval(0, 1, ...)                    # NameError: name 'Interval' is not defined
expr = Union(FiniteSet(1, 2), FiniteSet(3))   # NameError: name 'Union' is not defined
expr = x in S.Reals                           # NameError: name 'S' is not defined
expr = sympy.Lambda(x, )                      # TypeError: missing 1 required positional argument

Three separate faults, plus the exception #985 reported:

Unqualified names. The preamble is import sympy and nothing else, so FiniteSet, Interval, Union, Intersection, Complement, ConditionSet and S were every one of them a NameError. #985 named the lambda and the set builder; this is the rest of the file.

Parts interpolated rather than exported. Interval, Piecewise and the non-vector Matrix wrote their children as {Left} instead of {Left.ToSymPy()}. A bare variable spells the same in both languages, which is why it went unnoticed — it only shows once the part is a function:

[sin(a); b]   was: Interval(sin(a), b, ...)   # Python has no sin

A lambda emitted no body at all — sympy.Lambda(x, ).

A set builder threw. ConditionalSet.Codomain is Domain.Any, which SpecialSet.Create has no member for, so the exporter's cast raised AngouriBugException. SymPy names that set: S.UniversalSet is the one it prints ConditionSet without a third argument for, which is what "no restriction beyond the predicate" means here too.

x in RR changes shape, not just qualification

Python's in coerces its result to a bool, and a membership that is not decided is not one:

>>> x in sympy.S.Reals
TypeError: did not evaluate to a bool: (-oo < x) & (x < oo)
>>> sympy.S.Reals.contains(x)
(-oo < x) & (x < oo)
>>> sympy.S.Reals.contains(2)
True

.contains answers with the condition and still answers True/False where it can, so that is what is emitted now.

Why the tests passed

They assert substring containment. Assert.Contains("Piecewise((a, b), (c, d))") holds whether the parts were exported or merely interpolated — and holds on a program that does not run at all. Two of the existing cases were recording the broken output outright ("a in B" → "a in B", and ConditionSet(x, x > 0, S.Reals) unqualified); both are updated.

The new cases pin the whole emitted expression with Assert.Equal, and one asserts every SymPy name carries its qualifier.

Measured by running it

work/sympycheck in the analysis workspace executes the generated program rather than reading it — it exists because #909 and #911 were both invisible to string tests. Its corpus stopped at numbers; I extended it from 24 cases to 45 to reach sets, binders, piecewise and matrices.

programs that run
master 24 of 45
this PR 43 of 45
this PR + #1000 45 of 45

The two outstanding are the set builders, and they are not this PR's half: their expr line is correct here, but the preamble still declares %1 = sympy.Symbol('%1') — the placeholder leak of #989, fixed in #1000, and a SyntaxError whatever the body says. I built a local integration branch of both and ran it: 45 of 45, 0 inexact, suite 7395 passed 0 failed.

Also: suite 7385 passed, 0 failed on this branch alone; corpus 116/119, 0 wrong, with no case's verdict or answer changed.

Against the other open PRs

Derived with git merge-tree --write-tree:

against conflicts
#990, #991, #997, #998, #1000 BREAKING-CHANGES.md only

No source conflicts with any of them — this touches ToSympy.Omni.Classes.cs, which none of the others do. Whichever merges last takes the mechanical round and I will do it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbKcbJP266A3EGyQ5kq7Ru

ToSympyCode is documented as producing code you can run in SymPy. For every
set, every lambda, every piecewise and every non-vector matrix, it did not.
Three separate faults, none of which the tests could see:

Unqualified names. The preamble is `import sympy` and nothing else, so
FiniteSet, Interval, Union, Intersection, Complement, ConditionSet and S
were all NameError.

Parts interpolated rather than exported. Interval, Piecewise and the
non-vector Matrix wrote their children with {Left} instead of
{Left.ToSymPy()}. A bare variable spells the same in both languages, so it
only shows once the part is a function -- sin(a), which Python has not got.

A lambda emitted no body at all: sympy.Lambda(x, ), a TypeError.

And a set builder threw AngouriBugException out of the exporter, because
its Codomain is Domain.Any and SpecialSet.Create has no member for it.
SymPy names that set -- S.UniversalSet is the one it prints ConditionSet
without a third argument for, which is what "no restriction beyond the
predicate" means here too.

`x in RR` changes shape rather than only qualification. Python's `in`
coerces its result to a bool, and a membership that is not decided is not
one: `x in sympy.S.Reals` raises "did not evaluate to a bool". `.contains`
answers with the condition, and still answers True or False where it can.

Why the tests passed: they asserted substring containment, which holds
whether a part was exported or interpolated, and holds on a program that
does not run at all. The new cases pin the whole emitted expression, and
one asserts every SymPy name carries its qualifier.

Measured with work/sympycheck, which executes the generated program rather
than reading it, and whose corpus this extends from 24 cases to 45: 43 run,
0 inexact. The two that do not are the set builders, whose preamble still
declares the %1 placeholder of #989; their expr line is correct here.

Suite 7385 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
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

Composition with #1000 is measured, not assumed. Built a local integration branch of the two and ran both gates:

sympycheck: 45 emitted programs -- 0 failed to run, 0 returned an inexact value
suite:      7395 passed, 0 failed, 14 skipped

The two set builders that still fail on this branch alone are fixed by that combination and nothing else — their expr line is already right here:

# this PR alone
import sympy

%1 = sympy.Symbol('%1')          # <- #989's leak, a SyntaxError

expr = sympy.ConditionSet(x, x > 0, sympy.S.UniversalSet)   # <- correct

# with #1000
import sympy

x = sympy.Symbol('x')

expr = sympy.ConditionSet(x, x > 0, sympy.S.UniversalSet)   # runs

The only merge conflict between them is BREAKING-CHANGES.md, and both halves of it are additive — I resolved it by keeping both at-a-glance rows and both sections, which is what the integration branch above was built on.

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.

ToSympyCode drops a lambda's body and throws on a set builder

1 participant