def with_unique_lhs(
f, root: LogicStatement, bindings: dict[Alias, TensorFType]
) -> tuple[LogicStatement, dict[Alias, TensorFType]]:
"""
Ensures all left-hand sides (LHS) of queries are unique by inserting new
tensors.
"""
spc = Namespace(root)
for var in bindings:
spc.freshen(var.name)
renames: dict[Alias, Alias] = {}
bound = set(bindings.keys())
writes: dict[Alias, Alias] = {}
def rule_0(node):
match node:
case Query(Table(Alias() as lhs, idxs), rhs):
if lhs in bound:
var = Alias(spc.freshen(lhs.name))
renames[lhs] = var
if lhs in bindings:
writes[lhs] = var
return Query(Table(var, idxs), rhs)
bound.add(lhs)
return None
case Alias() as a if a in renames:
return renames[a]
case Produces(args):
return Produces(args + tuple(writes.values()))
root = Rewrite(PostWalk(rule_0))(root)
root, bindings = f(root, bindings)
unrenames = {v: k for k, v in renames.items()}
# Some produces may be renamed during the optimization,
# so we need to be a bit careful here.
def rule_1(node):
match node:
case Produces(args):
n_writes = len(writes)
v_post_list = args[-n_writes:] if n_writes else ()
bodies: list[LogicStatement] = []
for (k, _v_pre), v_post in zip(
writes.items(), v_post_list, strict=True
):
idxs = tuple(
Field(spc.freshen("i")) for _ in range(bindings[k].ndim)
)
bodies.append(Query(Table(k, idxs), Table(v_post, idxs)))
v_post_to_k = dict(zip(v_post_list, writes.keys(), strict=True))
args_2 = tuple(
v_post_to_k.get(a, unrenames.get(a, a))
for a in args[: len(args) - n_writes]
)
return Plan(tuple(bodies) + (Produces(args_2),))
return (Rewrite(PostWalk(rule_1))(root), bindings)