Skip to content

Stringize prints a implies (b implies c) as a implies b implies c, which reads back as a different truth function #1032

Description

@Rafael-SOWNet

implies parses left-associatively — the grammar's implies_expression is a (...)* loop — but
Impliesf.Stringize parenthesises the left operand at equal priority and the right one only at
strictly lower priority. The guards are on the wrong sides, so a right-nested implication prints as
something the parser reads as the left-nested one:

var e = MathS.FromString("a implies (b implies c)");
e.Stringize()                          // "a implies b implies c"
MathS.FromString(e.Stringize())        // (a implies b) implies c

Implication is not associative, so this is a change of value and not only of shape. Measured on
v2.3.0 (6b93b40) by substituting:

a b c a implies (b implies c) what it prints and reads back as
False True False True False
False False False True False
True True False False False

This is the same defect as the (2^3)^2 printed as 2^3^2 case AGENTS.md names: a wrong reading
that is still a valid expression, so nothing raises.

-> prints through the same method, so a -> (b -> c) has it too.

Providedf has the mirror of it. provided_expression is right-associative on purpose (the grammar
says so in a comment), and the printer parenthesises the right operand rather than the left:

var e = MathS.FromString("(a provided b) provided c");
e.Stringize()                          // "a provided b provided c"
MathS.FromString(e.Stringize())        // a provided (b provided c)

That one looks like shape only — the grammar comment argues provided is associative — but it is the
same guard on the same wrong side.

The fix is one character on each side of Impliesf.Stringize, in
Functions/Output/ToString/ToString.Discrete.Classes.cs:

// now
$"{Assumption.Stringize(Assumption.Priority <= Priority)} implies {Conclusion.Stringize(Conclusion.Priority < Priority)}"
// should be
$"{Assumption.Stringize(Assumption.Priority < Priority)} implies {Conclusion.Stringize(Conclusion.Priority <= Priority)}"

which is what Minusf, Divf and Modf next door already do for the same reason — left-associative
and not associative, so an equal-priority right operand needs the brackets. Latexize should be
checked at the same time; CSharpMath.Evaluation reads LaTeX back (#822).

Deliberately not the same case: Sumf, Mulf, Andf, Orf and Xorf also drop a right nesting —
1 + (2 + 3) prints as 1 + 2 + 3 and reads back as (1 + 2) + 3 — but those operators are
associative, so the value survives and only the node shape changes. That is a defensible choice about
output; implies is not one.

StringizeRoundTripsToTheSameNode in EveryNodeSurvivesEveryPipelineTest misses all of it because
every sample it builds is one level deep. A right-nested sample per binary node type would catch this
class, and is worth adding whatever is decided about the associative ones.

Found while measuring what the printed form does carry, for #323 / #1031.

Activity

  1. Rafael-SOWNet commented on Aug 23, 2026

    @Rafael-SOWNet
    MemberAuthor

    Already fixed, and by more than this asked for: #1009 landed on master hours before this was filed
    and brackets five operators, not two. Measured on master at ebf8475:

    "a implies (b implies c)"     -> "a implies (b implies c)"          round trips
    "a -> (b -> c)"               -> "a implies (b implies c)"          round trips
    "(a provided b) provided c"   -> "(a provided b) provided c"        round trips
    "{1,2,3} \ ({2,3} \ {3})"     -> "{ 1, 2, 3 } \ ({ 2, 3 } \ { 3 })" round trips
    

    My measurement was taken on v2.3.0 (6b93b40), which was the tip when I branched and was stale by the
    time I wrote this up — the issue tracker was right and I was reading an old build. The associative
    half of what I described is unchanged and is not a defect: 1 + (2 + 3) still prints as 1 + 2 + 3
    and reads back as (1 + 2) + 3, which is the same value written the other way round.

    #1031 now asserts the fixed behaviour instead of the broken one.

  2. added theissue type on Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions