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
28 changes: 18 additions & 10 deletions Sources/Analyzers/RuleRegistryGenerator/RuleRegistryGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,22 @@ private static string Sanitize(string name)
// they are emitted as a method taking the same parameters instead.
var lambda = method.ExpressionBody?.Expression as SimpleLambdaExpressionSyntax;
var inner = lambda?.Body as ExpressionSyntax ?? method.ExpressionBody?.Expression;
ParameterSyntax parameter;
if (lambda is not null)
parameter = lambda.Parameter;
else if (method.ParameterList.Parameters.Count == 1)
parameter = method.ParameterList.Parameters[0];
else
// Which parameter is the expression being rewritten. A factory says so by being a
// lambda; a switch says so by naming it; and a method with one parameter has only the
// one to offer. A switch that takes a second parameter — a sort level, say — is read
// by asking the switch rather than by counting, which is the whole of this.
ParameterSyntax? parameter =
lambda is not null ? lambda.Parameter
: inner is SwitchExpressionSyntax { GoverningExpression: IdentifierNameSyntax named }
? method.ParameterList.Parameters.FirstOrDefault(p => p.Identifier.Text == named.Identifier.Text)
: method.ParameterList.Parameters.Count == 1 ? method.ParameterList.Parameters[0]
: null;
if (parameter is null)
return null;
var subject = parameter.Identifier.Text;
// Everything else the method takes is closed over by the arms, so it decides whether
// they can be a field at all.
var captured = method.ParameterList.Parameters.Where(p => p != parameter).ToList();
// A lambda parameter is written without its type, and the arms are copied into a
// context where nothing infers it, so it is named rather than echoed.
var parameterType = parameter.Type?.ToString() ?? "global::AngouriMath.Entity";
Expand All @@ -200,7 +208,7 @@ private static string Sanitize(string name)
text.AppendLine($"{indent}/// The arms of <see cref=\"{method.Identifier.Text}\"/>, each one addressable on its own.");
text.AppendLine($"{indent}/// Generated from the <c>switch</c> itself, so the two cannot disagree.");
text.AppendLine($"{indent}/// </summary>");
if (lambda is null)
if (captured.Count == 0)
{
text.AppendLine($"{indent}[global::AngouriMath.Core.ConstantField]");
text.AppendLine($"{indent}internal static readonly global::System.Collections.Generic.IReadOnlyList"
Expand All @@ -209,7 +217,7 @@ private static string Sanitize(string name)
else
text.AppendLine($"{indent}internal static global::System.Collections.Generic.IReadOnlyList"
+ $"<global::AngouriMath.Core.Transformations.RewriteRule> {method.Identifier.Text}Arms"
+ $"{method.ParameterList} =>");
+ $"({string.Join(", ", captured)}) =>");
text.AppendLine($"{indent} new global::AngouriMath.Core.Transformations.RewriteRule[]");
text.AppendLine($"{indent} {{");

Expand Down Expand Up @@ -240,8 +248,8 @@ private static string Sanitize(string name)
text.AppendLine($"{indent} replacementSource: {Literal(replacement)},");
text.AppendLine($"{indent} growth: global::AngouriMath.Core.Transformations.RewriteRuleGrowth.{growth},");
text.AppendLine($"{indent} sourceLine: {line},");
// A factory's arm reads the factory's parameters, so its lambda cannot be static.
text.AppendLine($"{indent} apply: {(lambda is null ? "static " : "")}global::AngouriMath.Entity? "
// An arm that reads a captured parameter cannot be a static lambda.
text.AppendLine($"{indent} apply: {(captured.Count == 0 ? "static " : "")}global::AngouriMath.Entity? "
+ $"({parameterType} {subject}) => {arm.Apply}),");
}

Expand Down
14 changes: 9 additions & 5 deletions Sources/AngouriMath/Core/Transformations/RewriteRules.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
Expand Down Expand Up @@ -205,7 +205,8 @@ public static class RewriteRules
"Collapses nested quotients into a single numerator over a single denominator.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
Patterns.CollapseMultipleFractions);
Patterns.CollapseMultipleFractions,
Patterns.CollapseMultipleFractionsArms);

/// <summary>
/// Puts a sum of quotients over one denominator, grouping the terms by variables and
Expand All @@ -216,7 +217,8 @@ public static class RewriteRules
"Adds quotients by putting them over a common denominator.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.HIGH_LEVEL));
expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.HIGH_LEVEL),
Patterns.FractionCommonDenominatorRulesArms(TreeAnalyzer.SortLevel.HIGH_LEVEL));

/// <summary>
/// <see cref="CommonDenominator"/>, counting constants when it groups terms.
Expand All @@ -226,7 +228,8 @@ public static class RewriteRules
"Adds quotients over a common denominator, distinguishing terms by their constants too.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.MIDDLE_LEVEL));
expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.MIDDLE_LEVEL),
Patterns.FractionCommonDenominatorRulesArms(TreeAnalyzer.SortLevel.MIDDLE_LEVEL));

/// <summary>
/// <see cref="CommonDenominator"/>, grouping terms by the whole subtree.
Expand All @@ -236,7 +239,8 @@ public static class RewriteRules
"Adds quotients over a common denominator, grouping terms by the whole subtree.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.LOW_LEVEL));
expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.LOW_LEVEL),
Patterns.FractionCommonDenominatorRulesArms(TreeAnalyzer.SortLevel.LOW_LEVEL));

/// <summary>
/// Divides one polynomial by another, leaving a quotient plus a remainder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ private static IEnumerable<Entity> PairwiseGrouping(Entity num, Entity den, Tree
return factors.Values;
}

[AddressableRules]
internal static Entity FractionCommonDenominatorRules(Entity expr, TreeAnalyzer.SortLevel level)
=> expr switch
{
Expand Down Expand Up @@ -160,6 +161,7 @@ internal static Entity RationalizeDenominator(Entity expr)
return ((num * conjugate) / divisor).InnerSimplified;
}

[AddressableRules]
internal static Entity CollapseMultipleFractions(Entity expr)
=> expr switch
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
Expand Down Expand Up @@ -67,6 +67,16 @@ public sealed class AddressableRulesTest
yield return (RewriteRules.InvertNegativePowers, Patterns.InvertNegativePowers);
yield return (RewriteRules.PolynomialLongDivision, Patterns.PolynomialLongDivision);
yield return (RewriteRules.PolynomialGcdCancellation, Patterns.PolynomialGcdCancellation);
// A switch that takes a second parameter: one switch, three sets, differing only in
// the sort level it is closed over.
yield return (RewriteRules.CommonDenominator,
expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.HIGH_LEVEL));
yield return (RewriteRules.CommonDenominatorCountingConstants,
expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.MIDDLE_LEVEL));
yield return (RewriteRules.CommonDenominatorExact,
expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.LOW_LEVEL));
// A shape that was already readable and simply not marked.
yield return (RewriteRules.CollapseMultipleFractions, Patterns.CollapseMultipleFractions);
}

public static IEnumerable<object[]> AddressableSets()
Expand Down Expand Up @@ -262,27 +272,23 @@ public void TheRegistryIsAddressableAsFarAsItSaysItIs()
var without = RewriteRules.All.Where(set => set.Rules.Count == 0)
.Select(set => set.Name).OrderBy(name => name, StringComparer.Ordinal).ToList();

// What is left is not one kind of thing, and saying so would be wrong: of these eight,
// only RationalizeDenominator, ExpandFactorialDivisions and
// FactorizeFactorialMultiplications are methods with branches and locals. The three
// CommonDenominator sets are a switch that takes a second parameter, which the
// generator does not read yet; CollapseMultipleFractions is an ordinary one-parameter
// switch and PerfectSquare a single `is` pattern, both of which it reads today and
// neither of which is marked. So this list is a list, not a category.
// Three of these four are a method with a statement body, branches and locals, and no
// arms to read. PerfectSquare is not: it is a single `is` pattern that the generator
// reads perfectly well, and it is left out on cost rather than on shape. Replaying one
// rule against its switch over this corpus takes 5m10s, because deciding whether the
// cross term matches goes through Simplify -- as long as the entire rest of the suite.
// Worth doing, worth deciding on its own, and not worth smuggling in beside four sets
// that cost eight seconds between them.
Assert.Equal(new[]
{
"CollapseMultipleFractions",
"CommonDenominator",
"CommonDenominatorCountingConstants",
"CommonDenominatorExact",
"ExpandFactorialDivisions",
"FactorizeFactorialMultiplications",
"PerfectSquare",
"RationalizeDenominator",
}, without);

Assert.Equal(22, withRules.Count);
Assert.Equal(371, withRules.Sum(set => set.Rules.Count));
Assert.Equal(26, withRules.Count);
Assert.Equal(388, withRules.Sum(set => set.Rules.Count));
}

[Fact]
Expand Down Expand Up @@ -357,16 +363,16 @@ public void ASetWithNoAddressableRulesStillRecordsItsStep()
{
// Stated rather than assumed: if this set ever becomes addressable the test would
// otherwise keep passing while testing nothing at all. It has already happened twice
// -- CanonicalOrderExact stood here, then InvertNegativePowers -- so the assertion is
// load-bearing rather than decorative.
Assert.Empty(RewriteRules.CollapseMultipleFractions.Rules);
// -- CanonicalOrderExact, then InvertNegativePowers, then CollapseMultipleFractions --
// so the assertion is load-bearing rather than decorative.
Assert.Empty(RewriteRules.RationalizeDenominator.Rules);

using var recording = RewriteRecording.Start();
RewriteRules.CollapseMultipleFractions.ApplyOnce("x / y / z".ToEntity());
RewriteRules.RationalizeDenominator.ApplyOnce("1 / (3 - sqrt(5))".ToEntity());
recording.Dispose();

var step = Assert.Single(recording.Steps);
Assert.Equal(RewriteRules.CollapseMultipleFractions, step.RuleSet);
Assert.Equal(RewriteRules.RationalizeDenominator, step.RuleSet);
Assert.Null(step.Rule);
}
}
Expand Down