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
194 changes: 194 additions & 0 deletions Sources/AngouriMath/Core/Transformations/Matching/MatchPattern.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,63 @@ private protected virtual bool TryMatchChoiceCore(
/// </summary>
internal abstract int NodeCount { get; }

/// <summary>
/// Whether every expression <paramref name="other"/> matches, this one matches too — so
/// that <i>this pattern is the more general of the two</i>, and a rule written on it
/// would swallow a rule written on <paramref name="other"/> if it were tried first.
/// </summary>
/// <remarks>
/// <para>
/// <b>The fact behind an ordering that is currently a comment.</b>
/// <c>MatchedRules.CollapseMultipleFractions</c> says of itself that it "is
/// order-dependent, since <c>Mulf(Divf, Divf)</c> has to be tried before
/// <c>Mulf(a, Divf)</c> or the more general rule would swallow the special one". That is
/// this relation, observed by hand and then maintained by hand. Computed instead, the
/// order it implies is derived from the patterns rather than from where somebody typed
/// them.
/// </para>
/// <para>
/// <b>Sound in one direction only.</b> <see langword="true"/> is a claim — every
/// expression the other matches, this matches — and every clause below is structural, so
/// the claim holds for all expressions rather than for the ones a test happened to
/// generate. <see langword="false"/> means <i>not proved</i> and never <i>disproved</i>:
/// a hole carrying a predicate is arbitrary code, an <c>Exact</c> literal can be equal to
/// a value of another runtime type (a rational that reduced to an integer), and an n-ary
/// <c>Gathered</c> pattern matches a family this does not attempt to reason about. Each of
/// those answers <see langword="false"/> and leaves the pair ordered by where it was
/// written, which is what the code did before.
/// </para>
/// <para>
/// <b>It is a matching problem, not a size comparison.</b> A hole repeated across a
/// pattern is an equality constraint — <c>Mulf(a, a)</c> matches strictly less than
/// <c>Mulf(a, b)</c> while having the same node count — so this matches this pattern
/// <i>against</i> the other as a term, carrying an assignment from this pattern's holes to
/// the other's subpatterns and requiring a repeated hole to be assigned consistently.
/// Comparing <see cref="NodeCount"/> would call those two equally general and get the
/// ordering wrong in the one case the ordering exists for.
/// </para>
/// </remarks>
internal bool Subsumes(MatchPattern other)
=> other is not null
&& SubsumesCore(other, new Dictionary<string, MatchPattern>(StringComparer.Ordinal));

/// <summary>
/// <see cref="Subsumes"/>, carrying the assignment from this pattern's holes to the
/// subpatterns of the one being subsumed. Refusing is always sound, so the base answers
/// <see langword="false"/> and a pattern kind that can reason about itself says so.
/// </summary>
private protected virtual bool SubsumesCore(
MatchPattern other, Dictionary<string, MatchPattern> assigned) => false;

/// <summary>
/// Whether two patterns are written the same way, used to check that a repeated hole was
/// assigned consistently. Structural, and stricter than semantic equality: two patterns
/// that mean the same thing while being written differently answer <see langword="false"/>
/// here, which loses a subsumption rather than inventing one.
/// </summary>
private protected virtual bool SameShapeAs(MatchPattern other)
=> ReferenceEquals(this, other);

/// <summary>
/// The expression this pattern stands for under <paramref name="bindings"/>, or
/// <see langword="false"/> where those bindings do not satisfy it.
Expand Down Expand Up @@ -649,6 +706,50 @@ private protected override bool TryMatchOnceCore(

internal override int NodeCount => 1;

/// <summary>
/// A hole is the most general thing a pattern can be, and it subsumes whatever it is
/// allowed to stand for — subject to the two constraints it may carry.
/// </summary>
/// <remarks>
/// <para>
/// A <c>where</c> predicate is arbitrary code over an expression, so what it admits
/// cannot be read off the pattern and this refuses rather than guesses.
/// </para>
/// <para>
/// A required type is checked against what the other pattern <i>guarantees</i> at its
/// root, which is <see cref="RequiredRootType"/> — a necessary condition, and here the
/// direction that makes it usable: a pattern whose root is always a <c>Divf</c> is
/// certainly matched by a hole asking for an <c>Entity</c>. A pattern that guarantees
/// nothing, an <c>Exact</c> literal among them, is refused: two entities can be equal
/// without being the same runtime type, so a literal's own type is not what it
/// guarantees.
/// </para>
/// <para>
/// Then the assignment. A hole seen for the second time must stand for the same thing
/// it stood for the first time, which is what makes <c>Mulf(a, a)</c> strictly less
/// general than <c>Mulf(a, b)</c> rather than equally general.
/// </para>
/// </remarks>
private protected override bool SubsumesCore(
MatchPattern other, Dictionary<string, MatchPattern> assigned)
{
if (where is not null) return false;
if (required is not null
&& (other.RequiredRootType is not { } guaranteed
|| !required.IsAssignableFrom(guaranteed)))
return false;
if (assigned.TryGetValue(name, out var already))
return already.SameShapeAs(other);
assigned[name] = other;
return true;
}

private protected override bool SameShapeAs(MatchPattern other)
=> other is AnyPattern that
&& string.Equals(name, that.name, StringComparison.Ordinal)
&& required == that.required
&& where == that.where;

internal override bool TryBuild(Bindings bindings, out Entity built)
{
built = null!;
Expand Down Expand Up @@ -745,6 +846,17 @@ private protected override bool TryMatchOnceCore(

internal override int NodeCount => 1;

/// <summary>
/// A literal admits exactly one value, so it subsumes only a pattern admitting no
/// more than that — which among the kinds here is the same literal.
/// </summary>
private protected override bool SubsumesCore(
MatchPattern other, Dictionary<string, MatchPattern> assigned)
=> other is ExactPattern that && value.Equals(that.value);

private protected override bool SameShapeAs(MatchPattern other)
=> other is ExactPattern that && value.Equals(that.value);

internal override bool TryBuild(Bindings bindings, out Entity built)
{
built = value;
Expand Down Expand Up @@ -809,6 +921,88 @@ internal NodePattern(Type nodeType, MatchPattern[] children, bool commutative)

internal override int NodeCount => nodeCount;

/// <summary>
/// A node pattern pins the node type and the arity, so it subsumes only another node
/// pattern of that type and arity whose children it subsumes in turn — under one
/// assignment, so that a hole repeated across children has to be assigned the same
/// subpattern in each.
/// </summary>
/// <remarks>
/// <b>Commutativity is a claim about this side.</b> A commutative pattern matches a
/// node in either order, so subsuming needs only <i>one</i> of the two pairings to
/// work; a non-commutative one gets the written pairing and nothing else. When the
/// pattern being subsumed is the commutative one, it matches both orders, so this must
/// cover both — and a non-commutative pattern covers both only when its two children
/// are interchangeable, which the written pairing already decides. The assignment is
/// copied before each attempt, because a pairing that fails half way must not leave
/// its bindings behind for the other one.
/// </remarks>
private protected override bool SubsumesCore(
MatchPattern other, Dictionary<string, MatchPattern> assigned)
{
if (other is not NodePattern that
|| nodeType != that.nodeType
|| children.Length != that.children.Length)
return false;

if (that.commutative && !commutative)
{
var both = new Dictionary<string, MatchPattern>(assigned, StringComparer.Ordinal);
if (!InOrder(that.children, both, swapped: false)) return false;
if (!InOrder(that.children, both, swapped: true)) return false;
Adopt(assigned, both);
return true;
}

if (commutative)
{
var straight = new Dictionary<string, MatchPattern>(assigned, StringComparer.Ordinal);
if (InOrder(that.children, straight, swapped: false))
{
Adopt(assigned, straight);
return true;
}
var crossed = new Dictionary<string, MatchPattern>(assigned, StringComparer.Ordinal);
if (InOrder(that.children, crossed, swapped: true))
{
Adopt(assigned, crossed);
return true;
}
return false;
}

return InOrder(that.children, assigned, swapped: false);
}

private bool InOrder(
MatchPattern[] theirs, Dictionary<string, MatchPattern> assigned, bool swapped)
{
for (var i = 0; i < children.Length; i++)
{
var theirIndex = swapped ? children.Length - 1 - i : i;
if (!children[i].SubsumesCore(theirs[theirIndex], assigned)) return false;
}
return true;
}

private static void Adopt(
Dictionary<string, MatchPattern> into, Dictionary<string, MatchPattern> from)
{
foreach (var pair in from) into[pair.Key] = pair.Value;
}

private protected override bool SameShapeAs(MatchPattern other)
{
if (other is not NodePattern that
|| nodeType != that.nodeType
|| commutative != that.commutative
|| children.Length != that.children.Length)
return false;
for (var i = 0; i < children.Length; i++)
if (!children[i].SameShapeAs(that.children[i])) return false;
return true;
}

internal override IEnumerable<string> BoundNames => children.SelectMany(c => c.BoundNames);

public override string ToString()
Expand Down
112 changes: 106 additions & 6 deletions Sources/AngouriMath/Core/Transformations/Matching/MatchedRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ internal MatchedRuleSet(string name, params MatchedRule[] rules)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
this.rules = rules ?? throw new ArgumentNullException(nameof(rules));
byPriority = ByPriority(this.rules);
}

/// <summary>
Expand All @@ -427,6 +428,85 @@ internal MatchedRuleSet(string name, params MatchedRule[] rules)
/// </summary>
private readonly MatchedRule[] rules;

/// <summary>
/// The same rules in the order they are actually tried: <b>the more specific rule
/// first</b>, and declaration order wherever specificity has no opinion. See
/// <see cref="ByPriority"/>.
/// </summary>
private readonly MatchedRule[] byPriority;

/// <summary>
/// <paramref name="declared"/> ordered so that no rule is tried before a rule its pattern
/// subsumes — the more specific one first — keeping declaration order everywhere else.
/// </summary>
/// <remarks>
/// <para>
/// <b>What this replaces is the position somebody typed a rule at.</b> A set is
/// first-match-wins, so where two rules both fire at a node and disagree, whichever comes
/// first decides the answer. Where one pattern subsumes the other that decision is not a
/// preference: the general rule would swallow the special one and the special one would
/// never fire at all, so the specific rule has to be tried first. That was maintained by
/// hand and written in a comment on one set;
/// <see cref="MatchPattern.Subsumes"/> computes it, and this applies it.
/// </para>
/// <para>
/// <b>It changes no answer today, and that is a measurement rather than an intention.</b>
/// Over the 5,480 within-set rule pairs in <see cref="MatchedRules"/>, subsumption has an
/// opinion about <b>28</b> of them — spread across eight sets, <c>Boolean</c> and
/// <c>InequalityEquality</c> carrying half — and in <b>every one</b> the specific rule is
/// already declared first. None is mutual. So this orders what was already ordered; what
/// it adds is that inserting a general rule above a specific one no longer silently
/// reverses an answer. <c>RulePriorityTest</c> holds the two orders equal, so the file
/// stays readable as well as correct.
/// </para>
/// <para>
/// A stable topological sort, taking the earliest-declared rule that has nothing left
/// which must precede it. Subsumption is conservative, so most pairs constrain nothing and
/// fall through to declaration order. It cannot cycle — a cycle would need each of two
/// rules to be strictly more general than the other — but a cycle is emitted in
/// declaration order rather than trusted not to happen, because a sort that silently drops
/// a rule would be a set quietly missing a rewrite.
/// </para>
/// </remarks>
private static MatchedRule[] ByPriority(MatchedRule[] declared)
{
var count = declared.Length;
if (count < 2) return declared;

// mustPrecede[g] is the set of rules that have to be tried before rule g, which is
// every rule g's pattern strictly subsumes.
var waitingOn = new int[count];
var blocks = new List<int>[count];
for (var g = 0; g < count; g++)
for (var sp = 0; sp < count; sp++)
{
if (g == sp) continue;
if (!declared[g].Left.Subsumes(declared[sp].Left)) continue;
if (declared[sp].Left.Subsumes(declared[g].Left)) continue;
(blocks[sp] ??= new List<int>()).Add(g);
waitingOn[g]++;
}

var ordered = new MatchedRule[count];
var taken = new bool[count];
for (var slot = 0; slot < count; slot++)
{
var next = -1;
for (var i = 0; i < count; i++)
if (!taken[i] && waitingOn[i] == 0) { next = i; break; }
// Only reachable if subsumption were not antisymmetric. Falling back to
// declaration order keeps every rule in the set.
if (next < 0)
for (var i = 0; i < count; i++)
if (!taken[i]) { next = i; break; }
taken[next] = true;
ordered[slot] = declared[next];
if (blocks[next] is { } blocked)
foreach (var g in blocked) waitingOn[g]--;
}
return ordered;
}

// Indexing the rules by the node type each one requires -- the thing a `switch` cannot
// do and a set of values can -- was tried here and is deliberately absent. Measured, a
// per-runtime-type cache of the applicable rules cost 24 bytes per node and 912 bytes on
Expand All @@ -438,7 +518,17 @@ internal MatchedRuleSet(string name, params MatchedRule[] rules)

internal string Name { get; }

/// <summary>The rules, in the order they are tried. <b>Enumerable</b>, which is the whole point.</summary>
/// <summary>
/// The rules, <b>in the order they are written</b>. <b>Enumerable</b>, which is the whole
/// point.
/// </summary>
/// <remarks>
/// Not necessarily the order they are tried in — see <see cref="RulesByPriority"/>, which
/// puts a rule ahead of any rule whose pattern subsumes it. The two are equal today and
/// <c>RulePriorityTest</c> holds them equal, so this is the order to read the set in and to
/// index it by; <see cref="AsAddressable"/> and <see cref="Reversed"/> use it for that
/// reason.
/// </remarks>
internal IReadOnlyList<MatchedRule> Rules => rules;

/// <summary>
Expand Down Expand Up @@ -473,15 +563,25 @@ internal MatchedRuleSet Reversed
internal Soundness Soundness
=> rules.Length == 0 ? Soundness.Sound : rules.Max(rule => rule.Soundness);

/// <summary>The first rule that applies at this node, or null.</summary>
/// <summary>
/// The first rule that applies at this node, or null — first in
/// <see cref="ByPriority"/>'s order, which is the order it would be tried in.
/// </summary>
internal MatchedRule? FirstMatching(Entity expr)
{
foreach (var rule in rules)
foreach (var rule in byPriority)
if (rule.TryApply(expr) is not null)
return rule;
return null;
}

/// <summary>
/// The rules in the order they are tried, which is <see cref="ByPriority"/>'s and not
/// necessarily <see cref="Rules"/>'s. Exposed so that the two can be compared rather than
/// assumed equal.
/// </summary>
internal IReadOnlyList<MatchedRule> RulesByPriority => byPriority;

/// <summary>One rewrite at this node only, leaving children alone.</summary>
/// <summary>
/// These rules as <see cref="RewriteRule"/> values, so that a set written as data can
Expand Down Expand Up @@ -575,11 +675,11 @@ private MatchedRule[] ApplicableTo(Type type)
if (known.TryGetValue(type, out var found))
return found;

var matching = new List<MatchedRule>(rules.Length);
foreach (var rule in rules)
var matching = new List<MatchedRule>(byPriority.Length);
foreach (var rule in byPriority)
if (rule.Left.RequiredRootType is not { } required || required.IsAssignableFrom(type))
matching.Add(rule);
found = matching.Count == rules.Length ? rules : matching.ToArray();
found = matching.Count == byPriority.Length ? byPriority : matching.ToArray();

applicable = new Dictionary<Type, MatchedRule[]>(known) { [type] = found };
return found;
Expand Down
Loading
Loading