diff --git a/AGENTS.md b/AGENTS.md
index 4f84238e7..92e00db57 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -222,6 +222,38 @@ with the measurements against it.
product is contraction, not convolution), and operators or gates — those act *on* states rather than
being states, and they belong to a quantum computing library rather than to a CAS.
+## An operation is a value, not only a method
+
+`Simplify`, `Expand`, `Factorize`, `Differentiate`, `Integrate` and `Limit` are adapters over
+`AngouriMath.Core.Transformations` — a `Transformation` is the operation itself, carrying a name,
+what it claims about its output, and how well justified the claim is. The algorithms underneath are
+untouched; what changed is that a step can be named, composed and enumerated rather than only
+called. See [`Contributing/Transformations.md`](Sources/AngouriMath/Docs/Contributing/Transformations.md),
+and [#746](https://github.com/asc-community/AngouriMath/issues/746) for where it is going.
+
+Three habits it asks for, and each is the honesty rule above in a different place:
+
+**Say which relation you are claiming.** `Equivalence` means the output is another way of writing the
+input; `Derivation` means it is a different object. A derivative and an antiderivative are
+`Derivation`, and a test that subtracts one from its input and asserts zero is testing nothing.
+
+**Do not label a rewrite `Sound` without an argument.** Every rule set shipped today is
+`SoundUnderAssumptions`, and a test over `RewriteRules.All` holds it there. The tier is declared, not
+verified — so promoting one means changing that test, and saying in the same change why the rewrite
+needs no assumptions. Loosening a tier needs nothing.
+
+**Say "no answer" with `null`, here too.** `ApplyCore` returning `null` is the layer's way of saying
+"I could not settle this", and it is the one place where the new layer is *more* honest than the 1.x
+method it backs: `Transformation.Integration` has no answer where `Entity.Integrate` returns an
+unevaluated `Integralf`. Both are the same claim; neither is `NaN`.
+
+And what not to do with it. `Solve` is not a transformation — it consumes a goal and produces a
+solution set, and it belongs in a tactic layer that does not exist yet. `Entity.Set` being an
+`Entity` means it would type-check as `Entity -> Entity`, which is the reason to keep it out rather
+than a reason to put it in. Nor is there any inverse machinery: `Expand` and `Factor` are not
+inverses and `Unsolve` is not well defined, so the API does not invent a symmetry the mathematics
+does not have.
+
## Keep up with the mathematics
Algorithms here are decades of literature deep, and the good ones are written down. Before inventing
@@ -315,6 +347,7 @@ are short, and a stale one is worse than none — if you change what a file desc
| [`Contributing/General.md`](Sources/AngouriMath/Docs/Contributing/General.md) | the `Entity` hierarchy, in a paragraph |
| [`Contributing/AddingNode.cs`](Sources/AngouriMath/Docs/Contributing/AddingNode.cs) | every place a new node has to be taught about. Read it *before* adding one |
| [`Contributing/ImproveParser.md`](Sources/AngouriMath/Docs/Contributing/ImproveParser.md) | how to change the grammar and regenerate |
+| [`Contributing/Transformations.md`](Sources/AngouriMath/Docs/Contributing/Transformations.md) | the transformation layer the 1.x entry points sit on, and how to add the next rule set |
| [`Contributing/coding_rules.md`](Sources/AngouriMath/Docs/Contributing/coding_rules.md) | sealed-or-abstract, and immutability of `Entity` |
| [`WhatsNew/version_performance_control.md`](Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md) | the inter-version performance table, and how to add a column |
| `Sources/Analyzers/` | the custom analyzers, including the static-field one behind `[ConstantField]` |
diff --git a/Sources/.editorconfig b/Sources/.editorconfig
index 6055088ce..876c81824 100644
--- a/Sources/.editorconfig
+++ b/Sources/.editorconfig
@@ -19,3 +19,13 @@ dotnet_diagnostic.IDE0090.severity = none
# dotnet_diagnostic.CA2252.severity = none
file_header_template=\nCopyright (c) 2019-2022 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
+
+# Files written after 2022 say so. Bumping the year on the template above would make
+# IDE0073 fail on all 367 files that already carry the old one, which is a rewrite of every
+# header folded into whatever branch happened to need a new file -- so the year moves per
+# directory as directories are added, not all at once.
+[AngouriMath/Core/Transformations/*.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
+
+[Tests/UnitTests/Core/Transformations/*.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/AngouriMath/Core/Transformations/RewriteRuleSet.cs b/Sources/AngouriMath/Core/Transformations/RewriteRuleSet.cs
new file mode 100644
index 000000000..70e4f8248
--- /dev/null
+++ b/Sources/AngouriMath/Core/Transformations/RewriteRuleSet.cs
@@ -0,0 +1,104 @@
+//
+// 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;
+
+namespace AngouriMath.Core.Transformations
+{
+ ///
+ /// A named, attributable group of rewrites — the unit this library has always written
+ /// them in — carrying what it is called, what it claims and how well justified the
+ /// claim is, so that the set can be enumerated, tested and referred to by name instead
+ /// of only being called.
+ ///
+ ///
+ ///
+ /// The set, rather than the single pattern -> replacement line, is the unit
+ /// here on purpose. Every rewrite in this library is a case of one switch
+ /// matched against a node, and the C# compiler turns that switch into a type test and a
+ /// jump. Splitting each case into its own object would replace one dispatch per node
+ /// with one delegate call per rule per node on the hottest path in the library, and buy
+ /// nothing that a caller can use today. What a caller can use today is the set:
+ /// is enumerable, each entry is applicable on its own,
+ /// and the tests iterate it.
+ ///
+ ///
+ /// The finer grain is the next step rather than the abandoned one — see
+ /// #746 on rules
+ /// as data — and nothing here forecloses it: a set whose rewrites become individually
+ /// addressable keeps the same name and the same entry in the registry.
+ ///
+ ///
+ 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);
+
+ /// A stable identity for this set.
+ public string Name { get; }
+
+ /// What the set is for, in a sentence.
+ public string Description { get; }
+
+ /// What the rewrites in this set claim about the expressions they produce.
+ public TransformationRelation Relation { get; }
+
+ /// How well justified that claim is. See on what a tier here is and is not.
+ public Soundness Soundness { get; }
+
+ ///
+ /// Applies the set once, bottom-up over every node, exactly as
+ /// does. One pass: a rewrite
+ /// that opens up an opportunity for another rewrite in the same set will not see it
+ /// taken until the next pass.
+ ///
+ /// The expression to rewrite.
+ public Entity ApplyOnce(Entity expression)
+ => expression is null
+ ? throw new ArgumentNullException(nameof(expression))
+ : expression.Replace(rules);
+
+ ///
+ /// This set as a , so that it composes with the rest of
+ /// the catalogue.
+ ///
+ ///
+ /// Built on demand, and it has to be. derives
+ /// from , so constructing one runs that type's static
+ /// initialiser -- which reads . Doing it in this
+ /// constructor instead would make the two types depend on each other's
+ /// initialisation and hand whichever one lost the race a null rule set. Two threads
+ /// arriving together may each build one; they are equivalent and immutable, so
+ /// whichever reference lands is the one everyone then uses.
+ ///
+ public Transformation AsTransformation() => asTransformation ??= new RewritingTransformation(this);
+ private Transformation? asTransformation;
+
+ ///
+ public override string ToString() => Name;
+
+ private sealed class RewritingTransformation : Transformation
+ {
+ private readonly RewriteRuleSet ruleSet;
+
+ internal RewritingTransformation(RewriteRuleSet ruleSet) => this.ruleSet = ruleSet;
+
+ public override string Name => $"rewrite[{ruleSet.Name}]";
+
+ public override TransformationRelation Relation => ruleSet.Relation;
+
+ public override Soundness Soundness => ruleSet.Soundness;
+
+ // A rewrite pass always has an answer: where nothing matched, the answer is the
+ // expression it was given. That is a fixed point, not a failure, and
+ // TransformationResult.Changed is what tells the two apart.
+ protected override Entity? ApplyCore(Entity input) => ruleSet.ApplyOnce(input);
+ }
+ }
+}
diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs
new file mode 100644
index 000000000..592b11cfa
--- /dev/null
+++ b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs
@@ -0,0 +1,161 @@
+//
+// 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 AngouriMath.Functions;
+
+namespace AngouriMath.Core.Transformations
+{
+ ///
+ /// The rewrite rule sets this library ships, as data: named, described, attributed with
+ /// what they claim, and enumerable through .
+ ///
+ ///
+ ///
+ /// Registration is explicit and static — there is no assembly scanning and no
+ /// Activator, so the registry survives trimming and NativeAOT, and
+ /// is in a fixed order that does not depend on hashing, reflection or
+ /// which type happened to be loaded first.
+ ///
+ ///
+ /// This is a slice, not the whole pattern table. The sets below are the ones the
+ /// transformations in are built from; the rest of
+ /// Functions/Simplification/Patterns is still reached only from
+ /// Simplificator. Adding one here is five lines and gets it enumeration, a
+ /// soundness label and the tests over for free.
+ ///
+ ///
+ public static class RewriteRules
+ {
+ ///
+ /// Puts the operands of commutative chains into a canonical order and groups equal
+ /// ones together, so that x + y and y + x stop being different trees.
+ ///
+ public static RewriteRuleSet CanonicalOrder { get; } = new(
+ nameof(CanonicalOrder),
+ "Sorts and groups the operands of sums, products, conjunctions, disjunctions and set operations.",
+ TransformationRelation.Equivalence,
+ // Regrouping reads a quotient as a product with a negative power, which is the
+ // same value wherever the divisor is not zero.
+ Soundness.SoundUnderAssumptions,
+ Patterns.SortRules(TreeAnalyzer.SortLevel.HIGH_LEVEL));
+
+ ///
+ /// Turns a negative power into a quotient: a * b ^ (-1) becomes a / b.
+ ///
+ public static RewriteRuleSet InvertNegativePowers { get; } = new(
+ nameof(InvertNegativePowers),
+ "Rewrites negative powers as quotients.",
+ TransformationRelation.Equivalence,
+ Soundness.SoundUnderAssumptions,
+ Patterns.InvertNegativePowers);
+
+ ///
+ /// Brings a negative numeric factor out in front of the term it multiplies.
+ ///
+ public static RewriteRuleSet InvertNegativeMultipliers { get; } = new(
+ nameof(InvertNegativeMultipliers),
+ "Moves a negative numeric factor out of a product into the sign of the term.",
+ TransformationRelation.Equivalence,
+ Soundness.SoundUnderAssumptions,
+ Patterns.InvertNegativeMultipliers);
+
+ ///
+ /// The arithmetic housekeeping rules — collecting like terms, flattening nested
+ /// quotients, moving numeric coefficients to the front.
+ ///
+ public static RewriteRuleSet Common { get; } = new(
+ nameof(Common),
+ "Collects like terms and normalises the arrangement of products and quotients.",
+ TransformationRelation.Equivalence,
+ Soundness.SoundUnderAssumptions,
+ Patterns.CommonRules);
+
+ ///
+ /// Rules about powers, roots and logarithms.
+ ///
+ public static RewriteRuleSet Power { get; } = new(
+ nameof(Power),
+ "Gathers and splits powers, roots and logarithms.",
+ TransformationRelation.Equivalence,
+ // (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);
+
+ ///
+ /// The trigonometric identities.
+ ///
+ public static RewriteRuleSet Trigonometric { get; } = new(
+ nameof(Trigonometric),
+ "Applies trigonometric identities to sines, cosines and their relatives.",
+ TransformationRelation.Equivalence,
+ // 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);
+
+ ///
+ /// Multiplies products over sums out.
+ ///
+ public static RewriteRuleSet Expansion { get; } = new(
+ nameof(Expansion),
+ "Distributes products and powers over sums.",
+ TransformationRelation.Equivalence,
+ Soundness.SoundUnderAssumptions,
+ Patterns.ExpandRules);
+
+ ///
+ /// Takes common factors back out of a sum.
+ ///
+ public static RewriteRuleSet Factorization { get; } = new(
+ nameof(Factorization),
+ "Gathers common factors out of sums.",
+ TransformationRelation.Equivalence,
+ Soundness.SoundUnderAssumptions,
+ Patterns.FactorizeRules);
+
+ ///
+ /// Recognises a perfect square written out, so that factorisation has something to
+ /// gather.
+ ///
+ public static RewriteRuleSet PerfectSquare { get; } = new(
+ nameof(PerfectSquare),
+ "Collapses a written-out perfect square into a squared binomial.",
+ TransformationRelation.Equivalence,
+ Soundness.SoundUnderAssumptions,
+ Patterns.PerfectSquareRules);
+
+ ///
+ /// Clears a surd out of a two-term denominator.
+ ///
+ public static RewriteRuleSet RationaliseDenominator { get; } = new(
+ nameof(RationaliseDenominator),
+ "Multiplies a quotient by the conjugate of its denominator to clear a surd from it.",
+ TransformationRelation.Equivalence,
+ Soundness.SoundUnderAssumptions,
+ Patterns.RationaliseDenominator);
+
+ ///
+ /// Every rule set registered above, in a fixed order. Enumerable so that a property
+ /// that should hold of all of them can be tested over all of them rather than over
+ /// whichever ones somebody remembered.
+ ///
+ public static IReadOnlyList All { get; } = new[]
+ {
+ CanonicalOrder,
+ InvertNegativePowers,
+ InvertNegativeMultipliers,
+ Common,
+ Power,
+ Trigonometric,
+ Expansion,
+ Factorization,
+ PerfectSquare,
+ RationaliseDenominator,
+ };
+ }
+}
diff --git a/Sources/AngouriMath/Core/Transformations/Soundness.cs b/Sources/AngouriMath/Core/Transformations/Soundness.cs
new file mode 100644
index 000000000..b9361747b
--- /dev/null
+++ b/Sources/AngouriMath/Core/Transformations/Soundness.cs
@@ -0,0 +1,43 @@
+//
+// 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.
+//
+
+namespace AngouriMath.Core.Transformations
+{
+ ///
+ /// How well justified the relation a claims between its
+ /// input and its output is. The three tiers are never blurred: a heuristic labelled as
+ /// a proof is a wrong answer with a friendly face.
+ ///
+ ///
+ /// The tier is declared by whoever wrote the transformation, not derived from it.
+ /// Nothing in the library checks a declaration today, so a tier is a claim to be argued
+ /// with rather than a guarantee to be relied on, and the registry starts conservative
+ /// on purpose: tightening a label needs an argument, loosening one does not.
+ ///
+ public enum Soundness
+ {
+ ///
+ /// The claimed relation holds for every value of the free variables, with no side
+ /// conditions and no choice of branch.
+ ///
+ Sound,
+
+ ///
+ /// The claimed relation holds only where the stated assumptions do: wherever both
+ /// sides are defined, under the conditions the output carries as
+ /// , or under a branch-cut convention. This is the
+ /// honest tier for most of the rewrite rules in this library.
+ ///
+ SoundUnderAssumptions,
+
+ ///
+ /// Worth trying; proves nothing. A heuristic result has to be checked by something
+ /// else before it may be returned as an answer.
+ ///
+ Heuristic
+ }
+}
diff --git a/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs b/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs
new file mode 100644
index 000000000..51358d855
--- /dev/null
+++ b/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs
@@ -0,0 +1,297 @@
+//
+// 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 AngouriMath.Functions;
+using AngouriMath.Functions.Algebra;
+using static AngouriMath.Entity;
+
+namespace AngouriMath.Core.Transformations
+{
+ partial class Transformation
+ {
+ ///
+ /// Applies a rewrite rule set once over every node.
+ ///
+ /// The set to apply; see .
+ public static Transformation Rewriting(RewriteRuleSet ruleSet)
+ => (ruleSet ?? throw new ArgumentNullException(nameof(ruleSet))).AsTransformation();
+
+ ///
+ /// One structural tidying pass — . Cheap, and
+ /// no pattern search.
+ ///
+ public static Transformation InnerSimplification { get; } = new InnerSimplificationTransformation();
+
+ ///
+ /// The full simplification pipeline at the default level, as
+ /// runs it.
+ ///
+ public static Transformation Simplification { get; } = SimplificationAtLevel(2);
+
+ ///
+ /// The full simplification pipeline at a chosen level.
+ ///
+ ///
+ /// How hard to look; the same argument takes.
+ ///
+ public static Transformation SimplificationAtLevel(int level)
+ => LevelledCache.Simplification.For(level, static l => new SimplificationTransformation(l));
+
+ ///
+ /// Multiplies products over sums out, as does.
+ ///
+ public static Transformation Expansion { get; } = ExpansionAtLevel(2);
+
+ ///
+ /// Multiplies products over sums out, for a chosen number of passes.
+ ///
+ /// The number of passes; the argument takes.
+ public static Transformation ExpansionAtLevel(int level)
+ => LevelledCache.Expansion.For(level, static l => new ExpansionTransformation(l));
+
+ ///
+ /// Gathers common factors back out, as does.
+ ///
+ public static Transformation Factorization { get; } = FactorizationAtLevel(2);
+
+ ///
+ /// Gathers common factors back out, for a chosen number of passes.
+ ///
+ /// The number of passes; the argument takes.
+ ///
+ /// Built out of the registry rather than written again: one pass is the perfect
+ /// square rules, then the factorisation rules, then a tidying pass, and the level is
+ /// how many times that runs.
+ ///
+ public static Transformation FactorizationAtLevel(int level)
+ => LevelledCache.Factorization.For(level, static l =>
+ Rewriting(RewriteRules.PerfectSquare)
+ .Then(Rewriting(RewriteRules.Factorization))
+ .Then(InnerSimplification)
+ // Entity.Factorize has always run at least one pass, whatever it was asked for.
+ .Repeat(Math.Max(l, 1)));
+
+ ///
+ /// Puts commutative chains into a canonical order, so that expressions which differ
+ /// only in the arrangement of their operands become the same tree.
+ ///
+ ///
+ /// Not a simplification: it makes an expression comparable, not shorter. It has
+ /// always been available inside Simplify and had no name of its own until
+ /// this layer gave the rule set one.
+ ///
+ public static Transformation Normalization { get; }
+ = Rewriting(RewriteRules.CanonicalOrder).Then(InnerSimplification);
+
+ ///
+ /// Clears a surd out of a two-term denominator: 1 / (sqrt(3) + 5) becomes
+ /// (sqrt(3) - 5) / (-22).
+ ///
+ public static Transformation Rationalisation { get; }
+ = Rewriting(RewriteRules.RationaliseDenominator).Then(InnerSimplification);
+
+ ///
+ /// Replaces every occurrence of with
+ /// , as does.
+ ///
+ /// The subexpression to replace.
+ /// What to put in its place.
+ public static Transformation Substitution(Entity what, Entity with)
+ => new SubstitutionTransformation(
+ what ?? throw new ArgumentNullException(nameof(what)),
+ with ?? throw new ArgumentNullException(nameof(with)));
+
+ ///
+ /// The symbolic derivative over , as
+ /// computes it.
+ ///
+ /// The variable to differentiate over.
+ public static Transformation Differentiation(Variable variable)
+ => new DifferentiationTransformation(variable ?? throw new ArgumentNullException(nameof(variable)));
+
+ ///
+ /// The antiderivative over , without the constant
+ /// of integration — is what adds it.
+ ///
+ /// The variable to integrate over.
+ ///
+ /// Where no antiderivative is found this says so, by having no answer.
+ /// hands back an unevaluated
+ /// instead, which is the same claim in the shape 1.x
+ /// callers expect.
+ ///
+ public static Transformation Integration(Variable variable)
+ => new IntegrationTransformation(variable ?? throw new ArgumentNullException(nameof(variable)));
+
+ ///
+ /// The limit over as it approaches
+ /// from .
+ ///
+ /// The variable that approaches.
+ /// Where it approaches.
+ /// From which side.
+ ///
+ /// The limit as computed, not further simplified —
+ /// is what tidies it. Where the limit cannot be settled this has no answer, rather
+ /// than the unevaluated node the 1.x method returns;
+ /// neither is a claim that the limit does not exist.
+ ///
+ public static Transformation LimitAt(Variable variable, Entity destination, ApproachFrom side)
+ => new LimitTransformation(
+ variable ?? throw new ArgumentNullException(nameof(variable)),
+ destination ?? throw new ArgumentNullException(nameof(destination)),
+ side);
+
+ ///
+ /// The transformations that differ only by a level, each built at most once, so that
+ /// an ordinary Simplify() allocates nothing to reach its transformation.
+ ///
+ ///
+ /// Filled on demand rather than in the static initialiser, and it has to be. Building
+ /// the factorisation entry reads , whose sets in turn build
+ /// transformations of their own; doing that while this type is still initialising
+ /// makes the two types depend on each other's initialisation, and one of them then
+ /// reads the other's fields before they are set. Two threads arriving together may
+ /// each build the same entry; the entries are equivalent and immutable, so whichever
+ /// reference lands is the one everyone then uses.
+ ///
+ private sealed class LevelledCache
+ {
+ // The levels Simplify, Expand and Factorize are actually asked for: the default
+ // 2, the negated -level Simplify passes itself, and a little room either side.
+ private const int Lowest = -4;
+ private const int Highest = 4;
+
+ [ConcurrentField]
+ internal static readonly LevelledCache Simplification = new();
+ [ConcurrentField]
+ internal static readonly LevelledCache Expansion = new();
+ [ConcurrentField]
+ internal static readonly LevelledCache Factorization = new();
+
+ private readonly Transformation?[] cached = new Transformation?[Highest - Lowest + 1];
+
+ internal Transformation For(int level, Func make)
+ => level < Lowest || level > Highest
+ ? make(level)
+ : cached[level - Lowest] ??= make(level);
+ }
+
+ private sealed class InnerSimplificationTransformation : Transformation
+ {
+ public override string Name => "inner-simplify";
+
+ public override TransformationRelation Relation => TransformationRelation.Equivalence;
+
+ public override Soundness Soundness => Soundness.SoundUnderAssumptions;
+
+ protected override Entity? ApplyCore(Entity input) => input.InnerSimplified;
+ }
+
+ private sealed class SimplificationTransformation : Transformation
+ {
+ private readonly int level;
+
+ internal SimplificationTransformation(int level) => this.level = level;
+
+ public override string Name => $"simplify[{level}]";
+
+ public override TransformationRelation Relation => TransformationRelation.Equivalence;
+
+ public override Soundness Soundness => Soundness.SoundUnderAssumptions;
+
+ protected override Entity? ApplyCore(Entity input) => Simplificator.Simplify(input, level);
+ }
+
+ private sealed class ExpansionTransformation : Transformation
+ {
+ private readonly int level;
+
+ internal ExpansionTransformation(int level) => this.level = level;
+
+ public override string Name => $"expand[{level}]";
+
+ public override TransformationRelation Relation => TransformationRelation.Equivalence;
+
+ public override Soundness Soundness => Soundness.SoundUnderAssumptions;
+
+ protected override Entity? ApplyCore(Entity input) => input.ExpandOverSum(level);
+ }
+
+ private sealed class SubstitutionTransformation : Transformation
+ {
+ private readonly Entity what, with;
+
+ internal SubstitutionTransformation(Entity what, Entity with) => (this.what, this.with) = (what, with);
+
+ public override string Name => $"substitute[{what.Stringize()} := {with.Stringize()}]";
+
+ // The output is a different expression from the input, not another way of
+ // writing it, so subtracting the two means nothing.
+ public override TransformationRelation Relation => TransformationRelation.Derivation;
+
+ // Replacing every occurrence of a subexpression by another is valid whatever
+ // the two are; nothing about it is conditional.
+ public override Soundness Soundness => Soundness.Sound;
+
+ protected override Entity? ApplyCore(Entity input) => input.Substitute(what, with);
+ }
+
+ private sealed class DifferentiationTransformation : Transformation
+ {
+ private readonly Variable variable;
+
+ internal DifferentiationTransformation(Variable variable) => this.variable = variable;
+
+ public override string Name => $"differentiate[{variable.Name}]";
+
+ public override TransformationRelation Relation => TransformationRelation.Derivation;
+
+ // The derivative is the derivative where the expression is differentiable, and
+ // an unevaluated Derivativef node where this library does not know a rule.
+ public override Soundness Soundness => Soundness.SoundUnderAssumptions;
+
+ protected override Entity? ApplyCore(Entity input) => input.DifferentiateOnce(variable);
+ }
+
+ private sealed class IntegrationTransformation : Transformation
+ {
+ private readonly Variable variable;
+
+ internal IntegrationTransformation(Variable variable) => this.variable = variable;
+
+ public override string Name => $"integrate[{variable.Name}]";
+
+ public override TransformationRelation Relation => TransformationRelation.Derivation;
+
+ public override Soundness Soundness => Soundness.SoundUnderAssumptions;
+
+ protected override Entity? ApplyCore(Entity input)
+ => Functions.Algebra.Integration.ComputeIndefiniteIntegral(input.InnerSimplified, variable)?.InnerSimplified;
+ }
+
+ private sealed class LimitTransformation : Transformation
+ {
+ private readonly Variable variable;
+ private readonly Entity destination;
+ private readonly ApproachFrom side;
+
+ internal LimitTransformation(Variable variable, Entity destination, ApproachFrom side)
+ => (this.variable, this.destination, this.side) = (variable, destination, side);
+
+ public override string Name => $"limit[{variable.Name} -> {destination.Stringize()}, {side}]";
+
+ public override TransformationRelation Relation => TransformationRelation.Derivation;
+
+ public override Soundness Soundness => Soundness.SoundUnderAssumptions;
+
+ protected override Entity? ApplyCore(Entity input)
+ => LimitFunctional.ComputeLimit(input, variable, destination, side);
+ }
+ }
+}
diff --git a/Sources/AngouriMath/Core/Transformations/Transformation.Combinators.cs b/Sources/AngouriMath/Core/Transformations/Transformation.Combinators.cs
new file mode 100644
index 000000000..e9375356c
--- /dev/null
+++ b/Sources/AngouriMath/Core/Transformations/Transformation.Combinators.cs
@@ -0,0 +1,141 @@
+//
+// 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;
+
+namespace AngouriMath.Core.Transformations
+{
+ partial class Transformation
+ {
+ ///
+ /// Runs this transformation and then on what it produced.
+ /// Where either step has no answer, the composition has none: a step that could not
+ /// be settled is not silently skipped.
+ ///
+ /// The transformation to apply to this one's output.
+ public Transformation Then(Transformation next)
+ => new SequentialTransformation(this, next ?? throw new ArgumentNullException(nameof(next)));
+
+ ///
+ /// Runs this transformation times in a row. The bound is
+ /// the caller's, which is what keeps repetition terminating by construction rather
+ /// than by the good behaviour of whatever is being repeated.
+ ///
+ /// How many applications; must not be negative.
+ public Transformation Repeat(int times)
+ => times < 0
+ ? throw new ArgumentOutOfRangeException(nameof(times))
+ : new RepeatedTransformation(this, times);
+
+ ///
+ /// Runs this transformation until its output stops changing, giving up after
+ /// applications.
+ ///
+ /// The bound on applications; must be positive.
+ ///
+ /// Hitting the bound is reported as no answer rather than as the last value
+ /// reached. 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. It is also what lets a test assert that a rule set has a
+ /// fixed point on a given expression instead of assuming it.
+ ///
+ public Transformation UntilStable(int maxIterations)
+ => maxIterations < 1
+ ? throw new ArgumentOutOfRangeException(nameof(maxIterations))
+ : new StableTransformation(this, maxIterations);
+
+ ///
+ /// The weaker of two justifications. Composing anything with a heuristic gives a
+ /// heuristic; nothing composes upwards into a stronger claim.
+ ///
+ private static Soundness Weaker(Soundness one, Soundness another)
+ => one > another ? one : another;
+
+ ///
+ /// A chain is an equivalence only if every link is. One derivation anywhere in it
+ /// means the end no longer denotes the same value as the start.
+ ///
+ private static TransformationRelation Combine(TransformationRelation one, TransformationRelation another)
+ => one is TransformationRelation.Equivalence && another is TransformationRelation.Equivalence
+ ? TransformationRelation.Equivalence
+ : TransformationRelation.Derivation;
+
+ private sealed class SequentialTransformation : Transformation
+ {
+ private readonly Transformation first, second;
+
+ internal SequentialTransformation(Transformation first, Transformation second)
+ => (this.first, this.second) = (first, second);
+
+ public override string Name => $"{first.Name} then {second.Name}";
+
+ public override TransformationRelation Relation => Combine(first.Relation, second.Relation);
+
+ public override Soundness Soundness => Weaker(first.Soundness, second.Soundness);
+
+ protected override Entity? ApplyCore(Entity input)
+ => first.Apply(input).Output is { } intermediate
+ ? second.Apply(intermediate).Output
+ : null;
+ }
+
+ private sealed class RepeatedTransformation : Transformation
+ {
+ private readonly Transformation inner;
+ private readonly int times;
+
+ internal RepeatedTransformation(Transformation inner, int times)
+ => (this.inner, this.times) = (inner, times);
+
+ public override string Name => $"{inner.Name} x{times}";
+
+ public override TransformationRelation Relation => inner.Relation;
+
+ public override Soundness Soundness => inner.Soundness;
+
+ protected override Entity? ApplyCore(Entity input)
+ {
+ var current = input;
+ for (var i = 0; i < times; i++)
+ if (inner.Apply(current).Output is { } next)
+ current = next;
+ else
+ return null;
+ return current;
+ }
+ }
+
+ private sealed class StableTransformation : Transformation
+ {
+ private readonly Transformation inner;
+ private readonly int maxIterations;
+
+ internal StableTransformation(Transformation inner, int maxIterations)
+ => (this.inner, this.maxIterations) = (inner, maxIterations);
+
+ public override string Name => $"{inner.Name} until stable (<={maxIterations})";
+
+ public override TransformationRelation Relation => inner.Relation;
+
+ public override Soundness Soundness => inner.Soundness;
+
+ protected override Entity? ApplyCore(Entity input)
+ {
+ var current = input;
+ for (var i = 0; i < maxIterations; i++)
+ {
+ if (inner.Apply(current).Output is not { } next)
+ return null;
+ if (next == current)
+ return current;
+ current = next;
+ }
+ return null;
+ }
+ }
+ }
+}
diff --git a/Sources/AngouriMath/Core/Transformations/Transformation.cs b/Sources/AngouriMath/Core/Transformations/Transformation.cs
new file mode 100644
index 000000000..8d45b238a
--- /dev/null
+++ b/Sources/AngouriMath/Core/Transformations/Transformation.cs
@@ -0,0 +1,105 @@
+//
+// 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;
+
+namespace AngouriMath.Core.Transformations
+{
+ ///
+ /// A named mathematical operation that consumes an and produces
+ /// one, together with enough about itself — what it claims, how well justified the
+ /// claim is — to be inspected and composed rather than only invoked.
+ ///
+ ///
+ ///
+ /// This is the layer the 1.x entry points sit on:
+ /// , and
+ /// are adapters over the transformations named
+ /// below, and the algorithms underneath are unchanged. Callers who only want an answer
+ /// should keep using those methods; this type is for callers who want to know which
+ /// operation produced it, or to build an operation out of others.
+ ///
+ ///
+ /// Deterministic: the same transformation applied to the same expression under the same
+ /// gives the same result. Composition is by explicit
+ /// ordering — there is no registry that decides what to run next, and nothing here
+ /// consults reflection, so the layer stays trimmable and AOT-publishable.
+ ///
+ ///
+ /// Experimental. The three concepts — a transformation, its relation, its
+ /// soundness tier — are meant to last; the catalogue of factories will grow and the
+ /// signatures here may still move. The stable surface is and the
+ /// methods on .
+ ///
+ ///
+ ///
+ ///
+ /// using System;
+ /// using AngouriMath;
+ /// using AngouriMath.Core.Transformations;
+ ///
+ /// Entity expr = "(x + 1) ^ 2";
+ /// var result = Transformation.Expansion.Apply(expr);
+ /// Console.WriteLine(result.Output);
+ /// Console.WriteLine(result.Relation);
+ /// Console.WriteLine(result.Soundness);
+ ///
+ /// Prints
+ ///
+ /// 1 + 2 * x + x ^ 2
+ /// Equivalence
+ /// SoundUnderAssumptions
+ ///
+ ///
+ public abstract partial class Transformation
+ {
+ ///
+ /// A stable identity for this operation, used in diagnostics and in the failure a
+ /// caller is handed. Composed transformations name their parts.
+ ///
+ public abstract string Name { get; }
+
+ /// What this operation claims about its output relative to its input.
+ public abstract TransformationRelation Relation { get; }
+
+ /// How well justified that claim is.
+ public abstract Soundness Soundness { get; }
+
+ ///
+ /// Does the work. Returns to mean "I could not settle this" —
+ /// never an unevaluated node of the input, and never for a
+ /// question that merely went unanswered.
+ ///
+ /// The expression to transform.
+ protected abstract Entity? ApplyCore(Entity input);
+
+ ///
+ /// Applies the transformation, reporting what happened rather than only the value.
+ ///
+ /// The expression to transform.
+ ///
+ /// The input, the output where there is one, and this transformation. See
+ /// for the case where there is none.
+ ///
+ public TransformationResult Apply(Entity input)
+ {
+ if (input is null)
+ throw new ArgumentNullException(nameof(input));
+ return new TransformationResult(this, input, ApplyCore(input));
+ }
+
+ ///
+ /// Applies the transformation and hands back the input untouched where it produced
+ /// no answer — the convention the 1.x methods have always followed.
+ ///
+ /// The expression to transform.
+ public Entity ApplyOrKeep(Entity input) => Apply(input).OutputOrInput;
+
+ ///
+ public override string ToString() => Name;
+ }
+}
diff --git a/Sources/AngouriMath/Core/Transformations/TransformationRelation.cs b/Sources/AngouriMath/Core/Transformations/TransformationRelation.cs
new file mode 100644
index 000000000..99bee6c50
--- /dev/null
+++ b/Sources/AngouriMath/Core/Transformations/TransformationRelation.cs
@@ -0,0 +1,33 @@
+//
+// 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.
+//
+
+namespace AngouriMath.Core.Transformations
+{
+ ///
+ /// What a claims about its output relative to its input.
+ /// Without this, would be meaningless: "sound" is only a
+ /// statement about some relation, and the relation is not the same one for every
+ /// operation.
+ ///
+ public enum TransformationRelation
+ {
+ ///
+ /// The output denotes the same mathematical value as the input, so
+ /// input - output is zero wherever both are defined. Simplification,
+ /// expansion, factorisation and rewriting all claim this.
+ ///
+ Equivalence,
+
+ ///
+ /// The output is a different mathematical object computed from the input — a
+ /// derivative, an antiderivative, a limit, an instance under a substitution.
+ /// Subtracting it from the input means nothing, and a test that does so is testing
+ /// nothing.
+ ///
+ Derivation
+ }
+}
diff --git a/Sources/AngouriMath/Core/Transformations/TransformationResult.cs b/Sources/AngouriMath/Core/Transformations/TransformationResult.cs
new file mode 100644
index 000000000..c10dc2fa5
--- /dev/null
+++ b/Sources/AngouriMath/Core/Transformations/TransformationResult.cs
@@ -0,0 +1,65 @@
+//
+// 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.
+//
+
+namespace AngouriMath.Core.Transformations
+{
+ ///
+ /// What one application of a produced: the input, the
+ /// output if there was one, and which transformation was asked.
+ ///
+ ///
+ /// A struct, so that routing an ordinary Simplify or Differentiate call
+ /// through this layer costs no allocation.
+ ///
+ public readonly struct TransformationResult
+ {
+ internal TransformationResult(Transformation transformation, Entity input, Entity? output)
+ => (Transformation, Input, Output) = (transformation, input, output);
+
+ /// The transformation that was applied.
+ public Transformation Transformation { get; }
+
+ /// The expression it was applied to.
+ public Entity Input { get; }
+
+ ///
+ /// The result, or where the transformation could not settle
+ /// the question. Null means "no answer" and nothing more — in particular it does not
+ /// mean the answer does not exist.
+ ///
+ public Entity? Output { get; }
+
+ /// Whether there is an answer at all.
+ public bool Succeeded => Output is not null;
+
+ ///
+ /// Whether there is an answer and it differs from the input. A transformation that
+ /// succeeded without changing anything has reached a fixed point, which is what
+ /// looks for.
+ ///
+ public bool Changed => Output is not null && Output != Input;
+
+ ///
+ /// The answer where there is one, the untouched input otherwise. This is what the
+ /// 1.x API surface wants: those methods have always returned the original
+ /// expression rather than nothing when they could not improve on it.
+ ///
+ public Entity OutputOrInput => Output ?? Input;
+
+ /// What the transformation claims relates to .
+ public TransformationRelation Relation => Transformation.Relation;
+
+ /// How well justified that claim is.
+ public Soundness Soundness => Transformation.Soundness;
+
+ ///
+ public override string ToString()
+ => Output is null
+ ? $"{Transformation.Name}: no answer for {Input.Stringize()}"
+ : $"{Transformation.Name}: {Input.Stringize()} -> {Output.Stringize()}";
+ }
+}
diff --git a/Sources/AngouriMath/Docs/Contributing/README.md b/Sources/AngouriMath/Docs/Contributing/README.md
index 31427ac7b..e3028dceb 100644
--- a/Sources/AngouriMath/Docs/Contributing/README.md
+++ b/Sources/AngouriMath/Docs/Contributing/README.md
@@ -10,7 +10,9 @@ If you aren't sure about what to add, you may want to check the current projects
1. General information
2. Adding a new node (function, operator)
3. Improve parser
-4. Adding a public member — out of date; the `PublicApi.*.txt` files
+4. Transformations — the layer the 1.x entry points sit on, and
+ how to add the next rule set or transformation
+5. Adding a public member — out of date; the `PublicApi.*.txt` files
and the analyzer that required them are no longer in the tree
See also BREAKING-CHANGES.md, where a change that makes
diff --git a/Sources/AngouriMath/Docs/Contributing/Transformations.md b/Sources/AngouriMath/Docs/Contributing/Transformations.md
new file mode 100644
index 000000000..e9fec5a64
--- /dev/null
+++ b/Sources/AngouriMath/Docs/Contributing/Transformations.md
@@ -0,0 +1,132 @@
+# Transformations
+
+`AngouriMath.Core.Transformations` is the layer the 1.x mathematical entry points sit on. It exists
+so that an operation is a **value** — something with a name, a stated claim, and a way to be
+composed — instead of only a method you call.
+
+It is the first step of the rewrite-engine layer in
+[#746](https://github.com/asc-community/AngouriMath/issues/746). It is deliberately small, and it is
+**experimental**: the three concepts are meant to last, the catalogue will grow, and the signatures
+may still move. The stable surface is `MathS` and the methods on `Entity`.
+
+## Why it is not just an interface with `Apply`
+
+Three things have to be said about a mathematical operation before it can be composed with another
+one, and none of them fit in `Entity -> Entity`:
+
+**What it claims.** `Simplify` returns another way of writing the same value. `Differentiate` returns
+a different object. Both are `Entity -> Entity` and confusing them is how you get a test that
+subtracts a derivative from its integrand and asserts zero. `TransformationRelation` is `Equivalence`
+or `Derivation`, and a chain is an equivalence only if every link is.
+
+**How well justified the claim is.** `Soundness` is `Sound`, `SoundUnderAssumptions` or `Heuristic`,
+and composing takes the weaker of two — nothing composes upwards. The tier is **declared, not
+checked**: it is a claim to argue with, not a guarantee, which is why the registry starts
+conservative and why tightening a label needs an argument while loosening one does not.
+
+**Whether it answered at all.** `ApplyCore` returns `null` for "I could not settle this", which is
+the same distinction [AGENTS.md](../../../../AGENTS.md) draws between an unevaluated node and `NaN`.
+`Transformation.Integration("x")` applied to `e ^ (x ^ 2)` has no answer; `Entity.Integrate` makes the
+same claim in the shape its callers expect, by handing back an unevaluated `Integralf`.
+
+## The pieces
+
+| | |
+|---|---|
+| `Transformation` | the operation: `Name`, `Relation`, `Soundness`, `Apply`, and the static catalogue |
+| `TransformationResult` | input, output-or-nothing, and which transformation ran. A struct, so routing an ordinary call through this layer allocates nothing |
+| `RewriteRuleSet` | a named, attributed group of rewrites — `Name`, `Description`, `Relation`, `Soundness`, `ApplyOnce` |
+| `RewriteRules` | the registry: every shipped set, explicitly listed, enumerable through `All` in a fixed order |
+
+Composition is `Then`, `Repeat(n)` and `UntilStable(max)`. All three take their bound from the
+caller: there is no unbounded rewrite loop anywhere in the layer, and `UntilStable` reports hitting
+its bound as **no answer** rather than as the value it happened to be holding, so that a rule set
+which does not converge is visible instead of silently truncated.
+
+Registration is static and explicit. No assembly scanning, no `Activator`, no reflection — the layer
+stays trimmable and NativeAOT-publishable, and `RewriteRules.All` is in an order that does not depend
+on hashing or on which type loaded first.
+
+## What already uses it
+
+```
+Entity.Simplify(level) -> Transformation.SimplificationAtLevel(level) -> Simplificator.Simplify
+Entity.Expand(level) -> Transformation.ExpansionAtLevel(level) -> Entity.ExpandOverSum
+Entity.Factorize(level) -> Transformation.FactorizationAtLevel(level) -> RewriteRules, composed
+Entity.Differentiate(x) -> Transformation.Differentiation(x) -> Entity.DifferentiateOnce
+Entity.Integrate(x) -> Transformation.Integration(x) -> Integration.ComputeIndefiniteIntegral
+Entity.Limit(x, to, side) -> Transformation.LimitAt(x, to, side) -> LimitFunctional.ComputeLimit
+Simplificator.SimplifyChildren -> a composed chain of four registry entries
+```
+
+Two of these are real ports rather than wrappers. `Factorize` is no longer a method that names its
+own rules: it is `PerfectSquare`, then `Factorization`, then a tidying pass, repeated `level` times,
+built out of the registry. `SimplifyChildren` — which every stage of the simplification pipeline runs
+— is a chain composed once, statically, out of four registry entries instead of a hand-written run of
+`Replace` calls. Both produce exactly what they produced before.
+
+Everything else is a thin adapter over the algorithm that was already there. **Nothing that worked
+was rewritten to make the architecture tidier.**
+
+## What is deliberately not here
+
+**`Solve` is not a transformation.** It consumes a *goal* — an equation, with a variable to solve for
+— and produces a solution set, and the honest place for it is a tactic layer where a goal can become
+subgoals. `Entity.Set` being an `Entity` means `Solve` would type-check as `Entity -> Entity`; that
+is exactly why forcing it in would be a mistake, since it would compile while saying nothing true
+about what the operation does. Same for `Isolate`, `Eliminate` and `Parametrize` when they arrive.
+
+**There are no inverse transformations.** `Expand` and `Factor` are not inverses, and `Unsolve` is not
+a well-defined operation. Where an inverse is mathematically meaningful there is room to add one; the
+API does not invent symmetry that the mathematics does not have.
+
+**`Heuristic` currently has no instance in the catalogue.** That is a statement about what is
+registered so far, not a claim that nothing in the library guesses.
+
+**Most of the pattern table is still only reachable from `Simplificator`.** `RewriteRules` registers
+the ten sets the catalogue is built from. The rest are unchanged and unregistered.
+
+## Adding the next one
+
+A new transformation built from rules that already exist is one line:
+
+```csharp
+public static Transformation Rationalisation { get; }
+ = Rewriting(RewriteRules.RationaliseDenominator).Then(InnerSimplification);
+```
+
+A new rule set is five, and registering it gets it enumeration, an identity, a soundness label, and
+the tests that run over `RewriteRules.All` — including the one that asserts it reaches a fixed point
+rather than rewriting in a cycle:
+
+```csharp
+public static RewriteRuleSet Power { get; } = new(
+ nameof(Power),
+ "Gathers and splits powers, roots and logarithms.",
+ TransformationRelation.Equivalence,
+ Soundness.SoundUnderAssumptions,
+ Patterns.PowerRules);
+```
+
+Add it to `RewriteRules.All` in the same change — the list is explicit so that its order is a
+decision rather than an accident.
+
+Two things to get right:
+
+- **State the relation honestly.** If the output is not another way of writing the input, it is
+ `Derivation`, and the equivalence property test will correctly leave it alone.
+- **Do not claim `Sound` without an argument.** Every rule set shipped today is
+ `SoundUnderAssumptions`, and the test over `RewriteRules.All` enforces that, so promoting one means
+ changing that test and saying why in the same change.
+
+## What the next step is
+
+The unit here is the rule *set*, not the single `pattern -> replacement` line, because every rewrite
+in this library is a case of one `switch` and the compiler turns that into a type test and a jump —
+splitting each case into an object would trade one dispatch per node for one delegate call per rule
+per node, on the hottest path there is. Making the individual rewrites addressable without paying
+that is the next piece of work, and nothing here forecloses it: a set whose rewrites become
+individually reachable keeps its name and its entry in the registry.
+
+After that, in dependency order: the goal/tactic layer that `Solve` belongs in, and then derivations,
+which want every step to be attributable — which is what naming the steps was for.
diff --git a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs
index 6af8a1ae2..bef6ba8df 100644
--- a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs
@@ -5,6 +5,7 @@
// Website: https://am.angouri.org.
//
+using AngouriMath.Core.Transformations;
using PeterO.Numbers;
namespace AngouriMath
@@ -43,6 +44,14 @@ partial record Entity
///
///
public Entity Differentiate(Variable variable)
+ => Transformation.Differentiation(variable).ApplyOrKeep(this);
+
+ ///
+ /// What does, reachable by
+ /// without going back
+ /// through the public method and round again.
+ ///
+ internal Entity DifferentiateOnce(Variable variable)
=> InnerDifferentiate(variable).InnerSimplified;
///
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
index 1431fa30d..ed61893a5 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs
@@ -5,6 +5,7 @@
// Website: https://am.angouri.org.
//
+using AngouriMath.Core.Transformations;
using AngouriMath.Extensions;
using AngouriMath.Functions.Algebra;
using PeterO.Numbers;
@@ -31,7 +32,7 @@ partial record Entity
/// https://github.com/asc-community/AngouriMath/issues/772
///
public Entity Integrate(Variable x) =>
- Integration.ComputeIndefiniteIntegral(InnerSimplified, x)?.InnerSimplified is { } antiderivative
+ Transformation.Integration(x).Apply(this).Output is { } antiderivative
? antiderivative + (antiderivative.VarsAndConsts.Contains("C") ? Variable.CreateUnique(antiderivative, "C") : "C")
: new Integralf(this, x, null);
///
@@ -45,7 +46,7 @@ public Entity Integrate(Variable x) =>
/// An integrated expression. It might remain the same or be transformed into nodes with no integrals.
///
public Entity Integrate(Variable x, Entity from, Entity to) =>
- Integration.ComputeIndefiniteIntegral(InnerSimplified, x)?.InnerSimplified is { } antiderivative
+ Transformation.Integration(x).Apply(this).Output is { } antiderivative
? antiderivative.Substitute(x, to) - antiderivative.Substitute(x, from)
: new Integralf(this, x, (from, to));
}
diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Limit.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Limit.Definition.cs
index 8dc23ea0d..1754e1d61 100644
--- a/Sources/AngouriMath/Functions/Continuous/Limits/Limit.Definition.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Limits/Limit.Definition.cs
@@ -34,6 +34,7 @@ namespace AngouriMath.Core
namespace AngouriMath
{
using Core;
+ using Core.Transformations;
using static Functions.Algebra.LimitFunctional;
partial record Entity
{
@@ -58,8 +59,8 @@ partial record Entity
/// cannot be determined
///
public Entity Limit(Variable x, Entity destination, ApproachFrom side)
- {
- var res = ComputeLimit(this, x, destination, side);
+ {
+ var res = Transformation.LimitAt(x, destination, side).Apply(this).Output;
if (res is null) return new Limitf(this, x, destination, side);
return res.InnerSimplified;
}
@@ -118,7 +119,9 @@ public Entity Limit(Variable x, Entity destination, ApproachFrom side)
///
public Entity Limit(Variable x, Entity destination)
{
- var res = ComputeLimit(this, x, destination, ApproachFrom.BothSides);
+ // The NaN test is on the limit as computed, before it is tidied: an expression
+ // that inner-simplifies to NaN is not the same claim as one that came back NaN.
+ var res = Transformation.LimitAt(x, destination, ApproachFrom.BothSides).Apply(this).Output;
if (res is null || res == MathS.NaN)
return new Limitf(this, x, destination, ApproachFrom.BothSides).InnerSimplified;
return res.InnerSimplified;
diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
index 724c1bfec..8c8786c0d 100644
--- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
+++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
@@ -5,6 +5,7 @@
// Website: https://am.angouri.org.
//
+using AngouriMath.Core.Transformations;
using HonkSharp.Laziness;
namespace AngouriMath
@@ -204,6 +205,14 @@ private Entity InnerSimplifyWithCheck(bool isExact)
///
public Entity Expand(int level = 2)
+ => Transformation.ExpansionAtLevel(level).ApplyOrKeep(this);
+
+ ///
+ /// What does, reachable by
+ /// without going back through
+ /// the public method and round again.
+ ///
+ internal Entity ExpandOverSum(int level)
{
static Entity Expand_(Entity e, int level) =>
level <= 1
@@ -330,11 +339,16 @@ private static Entity CollectLikeTerms(Entity expanded)
/// (1 + x) * (1 + y)
///
///
- public Entity Factorize(int level = 2) => level <= 1
- // InnerSimplified, so that the factors come back finished: the rules leave
- // x ^ 1 where they mean x, and sqrt(4) where they mean 2.
- ? this.Replace(Patterns.PerfectSquareRules).Replace(Patterns.FactorizeRules).InnerSimplified
- : this.Replace(Patterns.PerfectSquareRules).Replace(Patterns.FactorizeRules).InnerSimplified.Factorize(level - 1);
+ ///
+ /// One pass is the perfect-square rules, then the factorisation rules, then
+ /// -- so that the factors come back finished, since
+ /// the rules leave x ^ 1 where they mean x and sqrt(4) where
+ /// they mean 2 -- and is how many times that runs.
+ /// Built out of by
+ /// .
+ ///
+ public Entity Factorize(int level = 2)
+ => Transformation.FactorizationAtLevel(level).ApplyOrKeep(this);
///
/// Simplifies an equation ( e.g. (x - y) * (x + y) -> x^2 - y^2, but 3 * x + y * x = (3 + y) * x )
@@ -386,7 +400,8 @@ public Entity Factorize(int level = 2) => level <= 1
/// cos((x * y) ^ 2 + x * y) * (2 * x * y ^ 2 + y)
///
///
- public Entity Simplify(int level = 2) => Simplificator.Simplify(this, level);
+ public Entity Simplify(int level = 2)
+ => Transformation.SimplificationAtLevel(level).ApplyOrKeep(this);
/// Finds all alternative forms of an expression sorted by their complexity
///
diff --git a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs
index 2f0eaf900..695e22cb0 100644
--- a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs
+++ b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs
@@ -6,7 +6,9 @@
//
using System;
+using AngouriMath.Core;
using AngouriMath.Core.Multithreading;
+using AngouriMath.Core.Transformations;
using PeterO.Numbers;
namespace AngouriMath.Functions
@@ -22,14 +24,27 @@ internal static Entity PickSimplest(Entity one, Entity another)
/// See more details in
internal static Entity Simplify(Entity expr, int level) => Alternate(expr, level).First().InnerSimplified;
- internal static Entity SimplifyChildren(Entity expr)
- {
- return expr.Replace(Patterns.InvertNegativePowers)
- .Replace(Patterns.InvertNegativeMultipliers).Replace(
- Patterns.SortRules(TreeAnalyzer.SortLevel.HIGH_LEVEL)
- )
- .InnerSimplified.Replace(Patterns.CommonRules).InnerSimplified;
- }
+ ///
+ /// The tidying pass every stage of runs: get
+ /// the quotients and the signs into their usual shape, put the operands in order,
+ /// then collect like terms.
+ ///
+ ///
+ /// Composed once, statically, out of rather than written
+ /// out as a chain of Replace calls -- so the sequence is a value that can be
+ /// named, printed and tested, and so each stage of it is a registry entry other
+ /// code can reach on its own. The passes and their order are unchanged.
+ ///
+ [ConstantField]
+ private static readonly Transformation simplifyChildren =
+ Transformation.Rewriting(RewriteRules.InvertNegativePowers)
+ .Then(Transformation.Rewriting(RewriteRules.InvertNegativeMultipliers))
+ .Then(Transformation.Rewriting(RewriteRules.CanonicalOrder))
+ .Then(Transformation.InnerSimplification)
+ .Then(Transformation.Rewriting(RewriteRules.Common))
+ .Then(Transformation.InnerSimplification);
+
+ internal static Entity SimplifyChildren(Entity expr) => simplifyChildren.ApplyOrKeep(expr);
/// Finds all alternative forms of an expression
internal static IEnumerable Alternate(Entity src, int level)
diff --git a/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs b/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs
new file mode 100644
index 000000000..b88301f7e
--- /dev/null
+++ b/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs
@@ -0,0 +1,416 @@
+//
+// 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;
+using AngouriMath.Core;
+using AngouriMath.Core.Transformations;
+using Xunit;
+
+namespace AngouriMath.Tests.Core.Transformations
+{
+ ///
+ /// The transformation layer: that it does what it says, that the 1.x methods built on
+ /// it still answer what they answered, and that it is honest about what it could not do.
+ ///
+ [Trait("Area", "Transformations")]
+ public sealed class TransformationTest
+ {
+ ///
+ /// Ordinary textbook-sized expressions, of the kinds the rule sets below are about.
+ ///
+ public static readonly IEnumerable