diff --git a/Sources/Tests/UnitTests/Core/Transformations/RuleSetsDoNotCycleTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RuleSetsDoNotCycleTest.cs index bb0d4c976..e0272bd5f 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/RuleSetsDoNotCycleTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/RuleSetsDoNotCycleTest.cs @@ -112,6 +112,60 @@ public void NoRuleSetRewritesBackToATermItHasAlreadyProduced() + string.Join("\n", cycles.Distinct())); } + /// + /// The same question over a whole tree rather than at the root, which is the half the + /// loop above cannot answer: a full pass can move its rewrite to a different node each + /// time and never repeat a term while still never settling. + /// + /// + /// UntilStable reports hitting its bound as no answer rather than as the + /// last value reached, and its remarks say why — "an unbounded rewrite loop is the failure + /// mode this layer is supposed to make visible, and handing back a value from the middle + /// of one would hide exactly the case worth seeing". This is a caller taking it up on + /// that. + /// + [Fact] + public void EveryRegisteredSetSettlesOnTheSeeds() + { + // Named rather than counted, so that finding another one is a change to this list. + // NumericNeat grows `-x / (-y)` by four nodes a pass for ever: the rules that bring a + // negative factor out of a numerator and out of a denominator each leave the + // magnitude of -1 behind as a literal `1 *` factor instead of folding it, and then + // undo each other around it -- + // -1 * x / (-y) + // -1 * 1 * x / (1 * y) * 1 * (-1) + // -1 * 1 * 1 * x / (1 * y) * 1 * 1 * (-1) + // `Simplify` is not affected and answers `x / y`, because the pipeline is bounded and + // picks by size rather than running this set alone to a fixed point. It is a set with + // no fixed point on an ordinary input all the same. + // https://github.com/asc-community/AngouriMath/issues/1167 + var known = new[] { "NumericNeat: -x / (-y)" }; + + var unsettled = new List(); + + foreach (var set in RewriteRules.All) + { + var rewriting = Transformation.Rewriting(set).UntilStable(Steps); + foreach (var start in Corpus()) + { + TransformationResult result; + try { result = rewriting.Apply(start); } + catch { continue; /* a rule declining loudly is not this test's subject */ } + if (!result.Succeeded) + unsettled.Add($"{set.Name}: {start.Stringize()}"); + } + } + + var unexpected = unsettled.Distinct().Except(known).ToList(); + Assert.True(unexpected.Count == 0, + $"{unexpected.Count} set-and-expression pairs newly fail to reach a fixed point " + + $"in {Steps} passes:\n" + string.Join("\n", unexpected.Take(20))); + + // And the known one is still known: fixing it should delete it from the list rather + // than leave a name here that no longer means anything. + Assert.Equal(known, unsettled.Distinct().Intersect(known).ToArray()); + } + /// /// The loop above has to actually rewrite something, or it passes by never firing. Named /// as a count that moves rather than as "it works".