Knuth-Bendix completion in Python
Turns a set of equations into a confluent, terminating rewrite system by adding critical-pair rules.
- File
knuth_bendix_completion.py, 431 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/knuth-bendix-completion/. The authors are listed in prov/WM035M.jsonl.
"""Knuth-Bendix completion with simplification, collapse and composition.
A term is a variable, written as a str, or an application, written as a tuple
(f, t1, ..., tn) whose first item is the function symbol; a constant a is
("a",). An equation or a rule is a pair (s, t) of terms. Positions are tuples
of 1-based argument indexes.
complete(equations, greater, max_steps) returns one of
("success", rules) rules is convergent and has the same equational
theory as the equations,
("fail", (s, t)) the normalized equation s = t cannot be oriented,
("give up", rules) max_steps iterations of the main loop were used.
greater(s, t) must be a reduction ordering; lpo(precedence) builds the
lexicographic path ordering. All functions use explicit stacks, so deep terms
do not hit the recursion limit.
"""
import heapq
from itertools import count
from typing import Callable, Optional, Union
Term = Union[str, tuple]
Pair = tuple[Term, Term]
Order = Callable[[Term, Term], bool]
def is_var(t: Term) -> bool:
return isinstance(t, str)
def term_eq(s: Term, t: Term) -> bool:
"""Syntactic equality, without recursion."""
stack = [(s, t)]
while stack:
u, v = stack.pop()
if u is v:
continue
if is_var(u) or is_var(v):
if u != v:
return False
continue
if len(u) != len(v) or u[0] != v[0]:
return False
stack.extend(zip(u[1:], v[1:]))
return True
def variables(t: Term) -> list[str]:
"""The variables of t in left-to-right order of first occurrence."""
out: list[str] = []
seen: set[str] = set()
stack = [t]
while stack:
u = stack.pop()
if is_var(u):
if u not in seen:
seen.add(u)
out.append(u)
else:
stack.extend(reversed(u[1:]))
return out
def size(t: Term) -> int:
n = 0
stack = [t]
while stack:
u = stack.pop()
n += 1
if not is_var(u):
stack.extend(u[1:])
return n
def substitute(t: Term, sigma: dict[str, Term]) -> Term:
"""t sigma. Bound values are inserted as they are, without copying."""
out: list[Term] = []
stack: list[tuple[bool, Term]] = [(False, t)]
while stack:
done, u = stack.pop()
if done:
n = len(u) - 1
args = out[len(out) - n:] if n else []
del out[len(out) - n:]
out.append((u[0],) + tuple(args))
elif is_var(u):
out.append(sigma.get(u, u))
else:
stack.append((True, u))
for a in reversed(u[1:]):
stack.append((False, a))
return out[0]
def match(pattern: Term, term: Term) -> Optional[dict[str, Term]]:
"""Return sigma with pattern sigma = term, or None."""
sigma: dict[str, Term] = {}
stack = [(pattern, term)]
while stack:
p, t = stack.pop()
if is_var(p):
if p in sigma:
if not term_eq(sigma[p], t):
return None
else:
sigma[p] = t
elif is_var(t) or len(p) != len(t) or p[0] != t[0]:
return None
else:
stack.extend(zip(p[1:], t[1:]))
return sigma
def _walk(t: Term, sigma: dict[str, Term]) -> Term:
while is_var(t) and t in sigma:
t = sigma[t]
return t
def _occurs(x: str, t: Term, sigma: dict[str, Term]) -> bool:
stack = [t]
while stack:
u = _walk(stack.pop(), sigma)
if is_var(u):
if u == x:
return True
else:
stack.extend(u[1:])
return False
def _resolve(t: Term, sigma: dict[str, Term]) -> Term:
"""Apply a triangular substitution completely."""
out: list[Term] = []
stack: list[tuple[bool, Term]] = [(False, t)]
while stack:
done, u = stack.pop()
if done:
n = len(u) - 1
args = out[len(out) - n:] if n else []
del out[len(out) - n:]
out.append((u[0],) + tuple(args))
continue
u = _walk(u, sigma)
if is_var(u):
out.append(u)
else:
stack.append((True, u))
for a in reversed(u[1:]):
stack.append((False, a))
return out[0]
def unify(s: Term, t: Term) -> Optional[dict[str, Term]]:
"""A most general unifier of s and t, fully applied, or None."""
sigma: dict[str, Term] = {}
stack = [(s, t)]
while stack:
a, b = stack.pop()
a, b = _walk(a, sigma), _walk(b, sigma)
if is_var(a) and is_var(b) and a == b:
continue
if is_var(b) and not is_var(a):
a, b = b, a
if is_var(a):
if _occurs(a, b, sigma):
return None
sigma[a] = b
continue
if len(a) != len(b) or a[0] != b[0]:
return None
stack.extend(zip(a[1:], b[1:]))
return {x: _resolve(x, sigma) for x in sigma}
def subterms(t: Term) -> list[tuple[tuple[int, ...], Term]]:
"""All (position, subterm) pairs of t in preorder."""
out = []
stack: list[tuple[tuple[int, ...], Term]] = [((), t)]
while stack:
p, u = stack.pop()
out.append((p, u))
if not is_var(u):
for i in range(len(u) - 1, 0, -1):
stack.append((p + (i,), u[i]))
return out
def replace(t: Term, p: tuple[int, ...], s: Term) -> Term:
"""t[s]_p, rebuilding only the path to p."""
path = []
u = t
for i in p:
path.append((u, i))
u = u[i]
for node, i in reversed(path):
s = node[:i] + (s,) + node[i + 1:]
return s
def rename(t: Term, names: dict[str, str]) -> Term:
return substitute(t, {x: y for x, y in names.items()})
_NAMES = ["x", "y", "z", "u", "v", "w"]
def canonical(pair: Pair) -> Pair:
"""Rename the variables of a pair to x, y, z, u, v, w, x7, x8, ..."""
s, t = pair
seen = variables(("=", s, t))
names = {v: (_NAMES[i] if i < len(_NAMES) else f"x{i + 1}") for i, v in enumerate(seen)}
return rename(s, names), rename(t, names)
# ---------- rewriting ----------
def reducible(t: Term, rules: list[Pair]) -> bool:
for _, u in subterms(t):
if not is_var(u):
for l, _ in rules:
if match(l, u) is not None:
return True
return False
def normalize(t: Term, rules: list[Pair]) -> Term:
"""Innermost normal form of t. The rules must terminate."""
by_root: dict[str, list[Pair]] = {}
for l, r in rules:
by_root.setdefault(l[0], []).append((l, r))
tasks: list[tuple] = [(0, t, None)]
vals: list[Term] = []
while tasks:
kind, u, env = tasks.pop()
if kind == 0: # evaluate u under env, whose values are normal forms
if is_var(u):
vals.append(env[u] if env is not None and u in env else u)
else:
tasks.append((1, u, len(u) - 1))
for a in reversed(u[1:]):
tasks.append((0, a, env))
else: # build u[0] over the top values and try the rules at the root
n = env
args = vals[len(vals) - n:] if n else []
del vals[len(vals) - n:]
s = (u[0],) + tuple(args)
for l, r in by_root.get(s[0], ()):
tau = match(l, s)
if tau is not None:
tasks.append((0, r, tau))
break
else:
vals.append(s)
return vals[0]
def critical_pairs(rule1: Pair, rule2: Pair, same: bool = False) -> list[Pair]:
"""Critical pairs from overlapping rule2 into non-variable positions of
rule1. If same is true, the root overlap is skipped."""
v1, v2 = variables(rule1[0]), variables(rule2[0])
l1 = rename(rule1[0], {x: f"_a{i}" for i, x in enumerate(v1)})
r1 = rename(rule1[1], {x: f"_a{i}" for i, x in enumerate(v1)})
l2 = rename(rule2[0], {x: f"_b{i}" for i, x in enumerate(v2)})
r2 = rename(rule2[1], {x: f"_b{i}" for i, x in enumerate(v2)})
out = []
for p, u in subterms(l1):
if is_var(u) or (same and p == ()):
continue
sigma = unify(u, l2)
if sigma is None:
continue
top = substitute(l1, sigma)
out.append(canonical((replace(top, p, substitute(r2, sigma)), substitute(r1, sigma))))
return out
def all_critical_pairs(rules: list[Pair]) -> list[Pair]:
out = []
for i, a in enumerate(rules):
for j, b in enumerate(rules):
out.extend(critical_pairs(a, b, same=(i == j)))
return out
# ---------- the lexicographic path ordering ----------
def lpo(precedence: dict[str, int]) -> Order:
"""s >lpo t for the precedence f > g iff precedence[f] > precedence[g].
Symbols missing from the precedence are incomparable with all others.
Pairs of subterms are memoized, so the time is polynomial."""
def greater(s: Term, t: Term) -> bool:
ids: dict[tuple, int] = {}
nodes: list[tuple] = []
varsets: list[frozenset] = []
def intern(term: Term) -> int:
out: list[int] = []
stack: list[tuple[bool, Term]] = [(False, term)]
while stack:
done, u = stack.pop()
if is_var(u):
key: tuple = ("?", u)
vs = frozenset([u])
elif not done:
stack.append((True, u))
for a in reversed(u[1:]):
stack.append((False, a))
continue
else:
n = len(u) - 1
kids = tuple(out[len(out) - n:]) if n else ()
del out[len(out) - n:]
key = (u[0], kids)
vs = frozenset().union(*(varsets[k] for k in kids)) if kids else frozenset()
if key not in ids:
ids[key] = len(nodes)
nodes.append(key)
varsets.append(vs)
out.append(ids[key])
return out[0]
a0, b0 = intern(s), intern(t)
memo: dict[tuple[int, int], bool] = {}
def frame(a: int, b: int):
fa, ka = nodes[a]
fb, kb = nodes[b]
if fa == "?":
return False
if fb == "?":
return kb in varsets[a] and a != b
for c in ka:
if c == b:
return True
if (yield (c, b)):
return True
pf, pg = precedence.get(fa), precedence.get(fb)
if fa != fb and pf is not None and pg is not None and pf > pg:
for c in kb:
if not (yield (a, c)):
return False
return True
if fa == fb and len(ka) == len(kb):
k = 0
while k < len(ka) and ka[k] == kb[k]:
k += 1
if k == len(ka) or not (yield (ka[k], kb[k])):
return False
for c in kb:
if not (yield (a, c)):
return False
return True
return False
stack = [((a0, b0), frame(a0, b0))]
val = None
while stack:
key, gen = stack[-1]
try:
q = gen.send(val)
except StopIteration as stop:
memo[key] = bool(stop.value)
val = memo[key]
stack.pop()
continue
if q in memo:
val = memo[q]
else:
stack.append((q, frame(*q)))
val = None
return memo[(a0, b0)]
return greater
# ---------- completion ----------
def complete(equations: list[Pair], greater: Order, max_steps: int = 1000):
"""Knuth-Bendix completion; see the module docstring for the result."""
tick = count()
pending: list = []
def push(s: Term, t: Term) -> None:
heapq.heappush(pending, (size(s) + size(t), next(tick), s, t))
for s, t in equations: # @line L1
push(s, t)
rules: list[Pair] = []
steps = 0
while True: # @line L2
if not pending: # @line L3
for u, v in all_critical_pairs(rules):
if not term_eq(normalize(u, rules), normalize(v, rules)):
push(u, v)
if not pending:
return "success", rules
steps += 1 # @line L4
if steps > max_steps:
return "give up", rules
_, _, s, t = heapq.heappop(pending) # @line L5
s, t = normalize(s, rules), normalize(t, rules)
if term_eq(s, t): # @line L6
continue
if greater(s, t): # @line L7
rho = canonical((s, t))
elif greater(t, s):
rho = canonical((t, s))
else:
return "fail", (s, t)
kept = [] # @line L8
for g, d in rules:
if reducible(g, [rho]):
push(g, d)
else:
kept.append((g, d))
for i, (g, d) in enumerate(kept): # @line L9
others = kept[:i] + kept[i + 1:] + [rho]
kept[i] = (g, normalize(d, others))
rules = kept + [rho] # @line L10
for rule in rules:
for u, v in critical_pairs(rho, rule, same=rule is rho):
push(u, v)
if rule is not rho:
for u, v in critical_pairs(rule, rho):
push(u, v)
test_knuth_bendix_completion.py 405 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/knuth-bendix-completion/. The authors are listed in prov/WM035M.jsonl.
import itertools
import random
from knuth_bendix_completion import (
canonical,
complete,
critical_pairs,
lpo,
normalize,
term_eq,
unify,
)
def P(text):
"""Parse 'f(x, g(a))' into a term; names starting with u..z are variables."""
pos = 0
def term():
nonlocal pos
start = pos
while pos < len(text) and (text[pos].isalnum() or text[pos] == "_"):
pos += 1
name = text[start:pos]
if pos < len(text) and text[pos] == "(":
pos += 1
args = [term()]
while text[pos] == ",":
pos += 1
while text[pos] == " ":
pos += 1
args.append(term())
pos += 1
return (name,) + tuple(args)
return name if name[0] in "uvwxyz" else (name,)
text = text.replace(" ", "")
return term()
def E(*pairs):
return [(P(a), P(b)) for a, b in pairs]
GROUP = E(("m(e, x)", "x"), ("m(i(x), x)", "e"), ("m(m(x, y), z)", "m(x, m(y, z))"))
GROUP_RULES = E(
("m(e, x)", "x"), ("m(x, e)", "x"), ("m(i(x), x)", "e"), ("m(x, i(x))", "e"),
("m(m(x, y), z)", "m(x, m(y, z))"), ("i(e)", "e"), ("i(i(x))", "x"),
("m(i(x), m(x, y))", "y"), ("m(x, m(i(x), y))", "y"), ("i(m(x, y))", "m(i(y), i(x))"),
)
GROUP_PREC = {"i": 3, "m": 2, "e": 1}
def as_set(rules):
return {canonical(r) for r in rules}
# ---------- an independent, naive oracle (recursive, for small terms) ----------
def o_var(t):
return isinstance(t, str)
def o_vars(t):
return {t} if o_var(t) else set().union(*[o_vars(a) for a in t[1:]]) if len(t) > 1 else set()
def o_sub(t, s):
return s.get(t, t) if o_var(t) else (t[0],) + tuple(o_sub(a, s) for a in t[1:])
def o_match(p, t, s):
if o_var(p):
if p in s:
return s[p] == t
s[p] = t
return True
if o_var(t) or p[0] != t[0] or len(p) != len(t):
return False
return all(o_match(a, b, s) for a, b in zip(p[1:], t[1:]))
def o_steps(t, rules):
out = []
for l, r in rules:
s = {}
if o_match(l, t, s):
out.append(o_sub(r, s))
if not o_var(t):
for i in range(1, len(t)):
for u in o_steps(t[i], rules):
out.append(t[:i] + (u,) + t[i + 1:])
return out
def o_nf(t, rules):
while True:
nxt = o_steps(t, rules)
if not nxt:
return t
t = nxt[-1] # any strategy: the rules are convergent when this is used
def o_unify(eqs):
s = {}
eqs = list(eqs)
while eqs:
a, b = eqs.pop()
a, b = o_sub(a, s), o_sub(b, s)
if a == b:
continue
if not o_var(a) and o_var(b):
a, b = b, a
if o_var(a):
if a in o_vars(b):
return None
s = {x: o_sub(v, {a: b}) for x, v in s.items()}
s[a] = b
continue
if a[0] != b[0] or len(a) != len(b):
return None
eqs.extend(zip(a[1:], b[1:]))
return s
def o_positions(t, p=()):
yield p, t
if not o_var(t):
for i in range(1, len(t)):
yield from o_positions(t[i], p + (i,))
def o_put(t, p, s):
if not p:
return s
i = p[0]
return t[:i] + (o_put(t[i], p[1:], s),) + t[i + 1:]
def o_cps(rules):
out = []
for i, (l1, r1) in enumerate(rules):
for j, (l2, r2) in enumerate(rules):
ren = {x: x + "'" for x in o_vars(l2)}
l2r, r2r = o_sub(l2, ren), o_sub(r2, ren)
for p, u in o_positions(l1):
if o_var(u) or (i == j and p == ()):
continue
s = o_unify([(u, l2r)])
if s is not None:
out.append((o_put(o_sub(l1, s), p, o_sub(r2r, s)), o_sub(r1, s)))
return out
def o_lpo(s, t, prec):
if o_var(s):
return False
if o_var(t):
return t in o_vars(s) and s != t
if any(a == t or o_lpo(a, t, prec) for a in s[1:]):
return True
f, g = s[0], t[0]
if f != g and f in prec and g in prec and prec[f] > prec[g]:
return all(o_lpo(s, b, prec) for b in t[1:])
if f == g and len(s) == len(t):
for a, b in zip(s[1:], t[1:]):
if a != b:
return o_lpo(a, b, prec) and all(o_lpo(s, c, prec) for c in t[1:])
return False
return False
def o_eval(t, alg, env):
if o_var(t):
return env[t]
return alg[t[0]][tuple(o_eval(a, alg, env) for a in t[1:])]
def o_holds(eq, alg, dom):
s, t = eq
vs = sorted(o_vars(s) | o_vars(t))
for vals in itertools.product(dom, repeat=len(vs)):
env = dict(zip(vs, vals))
if o_eval(s, alg, env) != o_eval(t, alg, env):
return False
return True
def all_algebras(sig, dom):
"""Every algebra over the domain for the signature {symbol: arity}."""
names = sorted(sig)
choices = []
for f in names:
keys = list(itertools.product(dom, repeat=sig[f]))
choices.append([dict(zip(keys, vals)) for vals in itertools.product(dom, repeat=len(keys))])
for combo in itertools.product(*choices):
yield dict(zip(names, combo))
def o_bfs(t, eqs, depth, max_size):
"""Terms reachable from t by at most depth replacements with eqs, both ways."""
both = eqs + [(b, a) for a, b in eqs if not o_var(b) and o_vars(a) <= o_vars(b)]
seen = {t}
frontier = [t]
for _ in range(depth):
nxt = []
for u in frontier:
for v in o_steps(u, both):
if v not in seen and len(repr(v)) <= max_size:
seen.add(v)
nxt.append(v)
frontier = nxt
return seen
# ---------- the examples of the page ----------
def test_page_example():
status, rules = complete(E(("f(f(x))", "g(x)")), lpo({"f": 2, "g": 1}))
assert status == "success"
assert rules == E(("f(f(x))", "g(x)"), ("f(g(x))", "g(f(x))"))
def test_groups():
status, rules = complete(GROUP, lpo(GROUP_PREC))
assert status == "success"
assert as_set(rules) == as_set(GROUP_RULES)
def test_groups_any_input_order_gives_the_same_reduced_system():
for perm in itertools.permutations(GROUP):
status, rules = complete(list(perm), lpo(GROUP_PREC))
assert status == "success" and as_set(rules) == as_set(GROUP_RULES)
def test_divergence_gives_up():
status, rules = complete(E(("f(g(f(x)))", "g(f(x))")), lpo({"f": 2, "g": 1}), max_steps=20)
assert status == "give up"
assert canonical((P("f(g(g(g(f(x)))))"), P("g(g(g(f(x))))"))) in as_set(rules)
def test_commutativity_fails():
status, eq = complete(E(("p(x, y)", "p(y, x)")), lpo({"p": 1}))
assert status == "fail" and as_set([eq]) == as_set(E(("p(x, y)", "p(y, x)")))
def test_empty_and_trivial():
assert complete([], lpo({})) == ("success", [])
assert complete([("x", "x")], lpo({})) == ("success", [])
assert complete([(P("f(x)"), P("f(x)"))], lpo({"f": 1})) == ("success", [])
assert complete([("x", "y")], lpo({}))[0] == "fail"
def test_non_linear_and_shared_variables():
status, rules = complete(E(("f(x, x)", "x")), lpo({"f": 1}))
assert status == "success" and rules == E(("f(x, x)", "x"))
# x occurs on both sides; the right side has fewer variables
status, rules = complete(E(("h(x, y)", "g(x)")), lpo({"h": 2, "g": 1}))
assert status == "success" and rules == E(("h(x, y)", "g(x)"))
# the other orientation would introduce a variable
status, eq = complete(E(("h(x, y)", "g(x)")), lpo({"h": 1, "g": 2}))
assert status == "fail"
def test_collapse_and_compose():
# a = b, f(a) = c, then b = c makes f(a) -> c's left side reducible
status, rules = complete(E(("f(a)", "c"), ("a", "b")), lpo({"f": 4, "a": 3, "b": 2, "c": 1}))
assert status == "success"
assert as_set(rules) == as_set(E(("a", "b"), ("f(b)", "c")))
def test_deep_terms():
n = 2000
t = ("a",)
for _ in range(n):
t = ("g", t)
status, rules = complete([(t, ("a",))], lpo({"g": 2, "a": 1}))
assert status == "success" and len(rules) == 1 and term_eq(rules[0][0], t)
u = ("b",)
for _ in range(n):
u = ("g", u)
assert unify(t, u) is None
deep = ("a",)
for _ in range(3 * n):
deep = ("g", deep)
assert term_eq(normalize(deep, rules), ("a",)) # g^6000(a) -> g^4000(a) -> g^2000(a) -> a
deep = ("g", deep)
assert term_eq(normalize(deep, rules), ("g", ("a",)))
def test_deep_normalization_result():
n = 1500
t = ("a",)
for _ in range(n):
t = ("g", t)
rules = [(("g", ("g", "x")), "x")]
assert term_eq(normalize(t, rules), ("a",))
assert lpo({"g": 1})(t, ("g", ("a",)))
# ---------- random systems against the oracle ----------
SIGS = [
{"f": 2, "g": 1, "a": 0},
{"m": 2, "i": 1, "e": 0},
{"f": 1, "g": 1, "a": 0, "b": 0},
{"h": 2, "a": 0, "b": 0},
]
def rand_term(rng, sig, vs, depth):
if depth == 0 or rng.random() < 0.3:
consts = [f for f, n in sig.items() if n == 0]
if vs and (not consts or rng.random() < 0.6):
return rng.choice(vs)
return (rng.choice(consts),)
f = rng.choice(sorted(sig))
return (f,) + tuple(rand_term(rng, sig, vs, depth - 1) for _ in range(sig[f]))
def test_random_against_oracle():
rng = random.Random(20261106)
models_cache = {}
stats = {"success": 0, "fail": 0, "give up": 0, "bfs": 0}
cases = 0
while cases < 600:
k = rng.randrange(len(SIGS))
sig = SIGS[k]
if k not in models_cache:
models_cache[k] = list(all_algebras(sig, (0, 1)))
eqs = [(rand_term(rng, sig, ["x", "y"], 2), rand_term(rng, sig, ["x", "y"], 2))
for _ in range(rng.randint(1, 3))]
order = sorted(sig)
rng.shuffle(order)
prec = {f: i for i, f in enumerate(order)}
status, out = complete(eqs, lpo(prec), max_steps=40)
cases += 1
stats[status] += 1
models = [a for a in models_cache[k] if all(o_holds(e, a, (0, 1)) for e in eqs)]
if status == "fail":
s, t = out
assert s != t and not o_lpo(s, t, prec) and not o_lpo(t, s, prec)
assert all(o_holds((s, t), a, (0, 1)) for a in models)
continue
rules = out
for l, r in rules:
assert o_lpo(l, r, prec) # every rule is oriented, so R terminates
assert all(o_holds((l, r), a, (0, 1)) for a in models) # sound
if status == "give up":
continue
for i, (l, r) in enumerate(rules): # the result is reduced
assert o_steps(r, rules) == []
assert o_steps(l, rules[:i] + rules[i + 1:]) == []
for u, v in o_cps(rules): # local confluence
assert o_nf(u, rules) == o_nf(v, rules)
for s, t in eqs: # the input equations are decided
assert o_nf(s, rules) == o_nf(t, rules)
for _ in range(3): # bounded search for E-equal terms
s = rand_term(rng, sig, ["x"], 2)
nf = o_nf(s, rules)
for t in o_bfs(s, eqs, 2, 60):
assert o_nf(t, rules) == nf
stats["bfs"] += 1
assert cases >= 500
assert stats["success"] >= 150 and stats["fail"] >= 30 and stats["bfs"] >= 1000, stats
def test_critical_pairs_against_oracle():
rng = random.Random(7)
checked = 0
for _ in range(800):
sig = SIGS[rng.randrange(len(SIGS))]
l1 = rand_term(rng, sig, ["x", "y"], 3)
l2 = rand_term(rng, sig, ["x", "y"], 2)
if o_var(l1) or o_var(l2):
continue
r1 = rand_term(rng, sig, sorted(o_vars(l1)), 2)
r2 = rand_term(rng, sig, sorted(o_vars(l2)), 2)
mine = sorted(map(repr, (canonical(p) for p in critical_pairs((l1, r1), (l2, r2)))))
ref = []
for p, u in o_positions(l1):
if o_var(u):
continue
ren = {x: x + "'" for x in o_vars(l2)}
s = o_unify([(u, o_sub(l2, ren))])
if s is not None:
ref.append(canonical((o_put(o_sub(l1, s), p, o_sub(o_sub(r2, ren), s)), o_sub(r1, s))))
assert mine == sorted(map(repr, ref))
checked += 1
assert checked >= 500
def test_lpo_against_oracle():
rng = random.Random(3)
for _ in range(3000):
sig = SIGS[rng.randrange(len(SIGS))]
order = sorted(sig)
rng.shuffle(order)
prec = {f: i for i, f in enumerate(order)}
s = rand_term(rng, sig, ["x", "y"], 3)
t = rand_term(rng, sig, ["x", "y"], 3)
assert lpo(prec)(s, t) == o_lpo(s, t, prec)
What the next level needs
- the tests did not run in this build