Skip to content

CallGraphPass / call-graph rewrites don't descend into ilist.map fn closures #830

Description

@weinbe58

Summary

CallGraphPass (bloqade/rewrite/passes/callgraph.py) applies a rewrite rule to every method in kirin.analysis.CallGraph(mt). That graph is built from func.Invoke (call) edges only, so it does not include a method that is reachable solely as the fn operand of an ilist.map (and, presumably, the other higher-order ilist combinators — foldr, scan, etc.).

As a result, a rewrite applied across the call graph never descends into the body of a mapped closure. Plain rewrite.Walk(rule).rewrite(mt.code) has the same blind spot: once constant folding hoists a pure map closure into an opaque constant Method, the statements inside it are unreachable to any walk over the parent method's code.

Why this matters

A dialect statement that must be rewritten/resolved at compile time cannot be carried inside an ilist.map if the resolving rewrite runs via CallGraphPass (or a top-level Walk) — the rewrite simply never sees it.

Concrete motivating case (bloqade-lanes)

We added a movement.cz_partner(loc) statement that is resolved at compile time by an arch-aware rewrite (ResolveCzPartner) — it reads the const-folded input location, looks up the CZ blockade partner in the architecture spec, and replaces the statement with a constant.

When cz_partner is marked ir.Pure, constant folding hoists the enclosing ilist.map closure (e.g. ilist.map(lambda a: movement.cz_partner(a), locs)) into a standalone constant Method. The resolver's Walk — and CallGraphPass — never descend into that method body, so cz_partner is never resolved and the map fails to lower.

We worked around it by deliberately marking cz_partner non-Pure, which keeps the closure inline so the unroller expands it (yielding concrete per-element cz_partner(loc_const) statements in the parent body) before the resolver runs. That works, but it forces a genuinely pure statement to lie about its purity (losing DCE and other Pure-only optimizations) purely to dodge this traversal gap.

Expected behavior

CallGraphPass (and ideally the underlying CallGraph traversal) should treat the method referenced by an ilist.map's fn operand — and analogous higher-order ilist ops — as a call-graph edge, so a rule applied across the call graph also rewrites inside mapped closures.

Pointers

  • CallGraphPass / UpdateDialectsOnCallGraph: bloqade/rewrite/passes/callgraph.py
  • Underlying graph: kirin.analysis.CallGraph (built from func.Invoke edges). The proper fix may belong upstream in kirin (add ilist.map-style fn edges to CallGraph), or CallGraphPass could special-case higher-order ilist ops — hence status: upstream.
  • Compounding factor: const folding hoisting pure closures into constant Methods makes the buried-statement case the common one.

Repro sketch

# pseudo: a statement S that a rewrite R must resolve, used inside a map
@kernel
def k(xs):
    def f(x):
        return some_dialect.S(x)   # must be rewritten by R at compile time
    return ilist.map(f, xs)

# CallGraphPass(rule=Walk(R), dialects=...)(k)  -> never rewrites S inside f

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: compiler optimizationArea: compiler optimization passes, IR transformations, and code generation improvements.category: bugCategory: this is a bug or something isn't working as expected.status: upstreamStatus: requires upstream changes, discussion, or review.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions