diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 84ecb4a0c..eff7e1b3e 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -66,3 +66,6 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Utils/Utils/CiTiming.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
+
+[Analyzers/RuleRegistryGenerator/*.cs]
+file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
diff --git a/Sources/Analyzers/RuleRegistryGenerator/RuleRegistryGenerator.cs b/Sources/Analyzers/RuleRegistryGenerator/RuleRegistryGenerator.cs
new file mode 100644
index 000000000..3f1200093
--- /dev/null
+++ b/Sources/Analyzers/RuleRegistryGenerator/RuleRegistryGenerator.cs
@@ -0,0 +1,335 @@
+//
+// Copyright (c) 2019-2026 Angouri.
+// AngouriMath is licensed under MIT.
+// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
+// Website: https://am.angouri.org.
+//
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace AngouriMath.Generators
+{
+ ///
+ /// Turns each arm of a rewrite rule switch into an addressable value, without the
+ /// switch ceasing to be the thing a human edits.
+ ///
+ ///
+ ///
+ /// This is what https://github.com/asc-community/AngouriMath/issues/825 settled on. The two
+ /// alternatives both fail on something measured: transcribing forty arms by hand is forty
+ /// chances to change a pattern silently, and expressing them through the runtime matcher costs
+ /// about five percent of Simplify per rule set exchanged. A generator over the arms
+ /// costs nothing at run time, because the method it reads is left exactly as it was and keeps
+ /// being what the simplifier calls.
+ ///
+ ///
+ /// The arm is copied as syntax rather than re-derived, so the generated rule cannot say
+ /// something the arm does not. What is derived is only what a reader would otherwise have to
+ /// count for themselves: the node type at the root of the pattern, and whether the rewrite
+ /// grows, shrinks or rearranges.
+ ///
+ ///
+ [Generator(LanguageNames.CSharp)]
+ public sealed class RuleRegistryGenerator : IIncrementalGenerator
+ {
+ private const string AttributeName = "AddressableRules";
+
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var files = context.SyntaxProvider.CreateSyntaxProvider(
+ predicate: static (node, _) => IsCandidate(node),
+ transform: static (ctx, _) => Describe((MethodDeclarationSyntax)ctx.Node))
+ .Where(static file => file is not null);
+
+ // The transform produces the finished text, so what travels through the pipeline is a
+ // pair of strings. Anything holding a syntax node or a symbol would keep a compilation
+ // alive and compare unequal on every keystroke.
+ context.RegisterSourceOutput(files, static (spc, file) =>
+ spc.AddSource(file!.HintName, SourceText.From(file.Source, Encoding.UTF8)));
+ }
+
+ private static bool IsCandidate(SyntaxNode node)
+ => node is MethodDeclarationSyntax method
+ && method.AttributeLists
+ .SelectMany(list => list.Attributes)
+ .Any(attribute => NameOf(attribute) is AttributeName or AttributeName + "Attribute");
+
+ private static string NameOf(AttributeSyntax attribute)
+ => attribute.Name switch
+ {
+ QualifiedNameSyntax qualified => qualified.Right.Identifier.Text,
+ SimpleNameSyntax simple => simple.Identifier.Text,
+ _ => attribute.Name.ToString()
+ };
+
+ private sealed class GeneratedFile
+ {
+ internal GeneratedFile(string hintName, string source)
+ => (HintName, Source) = (hintName, source);
+
+ internal string HintName { get; }
+ internal string Source { get; }
+ }
+
+ private static GeneratedFile? Describe(MethodDeclarationSyntax method)
+ {
+ var unit = method.FirstAncestorOrSelf();
+ var owner = method.FirstAncestorOrSelf();
+ if (unit is null || owner is null)
+ return null;
+
+ var containers = new List();
+ for (var at = owner; at is not null; at = at.Parent as TypeDeclarationSyntax)
+ containers.Insert(0, at);
+
+ var namespaceName = NamespaceOf(method);
+ var hint = Sanitize(namespaceName + "." + string.Join(".", containers.Select(c => c.Identifier.Text))
+ + "." + method.Identifier.Text) + ".Rules.g.cs";
+
+ var text = new StringBuilder();
+ text.AppendLine("// ");
+ text.AppendLine("#nullable enable");
+ // The arms are copied verbatim, so they need the names the file they came from had in
+ // scope. Carrying its using directives over is what makes that true, and a directive
+ // the compilation already has globally is then written twice.
+ text.AppendLine("#pragma warning disable CS0105 // duplicate using directive");
+ text.AppendLine("#pragma warning disable CS8933 // using directive appeared previously as global using");
+ foreach (var directive in unit.Usings)
+ text.AppendLine(directive.ToString());
+ foreach (var declared in method.Ancestors().OfType())
+ foreach (var directive in declared.Usings)
+ text.AppendLine(directive.ToString());
+ text.AppendLine();
+
+ var indent = "";
+ if (namespaceName.Length > 0)
+ {
+ text.AppendLine($"namespace {namespaceName}");
+ text.AppendLine("{");
+ indent = " ";
+ }
+ foreach (var container in containers)
+ {
+ text.AppendLine($"{indent}{Modifiers(container)} partial {container.Keyword.Text} {container.Identifier.Text}");
+ text.AppendLine($"{indent}{{");
+ indent += " ";
+ }
+
+ var body = Body(method, indent);
+ if (body is null)
+ {
+ // A `#error` rather than a reported diagnostic: it cannot be turned off by a
+ // severity setting, which matters because the failure mode being guarded against
+ // is a rule set silently having no rules rather than a rule being wrong.
+ text.AppendLine($"{indent}#error AddressableRules: '{method.Identifier.Text}' must be an "
+ + "expression-bodied method whose body is `parameter switch { ... }`.");
+ }
+ else
+ text.Append(body);
+
+ foreach (var _ in containers)
+ {
+ indent = indent.Substring(4);
+ text.AppendLine($"{indent}}}");
+ }
+ if (namespaceName.Length > 0)
+ text.AppendLine("}");
+
+ return new GeneratedFile(hint, text.ToString());
+ }
+
+ private static string Modifiers(TypeDeclarationSyntax type)
+ => string.Join(" ", type.Modifiers.Where(m => !m.IsKind(SyntaxKind.PartialKeyword)).Select(m => m.Text));
+
+ private static string NamespaceOf(SyntaxNode node)
+ {
+ var names = node.Ancestors().OfType()
+ .Select(declared => declared.Name.ToString()).Reverse();
+ return string.Join(".", names);
+ }
+
+ private static string Sanitize(string name)
+ {
+ var builder = new StringBuilder(name.Length);
+ foreach (var c in name)
+ builder.Append(char.IsLetterOrDigit(c) || c == '_' ? c : '.');
+ return builder.ToString();
+ }
+
+ private static string? Body(MethodDeclarationSyntax method, string indent)
+ {
+ if (method.ExpressionBody?.Expression is not SwitchExpressionSyntax dispatch)
+ return null;
+ if (dispatch.GoverningExpression is not IdentifierNameSyntax governing)
+ return null;
+ if (method.ParameterList.Parameters.Count != 1)
+ return null;
+ var parameter = method.ParameterList.Parameters[0];
+ if (parameter.Identifier.Text != governing.Identifier.Text)
+ return null;
+
+ var arms = dispatch.Arms.Where(arm => arm.Pattern is not DiscardPatternSyntax).ToList();
+ var names = new Dictionary(StringComparer.Ordinal);
+
+ var text = new StringBuilder();
+ text.AppendLine($"{indent}/// ");
+ text.AppendLine($"{indent}/// The arms of , each one addressable on its own.");
+ text.AppendLine($"{indent}/// Generated from the switch itself, so the two cannot disagree.");
+ text.AppendLine($"{indent}/// ");
+ text.AppendLine($"{indent}[global::AngouriMath.Core.ConstantField]");
+ text.AppendLine($"{indent}internal static readonly global::System.Collections.Generic.IReadOnlyList"
+ + $" {method.Identifier.Text}Arms =");
+ text.AppendLine($"{indent} new global::AngouriMath.Core.Transformations.RewriteRule[]");
+ text.AppendLine($"{indent} {{");
+
+ for (var index = 0; index < arms.Count; index++)
+ {
+ var arm = arms[index];
+ var pattern = Flatten(arm.Pattern.ToString());
+ var guard = arm.WhenClause is null ? null : Flatten(arm.WhenClause.Condition.ToString());
+ var replacement = Flatten(arm.Expression.ToString());
+ var key = guard is null ? pattern : pattern + " when " + guard;
+ names.TryGetValue(key, out var seen);
+ names[key] = seen + 1;
+ var name = seen == 0 ? key : $"{key} #{seen + 1}";
+
+ var line = arm.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
+ var nodeTypes = RootTypesOf(arm.Pattern);
+ var growth = Growth(arm.Pattern, arm.Expression);
+
+ text.AppendLine($"{indent} new global::AngouriMath.Core.Transformations.RewriteRule(");
+ text.AppendLine($"{indent} source: \"{method.Identifier.Text}\",");
+ text.AppendLine($"{indent} index: {index},");
+ text.AppendLine($"{indent} name: {Literal(name)},");
+ text.AppendLine($"{indent} description: {Literal(Description(arm))},");
+ text.AppendLine($"{indent} nodeTypes: new global::System.Type[] {{ "
+ + string.Join(", ", nodeTypes.Select(type => $"typeof({type})")) + " },");
+ text.AppendLine($"{indent} patternSource: {Literal(pattern)},");
+ text.AppendLine($"{indent} guardSource: {Literal(guard)},");
+ text.AppendLine($"{indent} replacementSource: {Literal(replacement)},");
+ text.AppendLine($"{indent} growth: global::AngouriMath.Core.Transformations.RewriteRuleGrowth.{growth},");
+ text.AppendLine($"{indent} sourceLine: {line},");
+ text.AppendLine($"{indent} apply: static global::AngouriMath.Entity? "
+ + $"({parameter.Type} {parameter.Identifier.Text}) => {parameter.Identifier.Text} switch");
+ text.AppendLine($"{indent} {{");
+ text.AppendLine($"{indent} {arm.Pattern}{(arm.WhenClause is null ? "" : " " + arm.WhenClause)} => {arm.Expression},");
+ text.AppendLine($"{indent} _ => null");
+ text.AppendLine($"{indent} }}),");
+ }
+
+ text.AppendLine($"{indent} }};");
+ return text.ToString();
+ }
+
+ /// The comment written above an arm, which is the rule stated as mathematics.
+ private static string? Description(SwitchExpressionArmSyntax arm)
+ {
+ var lines = arm.GetLeadingTrivia()
+ .Where(trivia => trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
+ .Select(trivia => trivia.ToString().TrimStart('/').Trim())
+ .Where(line => line.Length > 0)
+ .ToList();
+ return lines.Count == 0 ? null : string.Join(" ", lines);
+ }
+
+ ///
+ /// The node types the pattern admits at its root. Usually one; two where the arm is an
+ /// or of node types, and none where the constraint cannot be read off the syntax.
+ ///
+ ///
+ /// The list is a necessary condition and has to stay one, because the point of
+ /// recording it is that a scheduler may skip the rule on any other type. So an
+ /// unrecognised shape yields nothing rather than a guess, and an and takes one
+ /// side's constraint — both hold, so either is necessary — while an or has to take
+ /// both or neither.
+ ///
+ private static IReadOnlyList RootTypesOf(PatternSyntax pattern)
+ {
+ switch (pattern)
+ {
+ case RecursivePatternSyntax { Type: { } type }:
+ return new[] { type.ToString() };
+ case DeclarationPatternSyntax declaration:
+ return new[] { declaration.Type.ToString() };
+ case TypePatternSyntax typed:
+ return new[] { typed.Type.ToString() };
+ case ParenthesizedPatternSyntax parenthesized:
+ return RootTypesOf(parenthesized.Pattern);
+ case BinaryPatternSyntax binary when binary.IsKind(SyntaxKind.OrPattern):
+ {
+ var left = RootTypesOf(binary.Left);
+ var right = RootTypesOf(binary.Right);
+ return left.Count == 0 || right.Count == 0
+ ? Array.Empty()
+ : left.Concat(right).Distinct().ToArray();
+ }
+ case BinaryPatternSyntax binary when binary.IsKind(SyntaxKind.AndPattern):
+ {
+ var left = RootTypesOf(binary.Left);
+ return left.Count > 0 ? left : RootTypesOf(binary.Right);
+ }
+ default:
+ return Array.Empty();
+ }
+ }
+
+ ///
+ /// Whether the rewrite makes the expression bigger, smaller or the same size — counted as
+ /// operators plus operands on each side.
+ ///
+ ///
+ /// A syntactic proxy, and deliberately a crude one: it counts what is written rather than
+ /// what the expression evaluates to, so a rule whose replacement calls a helper is counted
+ /// as the call. It is enough for the question a saturation scheduler asks, which is which
+ /// pairs of rules are each other's inverse.
+ ///
+ private static string Growth(PatternSyntax pattern, ExpressionSyntax replacement)
+ {
+ var before = Size(pattern);
+ var after = Size(replacement);
+ return after > before ? "Expands" : after < before ? "Collects" : "Rearranges";
+ }
+
+ private static int Size(PatternSyntax pattern)
+ => pattern switch
+ {
+ RecursivePatternSyntax recursive =>
+ 1 + (recursive.PositionalPatternClause?.Subpatterns.Sum(sub => Size(sub.Pattern)) ?? 0)
+ + (recursive.PropertyPatternClause?.Subpatterns.Sum(sub => Size(sub.Pattern)) ?? 0),
+ ParenthesizedPatternSyntax parenthesized => Size(parenthesized.Pattern),
+ BinaryPatternSyntax binary => Size(binary.Left) + Size(binary.Right),
+ UnaryPatternSyntax unary => Size(unary.Pattern),
+ _ => 1
+ };
+
+ private static int Size(ExpressionSyntax expression)
+ => expression switch
+ {
+ ParenthesizedExpressionSyntax parenthesized => Size(parenthesized.Expression),
+ BinaryExpressionSyntax binary => 1 + Size(binary.Left) + Size(binary.Right),
+ PrefixUnaryExpressionSyntax prefix => 1 + Size(prefix.Operand),
+ CastExpressionSyntax cast => Size(cast.Expression),
+ InvocationExpressionSyntax invocation =>
+ 1 + invocation.ArgumentList.Arguments.Sum(argument => Size(argument.Expression))
+ + (invocation.Expression is MemberAccessExpressionSyntax access ? Size(access.Expression) : 0),
+ ObjectCreationExpressionSyntax creation =>
+ 1 + (creation.ArgumentList?.Arguments.Sum(argument => Size(argument.Expression)) ?? 0),
+ MemberAccessExpressionSyntax access => 1 + Size(access.Expression),
+ _ => 1
+ };
+
+ private static string Flatten(string text)
+ => string.Join(" ", text.Split(new[] { '\r', '\n', '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries));
+
+ private static string Literal(string? text)
+ => text is null ? "null" : "\"" + text.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
+ }
+}
diff --git a/Sources/Analyzers/RuleRegistryGenerator/RuleRegistryGenerator.csproj b/Sources/Analyzers/RuleRegistryGenerator/RuleRegistryGenerator.csproj
new file mode 100644
index 000000000..33506ce62
--- /dev/null
+++ b/Sources/Analyzers/RuleRegistryGenerator/RuleRegistryGenerator.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0
+ false
+ true
+
+
+ *$(MSBuildProjectFullPath)*
+
+
+
+
+
+
+
+
diff --git a/Sources/AngouriMath.sln b/Sources/AngouriMath.sln
index e983a6a63..2827231b4 100644
--- a/Sources/AngouriMath.sln
+++ b/Sources/AngouriMath.sln
@@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Analyzers.CodeFixes", "Anal
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Analyzers.Debug", "Analyzers\Analyzers.Debug\Analyzers.Debug.csproj", "{7C7DE223-D688-E7C3-1021-C86827910720}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RuleRegistryGenerator", "Analyzers\RuleRegistryGenerator\RuleRegistryGenerator.csproj", "{1E2C70F3-EB0F-46AC-8D12-293243D4C135}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AngouriMathPlot", "Samples\AngouriMathPlot\AngouriMathPlot.csproj", "{4B2B48EE-B454-A3AC-5534-63E894592140}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphicExample", "Samples\GraphicExample\GraphicExample.csproj", "{13409571-0ADC-B9C6-2EE7-44DF0B4AD43C}"
@@ -161,6 +163,10 @@ Global
{40BCFEFF-F691-B86D-DEF7-1B3D75C72011}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40BCFEFF-F691-B86D-DEF7-1B3D75C72011}.Release|Any CPU.ActiveCfg = Release|Any CPU
{40BCFEFF-F691-B86D-DEF7-1B3D75C72011}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1E2C70F3-EB0F-46AC-8D12-293243D4C135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1E2C70F3-EB0F-46AC-8D12-293243D4C135}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1E2C70F3-EB0F-46AC-8D12-293243D4C135}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1E2C70F3-EB0F-46AC-8D12-293243D4C135}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -188,5 +194,6 @@ Global
{FD30A551-F0D8-071A-9356-0BF0CDFB2FCC} = {D8022D2F-1DCE-4CE1-98DF-70B00D719D59}
{ABAD0619-30C7-242D-670F-2ECC908074A2} = {D8022D2F-1DCE-4CE1-98DF-70B00D719D59}
{40BCFEFF-F691-B86D-DEF7-1B3D75C72011} = {D8022D2F-1DCE-4CE1-98DF-70B00D719D59}
+ {1E2C70F3-EB0F-46AC-8D12-293243D4C135} = {358BFC2B-1A5B-4740-8BA1-0E53EFC7EDAC}
EndGlobalSection
EndGlobal
diff --git a/Sources/AngouriMath/AngouriMath.csproj b/Sources/AngouriMath/AngouriMath.csproj
index c851c7e03..dcc02ce36 100644
--- a/Sources/AngouriMath/AngouriMath.csproj
+++ b/Sources/AngouriMath/AngouriMath.csproj
@@ -49,6 +49,13 @@
+
+
+
diff --git a/Sources/AngouriMath/Core/CoreAttributes.cs b/Sources/AngouriMath/Core/CoreAttributes.cs
index 674e49e55..5f8c7bf09 100644
--- a/Sources/AngouriMath/Core/CoreAttributes.cs
+++ b/Sources/AngouriMath/Core/CoreAttributes.cs
@@ -22,4 +22,17 @@ internal sealed class ConstantFieldAttribute : Attribute { }
///
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
internal sealed class ConcurrentFieldAttribute : Attribute { }
+
+ ///
+ /// Marks a rewrite rule switch whose arms are to be generated as individually
+ /// addressable values, in a field named after the
+ /// method with Arms appended.
+ ///
+ ///
+ /// The method must be expression-bodied with a body of the form parameter switch { ... };
+ /// anything else is a build error rather than an empty list, since a rule set that silently
+ /// has no rules reads exactly like one that has been checked.
+ ///
+ [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
+ internal sealed class AddressableRulesAttribute : Attribute { }
}
diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRecording.cs b/Sources/AngouriMath/Core/Transformations/RewriteRecording.cs
index 58bd9a528..de67ccfea 100644
--- a/Sources/AngouriMath/Core/Transformations/RewriteRecording.cs
+++ b/Sources/AngouriMath/Core/Transformations/RewriteRecording.cs
@@ -127,11 +127,11 @@ public void Dispose()
///
internal static RewriteRecording? Current => current.Value;
- internal void Add(RewriteRuleSet ruleSet, Entity before, Entity after)
+ internal void Add(RewriteRuleSet ruleSet, RewriteRule? rule, Entity before, Entity after)
{
if (closed)
return;
- steps.Enqueue(new RewriteStep(ruleSet, before, after));
+ steps.Enqueue(new RewriteStep(ruleSet, rule, before, after));
}
}
}
diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRule.cs b/Sources/AngouriMath/Core/Transformations/RewriteRule.cs
new file mode 100644
index 000000000..47d0d9f7f
--- /dev/null
+++ b/Sources/AngouriMath/Core/Transformations/RewriteRule.cs
@@ -0,0 +1,195 @@
+//
+// Copyright (c) 2019-2026 Angouri.
+// AngouriMath is licensed under MIT.
+// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
+// Website: https://am.angouri.org.
+//
+
+using System;
+using System.Collections.Generic;
+
+namespace AngouriMath.Core.Transformations
+{
+ ///
+ /// Which way a rewrite moves: does it make the expression bigger, smaller, or neither.
+ ///
+ ///
+ ///
+ /// Counted from what the rule is written as — operators plus operands on the pattern side
+ /// against operators plus operands on the replacement side — and therefore a statement about
+ /// the rule, not about any particular expression it fires on.
+ ///
+ ///
+ /// It exists because a rewrite graph needs it and a rewrite pipeline does not.
+ /// applies a set, keeps a candidate and moves on, so an
+ /// expanding rule and a collecting one never meet: the order they run in decides which wins.
+ /// Equality saturation deletes that order and keeps both results, so it has to be told which
+ /// pairs undo each other or it will grow without bound —
+ /// #746 tier 2, measured
+ /// in the egraph harness at up to 7,143 times the e-nodes when it is not told.
+ ///
+ ///
+ public enum RewriteRuleGrowth
+ {
+ /// The replacement is written with fewer operators and operands than the pattern.
+ Collects,
+
+ /// The two are written with the same number, so the rule moves things about.
+ Rearranges,
+
+ /// The replacement is written with more, so the rule opens the expression out.
+ Expands
+ }
+
+ ///
+ /// One rewrite, addressable on its own: what it matches, what it puts there instead, where it
+ /// is written and which way it moves.
+ ///
+ ///
+ ///
+ /// A is the unit the library applies; this is the unit inside it.
+ /// The distinction is what
+ /// #28 asks for — a
+ /// derivation that says which rewrite fired rather than which group of them — and what
+ /// #825 is about.
+ ///
+ ///
+ /// These are generated from the switch that defines them, arm by arm, rather
+ /// than written out a second time. That is deliberate and it is the whole design: the
+ /// switch stays the thing a human edits and the thing the simplifier calls, so nothing
+ /// on the hot path changes and the two forms cannot drift apart. Transcribing forty arms into
+ /// forty objects by hand would be forty chances to alter a pattern silently, and expressing
+ /// them through the runtime matcher in
+ /// AngouriMath.Core.Transformations.Matching was measured at about five percent of
+ /// per rule set exchanged.
+ ///
+ ///
+ /// What is not here. A per-rule . A rule's tier is a claim
+ /// somebody has to argue for, and there is no honest way to derive one from syntax — so
+ /// remains the declared tier and this type does not
+ /// invent a finer one it cannot justify. What being addressable buys is that the finer tier
+ /// now has somewhere to live once the argument is made, which it did not before.
+ ///
+ ///
+ public sealed class RewriteRule
+ {
+ internal RewriteRule(
+ string source,
+ int index,
+ string name,
+ string? description,
+ IReadOnlyList nodeTypes,
+ string patternSource,
+ string? guardSource,
+ string replacementSource,
+ RewriteRuleGrowth growth,
+ int sourceLine,
+ Func apply)
+ {
+ Source = source;
+ Index = index;
+ Name = name;
+ Description = description;
+ NodeTypes = nodeTypes;
+ PatternSource = patternSource;
+ GuardSource = guardSource;
+ ReplacementSource = replacementSource;
+ Growth = growth;
+ SourceLine = sourceLine;
+ this.apply = apply;
+ }
+
+ private readonly Func apply;
+
+ /// The method whose switch this arm belongs to.
+ public string Source { get; }
+
+ /// Where it sits among that method's arms, which is the order it is tried in.
+ ///
+ /// First match wins, so a rule's index is part of what it does: two rules that can both
+ /// fire on one node are resolved by this and nothing else.
+ ///
+ public int Index { get; }
+
+ ///
+ /// What to call this rule in a report, a test or a bug — the pattern it matches, written
+ /// as the source writes it.
+ ///
+ ///
+ /// The pattern rather than the position, because a position moves whenever an arm is
+ /// inserted above it and the point of a name is to survive that. Where a set really does
+ /// write one pattern twice, the later ones are suffixed #2, #3 — and a set
+ /// that does is worth looking at, since the second is unreachable.
+ ///
+ public string Name { get; }
+
+ /// The comment written above the rule, where there is one: the identity in the notation a mathematician would use.
+ public string? Description { get; }
+
+ ///
+ /// The node types the rule can fire on — usually one, occasionally two, and empty where
+ /// the pattern's shape does not say.
+ ///
+ ///
+ /// A necessary condition, not a sufficient one: a node of one of these types may
+ /// still fail the rest of the pattern. That is the direction that makes it useful, since
+ /// what it licenses is skipping the rule on every other type — which is the dispatch a
+ /// large switch over distinct node types gets from the compiler for free and a
+ /// list of rules has to be told.
+ ///
+ public IReadOnlyList NodeTypes { get; }
+
+ /// The pattern the arm matches, as the C# source writes it.
+ ///
+ ///
+ /// Source text, and named so. This is what a reader would see in the
+ /// switch, not a representation anything can match against — to ask whether this
+ /// rule fires on a node, call , which runs the arm itself.
+ ///
+ ///
+ /// The name carries Source because
+ /// #746 tier 1 is
+ /// pattern matching as data, and when a pattern becomes a value it should be able to be
+ /// called Pattern without first breaking somebody. Deciding that after the
+ /// property shipped would have cost a major version; deciding it here cost nothing.
+ ///
+ ///
+ public string PatternSource { get; }
+
+ ///
+ /// The side condition as the C# source writes it, or where the arm
+ /// has no when clause. Source text — see .
+ ///
+ public string? GuardSource { get; }
+
+ ///
+ /// What the arm builds, as the C# source writes it. Source text — see
+ /// .
+ ///
+ public string ReplacementSource { get; }
+
+ /// Which way the rewrite moves. See .
+ public RewriteRuleGrowth Growth { get; }
+
+ /// The line of the source file the arm is written on.
+ public int SourceLine { get; }
+
+ ///
+ /// This one rule at this one node, ignoring its children and every other rule — or
+ /// where it does not apply.
+ ///
+ ///
+ /// Null means "this rule does not fire here", which is a different claim from the rule
+ /// set's handing back the expression it was
+ /// given. The set has to return something; a rule may decline.
+ ///
+ public Entity? TryApply(Entity node)
+ => node is null ? throw new ArgumentNullException(nameof(node)) : apply(node);
+
+ ///
+ public override string ToString()
+ => GuardSource is null
+ ? $"{PatternSource} => {ReplacementSource}"
+ : $"{PatternSource} when {GuardSource} => {ReplacementSource}";
+ }
+}
diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRuleSet.cs b/Sources/AngouriMath/Core/Transformations/RewriteRuleSet.cs
index f00b2ca69..d7c8859e9 100644
--- a/Sources/AngouriMath/Core/Transformations/RewriteRuleSet.cs
+++ b/Sources/AngouriMath/Core/Transformations/RewriteRuleSet.cs
@@ -17,31 +17,21 @@ namespace AngouriMath.Core.Transformations
///
///
///
- /// The set, rather than the single pattern -> replacement line, is the unit
- /// here because that is what has been built so far — not because the finer grain
- /// is too expensive. It was asserted here that it would be, on the grounds that each
- /// rule would cost a delegate call per node; measuring it found the opposite, and the
- /// measurement is in
- /// #825: rules
- /// bucketed by the node type they match run as fast as the hand-written switch
- /// at realistic set sizes and faster at small ones, because a large switch over
- /// distinct node types is compiled into that same dispatch anyway.
- ///
- ///
- /// What actually stands in the way is transcription: splitting forty switch arms
- /// by hand is forty chances to change a pattern silently. That points at a source
- /// generator over the existing bodies rather than at leaving the rewrites unnamed. See
- /// #746 item 50,
- /// and note that nothing here forecloses it: a set whose rewrites become individually
- /// addressable keeps the same name and the same entry in the registry.
+ /// The set is the unit the library applies; is the unit inside
+ /// it. Those rules are generated from the switch that defines the set rather than
+ /// written out again, so the switch stays the thing a human edits and the thing
+ /// this calls, and the two cannot drift apart. See
+ /// #825 and
+ /// #746 item 50.
///
///
public sealed class RewriteRuleSet
{
private readonly Func rules;
- internal RewriteRuleSet(string name, string description, TransformationRelation relation, Soundness soundness, Func rules)
- => (Name, Description, Relation, Soundness, this.rules) = (name, description, relation, soundness, rules);
+ internal RewriteRuleSet(string name, string description, TransformationRelation relation, Soundness soundness, Func rules, IReadOnlyList? addressable = null)
+ => (Name, Description, Relation, Soundness, this.rules, Rules)
+ = (name, description, relation, soundness, rules, addressable ?? Array.Empty());
/// A stable identity for this set.
public string Name { get; }
@@ -55,6 +45,44 @@ internal RewriteRuleSet(string name, string description, TransformationRelation
/// How well justified that claim is. See on what a tier here is and is not.
public Soundness Soundness { get; }
+ ///
+ /// The individual rewrites this set is made of, in the order they are tried.
+ ///
+ ///
+ ///
+ /// Empty is not the same as "no rewrites". A set whose rewrites are written as a
+ /// switch over the expression has every arm listed here; a set built some other
+ /// way — a sort, a polynomial division, a method with branches and locals — has none,
+ /// because there are no arms to generate from. and
+ /// behave identically either way, so this is a statement
+ /// about how finely the set can be reported on and not about what it does.
+ ///
+ ///
+ /// First match wins, exactly as in the switch, so the order is part of the
+ /// meaning: where two rules can fire on one node, the earlier one does.
+ ///
+ ///
+ public IReadOnlyList Rules { get; }
+
+ ///
+ /// The rule that fires at this node, or where none does.
+ ///
+ ///
+ /// At this node only, leaving its children alone — which is what an arm of the
+ /// switch sees. Always null for a set with no .
+ ///
+ public RewriteRule? RuleFiringAt(Entity node)
+ {
+ if (node is null)
+ throw new ArgumentNullException(nameof(node));
+ // The array, and by index: this is walked once per recorded rewrite and the sets are
+ // long -- CommonRules alone has 103 arms.
+ for (var i = 0; i < Rules.Count; i++)
+ if (Rules[i].TryApply(node) is not null)
+ return Rules[i];
+ return null;
+ }
+
///
/// Applies the set once, bottom-up over every node, exactly as
/// does. One pass: a rewrite
@@ -88,7 +116,10 @@ private Entity ApplyOnceRecording(Entity expression, RewriteRecording recording)
{
var rewritten = rules(node);
if (rewritten != node)
- recording.Add(this, node, rewritten);
+ // Which rule did it, asked only of a node that actually changed and only
+ // while somebody is recording. The arms are tried in the same order the
+ // switch tries them, so the first that applies is the one that fired.
+ recording.Add(this, RuleFiringAt(node), node, rewritten);
return rewritten;
});
diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs
index fccc41f19..e377cbe41 100644
--- a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs
+++ b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs
@@ -92,7 +92,8 @@ public static class RewriteRules
"Moves a negative numeric factor out of a product into the sign of the term.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.InvertNegativeMultipliers);
+ Patterns.InvertNegativeMultipliers,
+ Patterns.InvertNegativeMultipliersArms);
///
/// The arithmetic housekeeping rules — collecting like terms, flattening nested
@@ -103,7 +104,8 @@ public static class RewriteRules
"Collects like terms and normalises the arrangement of products and quotients.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.CommonRules);
+ Patterns.CommonRules,
+ Patterns.CommonRulesArms);
///
/// Gets a quotient into the shape the division rules expect before they run.
@@ -113,7 +115,8 @@ public static class RewriteRules
"Lifts numeric factors out of a quotient so that the division rules can see it.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.DivisionPreparingRules);
+ Patterns.DivisionPreparingRules,
+ Patterns.DivisionPreparingRulesArms);
///
/// Cosmetic arrangement of signs, so that adding a negative reads as a difference.
@@ -123,7 +126,8 @@ public static class RewriteRules
"Arranges signs so that adding a negative is written as subtracting a positive.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.NumericNeatRules);
+ Patterns.NumericNeatRules,
+ Patterns.NumericNeatRulesArms);
#endregion
@@ -139,7 +143,8 @@ public static class RewriteRules
// (a ^ b) ^ c is a ^ (b c) only on a branch; the rules guard for it, and the
// guard is what the tier is stating.
Soundness.SoundUnderAssumptions,
- Patterns.PowerRules);
+ Patterns.PowerRules,
+ Patterns.PowerRulesArms);
///
/// Multiplies products over sums out.
@@ -149,7 +154,8 @@ public static class RewriteRules
"Distributes products and powers over sums.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.ExpandRules);
+ Patterns.ExpandRules,
+ Patterns.ExpandRulesArms);
///
/// Takes common factors back out of a sum.
@@ -159,7 +165,8 @@ public static class RewriteRules
"Gathers common factors out of sums.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.FactorizeRules);
+ Patterns.FactorizeRules,
+ Patterns.FactorizeRulesArms);
///
/// Recognises a perfect square written out, so that factorisation has something to
@@ -261,7 +268,8 @@ public static class RewriteRules
// tan and cot bring poles with them, so an identity that introduces one holds
// away from those points rather than everywhere.
Soundness.SoundUnderAssumptions,
- Patterns.TrigonometricRules);
+ Patterns.TrigonometricRules,
+ Patterns.TrigonometricRulesArms);
///
/// Rewrites the derived trigonometric functions in terms of sine and cosine.
@@ -271,7 +279,8 @@ public static class RewriteRules
"Writes tangents, cotangents, secants and cosecants as sines and cosines.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.NormalTrigonometricForm);
+ Patterns.NormalTrigonometricForm,
+ Patterns.NormalTrigonometricFormArms);
///
/// Gathers sines and cosines back into the derived functions where that is shorter.
@@ -281,7 +290,8 @@ public static class RewriteRules
"Recognises a quotient or reciprocal of sines and cosines as a tangent, cotangent, secant or cosecant.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.CollapseTrigonometricFunctions);
+ Patterns.CollapseTrigonometricFunctions,
+ Patterns.CollapseTrigonometricFunctionsArms);
///
/// Opens a trigonometric function of a sum into functions of its terms.
@@ -291,7 +301,8 @@ public static class RewriteRules
"Expands a sine or cosine of a sum into products of sines and cosines of its terms.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.ExpandTrigonometricRules);
+ Patterns.ExpandTrigonometricRules,
+ Patterns.ExpandTrigonometricRulesArms);
///
/// Opens a trigonometric function of a multiplied angle.
@@ -305,7 +316,8 @@ public static class RewriteRules
"Expands a sine or cosine of an integer multiple of an angle.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.ExpandMultipleAngleRules);
+ Patterns.ExpandMultipleAngleRules,
+ Patterns.ExpandMultipleAngleRulesArms);
#endregion
@@ -319,7 +331,8 @@ public static class RewriteRules
"Applies the identities of boolean algebra to conjunctions, disjunctions and negations.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.BooleanRules);
+ Patterns.BooleanRules,
+ Patterns.BooleanRulesArms);
///
/// Rules about equalities and inequalities.
@@ -329,7 +342,8 @@ public static class RewriteRules
"Rearranges equalities and inequalities into their usual form.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.InequalityEqualityRules);
+ Patterns.InequalityEqualityRules,
+ Patterns.InequalityEqualityRulesArms);
///
/// Rules about unions, intersections and set differences.
@@ -339,7 +353,8 @@ public static class RewriteRules
"Applies the identities of set algebra to unions, intersections and set differences.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.SetOperatorRules);
+ Patterns.SetOperatorRules,
+ Patterns.SetOperatorRulesArms);
///
/// Cancels a quotient of factorials down to the terms that survive.
@@ -369,7 +384,8 @@ public static class RewriteRules
"Applies the multiplicative identities of Euler's totient function.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
- Patterns.PhiFunctionRules);
+ Patterns.PhiFunctionRules,
+ Patterns.PhiFunctionRulesArms);
#endregion
diff --git a/Sources/AngouriMath/Core/Transformations/RewriteStep.cs b/Sources/AngouriMath/Core/Transformations/RewriteStep.cs
index 8b10274e5..e6caab826 100644
--- a/Sources/AngouriMath/Core/Transformations/RewriteStep.cs
+++ b/Sources/AngouriMath/Core/Transformations/RewriteStep.cs
@@ -19,12 +19,24 @@ namespace AngouriMath.Core.Transformations
///
public readonly struct RewriteStep
{
- internal RewriteStep(RewriteRuleSet ruleSet, Entity before, Entity after)
- => (RuleSet, Before, After) = (ruleSet, before, after);
+ internal RewriteStep(RewriteRuleSet ruleSet, RewriteRule? rule, Entity before, Entity after)
+ => (RuleSet, Rule, Before, After) = (ruleSet, rule, before, after);
/// Which rule set rewrote it.
public RewriteRuleSet RuleSet { get; }
+ ///
+ /// Which single rewrite in that set did it, where the set is addressable at that grain —
+ /// see . Null where it is not.
+ ///
+ ///
+ /// This is the grain
+ /// #28 asks for: a
+ /// derivation that names the identity applied, rather than the group of identities it
+ /// was filed under.
+ ///
+ public RewriteRule? Rule { get; }
+
/// The subexpression as it was matched.
public Entity Before { get; }
@@ -38,6 +50,9 @@ internal RewriteStep(RewriteRuleSet ruleSet, Entity before, Entity after)
public Soundness Soundness => RuleSet.Soundness;
///
- public override string ToString() => $"{RuleSet.Name}: {Before.Stringize()} -> {After.Stringize()}";
+ public override string ToString()
+ => Rule is null
+ ? $"{RuleSet.Name}: {Before.Stringize()} -> {After.Stringize()}"
+ : $"{RuleSet.Name}/{Rule.Name}: {Before.Stringize()} -> {After.Stringize()}";
}
}
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Boolean.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Boolean.cs
index dbfb3d8e7..e87e9888b 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Boolean.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Boolean.cs
@@ -22,6 +22,7 @@ private static bool IsLogic(Entity a, Entity b)
private static bool IsLogic(Entity a, Entity b, Entity c)
=> IsLogic(a, b) && IsLogic(c);
+ [AddressableRules]
internal static Entity BooleanRules(Entity x) => x switch
{
Impliesf(var ass, var other) when ass == False && IsLogic(other) => True.Provided(other.DomainCondition),
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Common.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Common.cs
index c1b75c753..720fb55f3 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Common.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Common.cs
@@ -12,6 +12,7 @@ namespace AngouriMath.Functions
{
internal static partial class Patterns
{
+ [AddressableRules]
internal static Entity DivisionPreparingRules(Entity x) => x switch
{
Mulf(var any1, Divf(Integer(1), var any2)) => any1 / any2,
@@ -20,6 +21,7 @@ internal static partial class Patterns
_ => x
};
+ [AddressableRules]
internal static Entity NumericNeatRules(Entity x) => x switch
{
// (-a) + (-b) is -(a + b), and the operands are already negative here, so what is
@@ -61,6 +63,7 @@ internal static partial class Patterns
_ => x
};
+ [AddressableRules]
internal static Entity CommonRules(Entity x) => x switch
{
// (a * f(x)) * g(x) = a * (f(x) * g(x))
@@ -149,9 +152,6 @@ internal static partial class Patterns
Sumf(Divf(var anyButNot1 and not Integer(1), var any2), var anyButNot1a)
when anyButNot1 == anyButNot1a => anyButNot1 * (1 + 1 / any2),
- // {1} * {2} - {1} * {3} = {1} * ({2} - {3})
- Minusf(Mulf(var any1, var any2), Mulf(var any1a, var any3)) when any1 == any1a => any1 * (any2 - any3),
-
// x * x = x ^ 2
Mulf(var any1, var any1a) when any1 == any1a => new Powf(any1, 2),
@@ -205,10 +205,6 @@ internal static partial class Patterns
// a * (b * {}) = (a * b) * {}
Mulf(Number const1, Mulf(Number const2, var any1)) => const1 * const2 * any1,
- // {1} - {2} * {1}
- Minusf(var any1, Mulf(var any2, var any1a)) when any1 == any1a => any1 * (1 - any2),
- Minusf(var any1, Mulf(var any1a, var any2)) when any1 == any1a => any1 * (1 - any2),
-
Sumf(var any1, Sumf(var any2, var any1a)) when any1 == any1a => 2 * any1 + any2,
Sumf(var any1, Sumf(var any1a, var any2)) when any1 == any1a => 2 * any1 + any2,
Sumf(Sumf(var any2, var any1), var any1a) when any1 == any1a => 2 * any1 + any2,
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.EqualityInequality.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.EqualityInequality.cs
index a51a5dc83..1f368e9d2 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.EqualityInequality.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.EqualityInequality.cs
@@ -96,6 +96,7 @@ private static Entity OrderedCondition(Entity entity)
private static Entity BothHold(Entity left, Entity right)
=> left == True ? right : right == True ? left : left & right;
+ [AddressableRules]
internal static Entity InequalityEqualityRules(Entity x) => x switch
{
Orf(Lessf(var any1, var any2), Equalsf(var any1a, var any2a)) when any1 == any1a && any2 == any2a => any1 <= any2,
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs
index a6c6867fc..fc102f4e6 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs
@@ -12,6 +12,7 @@ namespace AngouriMath.Functions
{
internal static partial class Patterns
{
+ [AddressableRules]
internal static Entity ExpandRules(Entity x) => x switch
{
Sinf(Sumf(var any1, var any2)) => new Sinf(any1) * new Cosf(any2) + new Sinf(any2) * new Cosf(any1),
@@ -20,6 +21,7 @@ internal static partial class Patterns
_ => x
};
+ [AddressableRules]
internal static Entity FactorizeRules(Entity x) => x switch
{
// {1}^2n - {2}^2m = ({1}^n - {2}^m) * ({1}^n + {2}^m).
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.NumberTheory.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.NumberTheory.cs
index 0f1e7caf1..9a3ef866b 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.NumberTheory.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.NumberTheory.cs
@@ -11,6 +11,7 @@ namespace AngouriMath.Functions
{
internal static partial class Patterns
{
+ [AddressableRules]
internal static Entity PhiFunctionRules(Entity x) => x switch
{
Phif(Powf(Integer prime, var variable)) when prime.IsPrime => new Powf(prime, variable - 1) * (prime - 1),
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs
index f701fbd10..9dcae3108 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Power.cs
@@ -18,6 +18,7 @@ expr is Powf(var @base, Integer { IsNegative: true } pow)
? 1 / MathS.Pow(@base, -1 * pow)
: expr;
/// 1 + (-x) => 1 - x, and -(a - b) => b - a
+ [AddressableRules]
internal static Entity InvertNegativeMultipliers(Entity expr) => expr switch
{
Sumf(var any1, Mulf(Real { IsNegative: true } const1, var any2))
@@ -40,6 +41,7 @@ expr is Powf(var @base, Integer { IsNegative: true } pow)
_ => expr
};
+ [AddressableRules]
internal static Entity PowerRules(Entity x) => x switch
{
// {} / {} = 1 provided not {} = 0
@@ -127,9 +129,6 @@ expr is Powf(var @base, Integer { IsNegative: true } pow)
// x^n / x
Divf(Powf(var any1, var any2), var any1a) when any1 == any1a => new Powf(any1, any2 - 1),
- // x^n / x^m
- Divf(Powf(var any1, var any2), Powf(var any1a, var any3)) when any1 == any1a => new Powf(any1, any2 - any3),
-
// c ^ log(c, a) = a
Powf(Number const1, Logf(Number const1a, var any1)) when const1 == const1a => any1,
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Sets.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Sets.cs
index dc4bfc2ab..ca37aeea7 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Sets.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Sets.cs
@@ -15,6 +15,7 @@ internal static partial class Patterns
{
[ConstantField] private static readonly FiniteSet FullBooleanSet = new FiniteSet(True, False);
+ [AddressableRules]
internal static Entity SetOperatorRules(Entity x) => x switch
{
Intersectionf(var any1, var any1a) when any1 == any1a => any1,
diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs
index 49c0218d6..b23e46d29 100644
--- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs
@@ -92,6 +92,7 @@ private static bool WithinZeroAndPi(Entity argument, bool closed)
}
+ [AddressableRules]
internal static Entity TrigonometricRules(Entity x) => x switch
{
// sin({}) * cos({}) = 1/2 * sin(2{})
@@ -220,6 +221,7 @@ private static bool WithinZeroAndPi(Entity argument, bool closed)
_ => x
};
+ [AddressableRules]
internal static Entity ExpandTrigonometricRules(Entity x) => x switch
{
Mulf(Rational(Integer(1), Integer(2)), Sinf(Mulf(Integer(2), var any1))) => new Sinf(any1) * new Cosf(any1),
@@ -247,6 +249,7 @@ private static bool WithinZeroAndPi(Entity argument, bool closed)
/// zero and neither did (sin(2t)csc(t))^2/4 - cos(2t) - sin(t)^2, which is
/// https://github.com/asc-community/AngouriMath/issues/557.
///
+ [AddressableRules]
internal static Entity ExpandMultipleAngleRules(Entity x) => x switch
{
Sinf(Mulf(Integer n, var inner)) when IsWorthExpanding(n) =>
@@ -263,6 +266,7 @@ private static bool WithinZeroAndPi(Entity argument, bool closed)
private static bool IsWorthExpanding(Integer n)
=> n.EInteger.Abs() >= 2 && n.EInteger.Abs() <= MaxAngleMultiplier;
+ [AddressableRules]
internal static Entity CollapseTrigonometricFunctions(Entity x) => x switch
{
// sin / cos = tan
@@ -279,6 +283,7 @@ private static bool IsWorthExpanding(Integer n)
///
/// For this it is true that any trigonometric function is either sin or cos
///
+ [AddressableRules]
internal static Entity NormalTrigonometricForm(Entity x) => x switch
{
Tanf(var any1) => any1.Sin() / any1.Cos(),
diff --git a/Sources/Tests/UnitTests/Common/PublicApi.txt b/Sources/Tests/UnitTests/Common/PublicApi.txt
index 3a40f5c30..c8b48db33 100644
--- a/Sources/Tests/UnitTests/Common/PublicApi.txt
+++ b/Sources/Tests/UnitTests/Common/PublicApi.txt
@@ -120,11 +120,29 @@ AngouriMath.Core.ReasonOfFailureWhileParsing.op_Inequality(AngouriMath.Core.Reas
AngouriMath.Core.Transformations.RewriteRecording.Dispose() : System.Void
AngouriMath.Core.Transformations.RewriteRecording.Start() : AngouriMath.Core.Transformations.RewriteRecording
AngouriMath.Core.Transformations.RewriteRecording.Steps { } : System.Collections.Generic.IReadOnlyList
+AngouriMath.Core.Transformations.RewriteRule.Description { } : System.String
+AngouriMath.Core.Transformations.RewriteRule.Growth { } : AngouriMath.Core.Transformations.RewriteRuleGrowth
+AngouriMath.Core.Transformations.RewriteRule.GuardSource { } : System.String
+AngouriMath.Core.Transformations.RewriteRule.Index { } : System.Int32
+AngouriMath.Core.Transformations.RewriteRule.Name { } : System.String
+AngouriMath.Core.Transformations.RewriteRule.NodeTypes { } : System.Collections.Generic.IReadOnlyList
+AngouriMath.Core.Transformations.RewriteRule.PatternSource { } : System.String
+AngouriMath.Core.Transformations.RewriteRule.ReplacementSource { } : System.String
+AngouriMath.Core.Transformations.RewriteRule.Source { } : System.String
+AngouriMath.Core.Transformations.RewriteRule.SourceLine { } : System.Int32
+AngouriMath.Core.Transformations.RewriteRule.ToString() : System.String
+AngouriMath.Core.Transformations.RewriteRule.TryApply(AngouriMath.Entity) : AngouriMath.Entity
+AngouriMath.Core.Transformations.RewriteRuleGrowth.Collects : AngouriMath.Core.Transformations.RewriteRuleGrowth
+AngouriMath.Core.Transformations.RewriteRuleGrowth.Expands : AngouriMath.Core.Transformations.RewriteRuleGrowth
+AngouriMath.Core.Transformations.RewriteRuleGrowth.Rearranges : AngouriMath.Core.Transformations.RewriteRuleGrowth
+AngouriMath.Core.Transformations.RewriteRuleGrowth.value__ : System.Int32
AngouriMath.Core.Transformations.RewriteRuleSet.ApplyOnce(AngouriMath.Entity) : AngouriMath.Entity
AngouriMath.Core.Transformations.RewriteRuleSet.AsTransformation() : AngouriMath.Core.Transformations.Transformation
AngouriMath.Core.Transformations.RewriteRuleSet.Description { } : System.String
AngouriMath.Core.Transformations.RewriteRuleSet.Name { } : System.String
AngouriMath.Core.Transformations.RewriteRuleSet.Relation { } : AngouriMath.Core.Transformations.TransformationRelation
+AngouriMath.Core.Transformations.RewriteRuleSet.RuleFiringAt(AngouriMath.Entity) : AngouriMath.Core.Transformations.RewriteRule
+AngouriMath.Core.Transformations.RewriteRuleSet.Rules { } : System.Collections.Generic.IReadOnlyList
AngouriMath.Core.Transformations.RewriteRuleSet.Soundness { } : AngouriMath.Core.Transformations.Soundness
AngouriMath.Core.Transformations.RewriteRuleSet.ToString() : System.String
AngouriMath.Core.Transformations.RewriteRules.All { } : System.Collections.Generic.IReadOnlyList
@@ -161,6 +179,7 @@ AngouriMath.Core.Transformations.RewriteRules.Trigonometric { } : AngouriMath.Co
AngouriMath.Core.Transformations.RewriteStep.After { } : AngouriMath.Entity
AngouriMath.Core.Transformations.RewriteStep.Before { } : AngouriMath.Entity
AngouriMath.Core.Transformations.RewriteStep.Relation { } : AngouriMath.Core.Transformations.TransformationRelation
+AngouriMath.Core.Transformations.RewriteStep.Rule { } : AngouriMath.Core.Transformations.RewriteRule
AngouriMath.Core.Transformations.RewriteStep.RuleSet { } : AngouriMath.Core.Transformations.RewriteRuleSet
AngouriMath.Core.Transformations.RewriteStep.Soundness { } : AngouriMath.Core.Transformations.Soundness
AngouriMath.Core.Transformations.RewriteStep.ToString() : System.String
@@ -2504,6 +2523,7 @@ class AngouriMath.Core.ReasonOfFailureWhileParsing+InternalError
class AngouriMath.Core.ReasonOfFailureWhileParsing+MissingOperator
class AngouriMath.Core.ReasonOfFailureWhileParsing+Unknown
class AngouriMath.Core.Transformations.RewriteRecording
+class AngouriMath.Core.Transformations.RewriteRule
class AngouriMath.Core.Transformations.RewriteRuleSet
class AngouriMath.Core.Transformations.RewriteRules
class AngouriMath.Core.Transformations.Transformation
@@ -2604,6 +2624,7 @@ class AngouriMath.MathS+UnsafeAndInternal
class AngouriMath.MathS+Utils
enum AngouriMath.Core.ApproachFrom
enum AngouriMath.Core.Domain
+enum AngouriMath.Core.Transformations.RewriteRuleGrowth
enum AngouriMath.Core.Transformations.Soundness
enum AngouriMath.Core.Transformations.TransformationRelation
enum AngouriMath.MathS+Matrices+Direction
diff --git a/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs b/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs
new file mode 100644
index 000000000..f14e65e39
--- /dev/null
+++ b/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs
@@ -0,0 +1,356 @@
+//
+// Copyright (c) 2019-2026 Angouri.
+// AngouriMath is licensed under MIT.
+// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
+// Website: https://am.angouri.org.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AngouriMath.Core.Transformations;
+using AngouriMath.Extensions;
+using AngouriMath.Functions;
+using Xunit;
+
+namespace AngouriMath.Tests.Core.Transformations
+{
+ ///
+ /// The generated rules have to be the switch they were generated from, arm for arm.
+ /// https://github.com/asc-community/AngouriMath/issues/825
+ ///
+ ///
+ ///
+ /// This is the check that makes the generator worth trusting, and it is why the design is a
+ /// generator rather than a hand transcription: the arms are copied as syntax, so a rule
+ /// cannot say something its arm does not — and this test says so out loud, over
+ /// generated input, at every node rather than only at the root. Most rules match a shape that
+ /// only ever occurs as a subexpression, so a root-only comparison would exercise a handful of
+ /// them and report green.
+ ///
+ ///
+ /// The comparison is per node and three-way: which rule the set says fired, what that rule
+ /// produces, and what the switch produces. Agreeing on the answer while disagreeing on
+ /// which rule gave it would make every derivation built on this wrong in a way no output
+ /// comparison notices.
+ ///
+ ///
+ [Trait("Area", "Core")]
+ public sealed class AddressableRulesTest
+ {
+ /// Each addressable set, next to the method its arms were generated from.
+ private static IEnumerable<(RewriteRuleSet Set, Func Switch)> Addressable()
+ {
+ yield return (RewriteRules.Common, Patterns.CommonRules);
+ yield return (RewriteRules.DivisionPreparing, Patterns.DivisionPreparingRules);
+ yield return (RewriteRules.NumericNeat, Patterns.NumericNeatRules);
+ yield return (RewriteRules.InvertNegativeMultipliers, Patterns.InvertNegativeMultipliers);
+ yield return (RewriteRules.Power, Patterns.PowerRules);
+ yield return (RewriteRules.Expansion, Patterns.ExpandRules);
+ yield return (RewriteRules.Factorization, Patterns.FactorizeRules);
+ yield return (RewriteRules.Trigonometric, Patterns.TrigonometricRules);
+ yield return (RewriteRules.NormalTrigonometricForm, Patterns.NormalTrigonometricForm);
+ yield return (RewriteRules.CollapseTrigonometricFunctions, Patterns.CollapseTrigonometricFunctions);
+ yield return (RewriteRules.ExpandTrigonometric, Patterns.ExpandTrigonometricRules);
+ yield return (RewriteRules.ExpandMultipleAngle, Patterns.ExpandMultipleAngleRules);
+ yield return (RewriteRules.Boolean, Patterns.BooleanRules);
+ yield return (RewriteRules.InequalityEquality, Patterns.InequalityEqualityRules);
+ yield return (RewriteRules.SetOperator, Patterns.SetOperatorRules);
+ yield return (RewriteRules.PhiFunction, Patterns.PhiFunctionRules);
+ }
+
+ public static IEnumerable