Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ read first.
| loud | `mod` as a variable name | a variable | a keyword, so a parse error |
| loud | `NaN` as a variable name | a variable | a keyword, so the NaN value |
| loud | `MathS.ToSympyCode` of any non-integer rational | `SyntaxError` — a parenthesis was never closed | code that runs |
| **silent** | `MathS.ToSympyCode` of `1/2`, `2^(-1)` | ran, and gave the float `0.5` | `1/2`, exact |
| loud | `MathS.ToSympyCode` of `NaN`, `+oo`, `-oo` | `NameError` — the name is never bound | `sympy.nan`, `sympy.oo`, `-sympy.oo` |
| **silent** | `NaN` printed and read back | a variable of that name, which cancels and collects | the NaN value |
| **silent** | `Stringize` of powers, lambdas, applications, piecewises | did not parse back | parses back |
Expand Down Expand Up @@ -1040,6 +1041,36 @@ through `sympy.`. 17 of their 23 cases fail against the old exporter.

Issue [#909](https://github.com/asc-community/AngouriMath/issues/909).

### `MathS.ToSympyCode` keeps an exact value exact

The generated program ran, and then quietly gave a different number. Python's `/`, and its `**` with a
negative exponent, are float operations on two integers:

| expression | emitted | SymPy read it as | now emitted | and reads as |
|---|---|---|---|---|
| `1/2` | `1 / 2` | `0.500000000000000`, a `Float` | `sympy.Integer(1) / 2` | `1/2`, a `Rational` |
| `2^(-1)` | `2 ** (-1)` | `0.5` | `sympy.Integer(2) ** (-1)` | `1/2` |
| `2^(-3)` | `2 ** (-3)` | `0.125` | `sympy.Integer(2) ** (-3)` | `1/8` |
| `x + 1/2` | `x + 1 / 2` | `x + 0.5` | `x + sympy.Integer(1) / 2` | `x + 1/2` |

Making one operand a SymPy integer hands the arithmetic to SymPy, which keeps it exact. **Only a pair of
integers is rewritten**, and the rest of the emitted code is unchanged: with a symbol anywhere in the
shape SymPy's own operators already take over (`x / 2`, `1 / x`, `x ** (-1)`), and `+`, `-`, `*` and a
non-negative `**` are exact on Python integers, whose precision is unbounded — `2 ** 70` was always
right.

It bit only the **unsimplified** form, which is the one a caller writes: `"1/2".ToEntity()` is a `Divf`
of two integers, because a printed rational parses back as a division
([#873](https://github.com/asc-community/AngouriMath/issues/873)), while a simplified `1/2` is a
`Rational` node and already emitted `sympy.Rational(1, 2)`.

Checked by running the emitted programs against SymPy 1.14, which is the only way this class of defect
shows itself — the code was always valid, so the earlier tests could not have caught it, and the two
added here assert the property instead: no two plain integer literals are combined with `/` or with a
negative `**`.

Issue [#911](https://github.com/asc-community/AngouriMath/issues/911).

### `NaN` is now a keyword, and the printed form of NaN reads back

`Stringize` prints the NaN value as `NaN`, and the grammar had no such token, so reading it back gave a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,31 @@ internal override string ToSymPy() =>
Multiplier.ToSymPy(Multiplier.Priority < Priority.Mul) + " * " + Multiplicand.ToSymPy(Multiplicand.Priority < Priority.Mul);
}

/// <summary>
/// The operand written so that SymPy does the arithmetic rather than Python.
/// </summary>
/// <remarks>
/// Python's <c>/</c> and its <c>**</c> with a negative exponent are float operations on two
/// integers, so an exact value would leave here inexact: <c>1 / 2</c> is <c>0.5</c> there,
/// and <c>sympify(0.5)</c> is a <c>Float</c> and not <c>Rational(1, 2)</c> — the exactness
/// is gone before SymPy sees the expression, and nothing downstream recovers it. Making one
/// operand a SymPy integer is enough, since its operators sympify the other side.
/// <para/>
/// Only a pair of integers needs this. With a symbol anywhere in the shape SymPy's own
/// operators already take over, and <c>+</c>, <c>-</c>, <c>*</c> and a non-negative
/// <c>**</c> are exact on Python integers, whose precision is unbounded.
/// https://github.com/asc-community/AngouriMath/issues/911
/// </remarks>
private static string ToSymPyExactly(Entity operand)
=> $"sympy.Integer({operand.ToSymPy()})";

public partial record Divf
{
internal override string ToSymPy() =>
Dividend.ToSymPy(Dividend.Priority < Priority.Div) + " / " + Divisor.ToSymPy(Divisor.Priority <= Priority.Div);
(Dividend is Number.Integer && Divisor is Number.Integer
? ToSymPyExactly(Dividend)
: Dividend.ToSymPy(Dividend.Priority < Priority.Div))
+ " / " + Divisor.ToSymPy(Divisor.Priority <= Priority.Div);
}

public partial record Modf
Expand All @@ -49,7 +70,10 @@ public partial record Powf
internal override string ToSymPy() =>
Exponent == 0.5m
? "sympy.sqrt(" + Base.ToSymPy() + ")"
: Base.ToSymPy(Base.Priority < Priority.Pow) + " ** " + Exponent.ToSymPy(Exponent.Priority < Priority.Pow);
: (Base is Number.Integer && Exponent is Number.Integer { IsNegative: true }
? ToSymPyExactly(Base)
: Base.ToSymPy(Base.Priority < Priority.Pow))
+ " ** " + Exponent.ToSymPy(Exponent.Priority < Priority.Pow);
}

public partial record Signumf
Expand Down
46 changes: 46 additions & 0 deletions Sources/Tests/UnitTests/Convenience/ToSympyCodeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,51 @@ public void TheEmittedCodeHasBalancedParentheses(string expression)
[InlineData("-oo", "-sympy.oo")]
public void AValueIsEmittedWithSympysOwnSpelling(string expression, string expected) =>
Assert.Contains(expected, MathS.ToSympyCode(expression.ToEntity().Simplify()));

/// <summary>
/// Python's <c>/</c>, and its <c>**</c> with a negative exponent, are float operations on
/// two integers. So the emitted body must never combine two plain integer literals with
/// either: <c>1 / 2</c> is <c>0.5</c> there, and an exact value would leave here inexact
/// with nothing downstream able to recover it.
/// </summary>
/// <remarks>
/// Asserted as a property of the emitted text rather than by running it, since the suite
/// cannot depend on an interpreter. These are taken <em>unsimplified</em> on purpose: a
/// simplified <c>1/2</c> is a <c>Rational</c> node and was always exact, while what a
/// caller writes parses to a <c>Divf</c> of two integers — which is #873 — and that is the
/// shape that was losing the value.
/// https://github.com/asc-community/AngouriMath/issues/911
/// </remarks>
[Theory]
[InlineData("1/2")]
[InlineData("4/2")]
[InlineData("x + 1/2")]
[InlineData("2 ^ (-1)")]
[InlineData("2 ^ (-3)")]
[InlineData("(1/2) ^ (-1)")]
[InlineData("1/3 + 1/6")]
[InlineData("sin(x) / 2 + 1/4")]
public void NoTwoIntegerLiteralsAreCombinedInexactly(string expression)
{
var (_, body) = Split(MathS.ToSympyCode(expression.ToEntity()));
Assert.DoesNotMatch(@"(?<![\w.)])\d+\s*/\s*\d", body);
Assert.DoesNotMatch(@"(?<![\w.)])\d+\s*\*\*\s*\(-", body);
}

/// <summary>
/// What that looks like: one operand handed to SymPy, which then does the arithmetic and
/// keeps it exact. Only the pair-of-integers shapes are touched — with a symbol anywhere
/// SymPy's operators already take over, and <c>2 ** 70</c> is exact because Python's
/// integers are unbounded.
/// </summary>
[Theory]
[InlineData("1/2", "sympy.Integer(1) / 2")]
[InlineData("2 ^ (-1)", "sympy.Integer(2) ** (-1)")]
[InlineData("x / 2", "x / 2")]
[InlineData("1/x", "1 / x")]
[InlineData("2 ^ 70", "2 ** 70")]
[InlineData("x ^ (-1)", "x ** (-1)")]
public void OnlyAPairOfIntegersIsRewritten(string expression, string expected) =>
Assert.Contains(expected, MathS.ToSympyCode(expression.ToEntity()));
}
}
Loading