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
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
335 changes: 335 additions & 0 deletions Sources/Analyzers/RuleRegistryGenerator/RuleRegistryGenerator.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>

<!-- Avoid ID conflicts with the package project. -->
<PackageId>*$(MSBuildProjectFullPath)*</PackageId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions Sources/AngouriMath.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
7 changes: 7 additions & 0 deletions Sources/AngouriMath/AngouriMath.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@

<!--Analyzers-->
<ProjectReference Include="../Analyzers/Analyzers/Analyzers.csproj" PrivateAssets="all" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />

<!--
The rule registry generator. Both attributes are load-bearing: without OutputItemType the
compiler is never handed the generator, and the failure shows up as the generated fields
being missing rather than as anything about generators.
-->
<ProjectReference Include="../Analyzers/RuleRegistryGenerator/RuleRegistryGenerator.csproj" PrivateAssets="all" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
13 changes: 13 additions & 0 deletions Sources/AngouriMath/Core/CoreAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,17 @@ internal sealed class ConstantFieldAttribute : Attribute { }
/// </summary>
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
internal sealed class ConcurrentFieldAttribute : Attribute { }

/// <summary>
/// Marks a rewrite rule <c>switch</c> whose arms are to be generated as individually
/// addressable <see cref="Transformations.RewriteRule"/> values, in a field named after the
/// method with <c>Arms</c> appended.
/// </summary>
/// <remarks>
/// The method must be expression-bodied with a body of the form <c>parameter switch { ... }</c>;
/// 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.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
internal sealed class AddressableRulesAttribute : Attribute { }
}
4 changes: 2 additions & 2 deletions Sources/AngouriMath/Core/Transformations/RewriteRecording.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ public void Dispose()
/// </summary>
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));
}
}
}
195 changes: 195 additions & 0 deletions Sources/AngouriMath/Core/Transformations/RewriteRule.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Which way a rewrite moves: does it make the expression bigger, smaller, or neither.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// It exists because a rewrite <i>graph</i> needs it and a rewrite <i>pipeline</i> does not.
/// <see cref="Entity.Simplify(int)"/> 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 —
/// <a href="https://github.com/asc-community/AngouriMath/issues/746">#746</a> tier 2, measured
/// in the <c>egraph</c> harness at up to 7,143 times the e-nodes when it is not told.
/// </para>
/// </remarks>
public enum RewriteRuleGrowth
{
/// <summary>The replacement is written with fewer operators and operands than the pattern.</summary>
Collects,

/// <summary>The two are written with the same number, so the rule moves things about.</summary>
Rearranges,

/// <summary>The replacement is written with more, so the rule opens the expression out.</summary>
Expands
}

/// <summary>
/// One rewrite, addressable on its own: what it matches, what it puts there instead, where it
/// is written and which way it moves.
/// </summary>
/// <remarks>
/// <para>
/// A <see cref="RewriteRuleSet"/> is the unit the library applies; this is the unit inside it.
/// The distinction is what
/// <a href="https://github.com/asc-community/AngouriMath/issues/28">#28</a> asks for — a
/// derivation that says <i>which rewrite</i> fired rather than which group of them — and what
/// <a href="https://github.com/asc-community/AngouriMath/issues/825">#825</a> is about.
/// </para>
/// <para>
/// These are <b>generated from the <c>switch</c> that defines them</b>, arm by arm, rather
/// than written out a second time. That is deliberate and it is the whole design: the
/// <c>switch</c> 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
/// <c>AngouriMath.Core.Transformations.Matching</c> was measured at about five percent of
/// <see cref="Entity.Simplify(int)"/> per rule set exchanged.
/// </para>
/// <para>
/// <b>What is not here.</b> A per-rule <see cref="Soundness"/>. A rule's tier is a claim
/// somebody has to argue for, and there is no honest way to derive one from syntax — so
/// <see cref="RewriteRuleSet.Soundness"/> 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.
/// </para>
/// </remarks>
public sealed class RewriteRule
{
internal RewriteRule(
string source,
int index,
string name,
string? description,
IReadOnlyList<Type> nodeTypes,
string patternSource,
string? guardSource,
string replacementSource,
RewriteRuleGrowth growth,
int sourceLine,
Func<Entity, Entity?> 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<Entity, Entity?> apply;

/// <summary>The method whose <c>switch</c> this arm belongs to.</summary>
public string Source { get; }

/// <summary>Where it sits among that method's arms, which is the order it is tried in.</summary>
/// <remarks>
/// 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.
/// </remarks>
public int Index { get; }

/// <summary>
/// What to call this rule in a report, a test or a bug — the pattern it matches, written
/// as the source writes it.
/// </summary>
/// <remarks>
/// 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 <c>#2</c>, <c>#3</c> — and a set
/// that does is worth looking at, since the second is unreachable.
/// </remarks>
public string Name { get; }

/// <summary>The comment written above the rule, where there is one: the identity in the notation a mathematician would use.</summary>
public string? Description { get; }

/// <summary>
/// The node types the rule can fire on — usually one, occasionally two, and empty where
/// the pattern's shape does not say.
/// </summary>
/// <remarks>
/// A <b>necessary</b> 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 <c>switch</c> over distinct node types gets from the compiler for free and a
/// list of rules has to be told.
/// </remarks>
public IReadOnlyList<Type> NodeTypes { get; }

/// <summary>The pattern the arm matches, as the C# source writes it.</summary>
/// <remarks>
/// <para>
/// <b>Source text, and named so.</b> This is what a reader would see in the
/// <c>switch</c>, not a representation anything can match against — to ask whether this
/// rule fires on a node, call <see cref="TryApply(Entity)"/>, which runs the arm itself.
/// </para>
/// <para>
/// The name carries <c>Source</c> because
/// <a href="https://github.com/asc-community/AngouriMath/issues/746">#746</a> tier 1 is
/// pattern matching as data, and when a pattern becomes a value it should be able to be
/// called <c>Pattern</c> without first breaking somebody. Deciding that after the
/// property shipped would have cost a major version; deciding it here cost nothing.
/// </para>
/// </remarks>
public string PatternSource { get; }

/// <summary>
/// The side condition as the C# source writes it, or <see langword="null"/> where the arm
/// has no <c>when</c> clause. Source text — see <see cref="PatternSource"/>.
/// </summary>
public string? GuardSource { get; }

/// <summary>
/// What the arm builds, as the C# source writes it. Source text — see
/// <see cref="PatternSource"/>.
/// </summary>
public string ReplacementSource { get; }

/// <summary>Which way the rewrite moves. See <see cref="RewriteRuleGrowth"/>.</summary>
public RewriteRuleGrowth Growth { get; }

/// <summary>The line of the source file the arm is written on.</summary>
public int SourceLine { get; }

/// <summary>
/// This one rule at this one node, ignoring its children and every other rule — or
/// <see langword="null"/> where it does not apply.
/// </summary>
/// <remarks>
/// Null means "this rule does not fire here", which is a different claim from the rule
/// set's <see cref="RewriteRuleSet.ApplyOnce(Entity)"/> handing back the expression it was
/// given. The set has to return something; a rule may decline.
/// </remarks>
public Entity? TryApply(Entity node)
=> node is null ? throw new ArgumentNullException(nameof(node)) : apply(node);

/// <inheritdoc/>
public override string ToString()
=> GuardSource is null
? $"{PatternSource} => {ReplacementSource}"
: $"{PatternSource} when {GuardSource} => {ReplacementSource}";
}
}
Loading
Loading