MathS.ToSympyCode emits a quotient of two integers as Python's /, which is float division, so an
exact value leaves as an inexact one. Measured on master (548ea178) with SymPy 1.14:
"1/2".ToEntity() => expr = 1 / 2 which Python evaluates to 0.5, a float
"x + 1/2".ToEntity() => expr = x + 1 / 2 which is x + 0.5, a Float coefficient
sympy.sympify(1/2) is 0.500000000000000 — a Float, not Rational(1, 2) — so the exactness is gone
before SymPy ever sees the expression, and nothing downstream can recover it.
It bites only the unsimplified form. "1/2".ToEntity() is a Divf of two integers, because a
printed rational parses back as a division (#873);
once simplified it is a Rational node and emits sympy.Rational(1, 2), which is exact. So the defect
appears exactly when a caller exports what they wrote rather than what the library computed — which is a
normal thing to do, and gives no sign that anything was lost.
Split out of #909, which was about names the
generated program never binds. That one is a defect with one answer; this one is a design choice, which
is why it is separate.
The choice
Emit sympy.Integer(n) for every integer. Then 1 / 2 becomes
sympy.Integer(1) / sympy.Integer(2), which SymPy evaluates to Rational(1, 2), and every arithmetic
operation in the emitted program is exact for the same reason. The cost is that x + 1 becomes
x + sympy.Integer(1), and a polynomial becomes noticeably wordier.
Or wrap only where exactness is at stake — the numerator or denominator of a division, the exponent
of a negative power. That keeps the common case readable, at the cost of the exporter needing to know
its context, which a per-node ToSymPy() does not have. It would mean passing that context down, or
handling the quotient case inside Divf.ToSymPy() by wrapping integer operands there.
My recommendation is the second, done in Divf. Divf(Integer, Integer) is the only shape where
Python's / silently changes the value — +, -, * and ** on Python integers are all exact — so
one arm covers it, and the emitted code stays readable everywhere else. 2 ** 70 already exports
correctly because Python's integers are arbitrary-precision.
Worth deciding at the same time: a finite Real emits Stringize(), which for a 100-digit EDecimal
is a decimal literal that Python narrows to a 17-digit float. sympy.Float('<digits>', <precision>)
would keep it. Same class of loss, different node, and it has no bearing on the choice above.
Why nothing caught it
The emitted code is never executed by the suite, and it never was — #909's fix added tests for what can
be checked without an interpreter (balanced parentheses, bound names), and value fidelity is not in
that set. The general answer is a harness that emits code for a corpus, runs it under Python, and
compares SymPy's value against the library's own — which would also serve
#717.
MathS.ToSympyCodeemits a quotient of two integers as Python's/, which is float division, so anexact value leaves as an inexact one. Measured on
master(548ea178) with SymPy 1.14:sympy.sympify(1/2)is0.500000000000000— aFloat, notRational(1, 2)— so the exactness is gonebefore SymPy ever sees the expression, and nothing downstream can recover it.
It bites only the unsimplified form.
"1/2".ToEntity()is aDivfof two integers, because aprinted rational parses back as a division (#873);
once simplified it is a
Rationalnode and emitssympy.Rational(1, 2), which is exact. So the defectappears exactly when a caller exports what they wrote rather than what the library computed — which is a
normal thing to do, and gives no sign that anything was lost.
Split out of #909, which was about names the
generated program never binds. That one is a defect with one answer; this one is a design choice, which
is why it is separate.
The choice
Emit
sympy.Integer(n)for every integer. Then1 / 2becomessympy.Integer(1) / sympy.Integer(2), which SymPy evaluates toRational(1, 2), and every arithmeticoperation in the emitted program is exact for the same reason. The cost is that
x + 1becomesx + sympy.Integer(1), and a polynomial becomes noticeably wordier.Or wrap only where exactness is at stake — the numerator or denominator of a division, the exponent
of a negative power. That keeps the common case readable, at the cost of the exporter needing to know
its context, which a per-node
ToSymPy()does not have. It would mean passing that context down, orhandling the quotient case inside
Divf.ToSymPy()by wrapping integer operands there.My recommendation is the second, done in
Divf.Divf(Integer, Integer)is the only shape wherePython's
/silently changes the value —+,-,*and**on Python integers are all exact — soone arm covers it, and the emitted code stays readable everywhere else.
2 ** 70already exportscorrectly because Python's integers are arbitrary-precision.
Worth deciding at the same time: a finite
RealemitsStringize(), which for a 100-digitEDecimalis a decimal literal that Python narrows to a 17-digit float.
sympy.Float('<digits>', <precision>)would keep it. Same class of loss, different node, and it has no bearing on the choice above.
Why nothing caught it
The emitted code is never executed by the suite, and it never was — #909's fix added tests for what can
be checked without an interpreter (balanced parentheses, bound names), and value fidelity is not in
that set. The general answer is a harness that emits code for a corpus, runs it under Python, and
compares SymPy's value against the library's own — which would also serve
#717.