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
Summary
CallGraphPass(bloqade/rewrite/passes/callgraph.py) applies a rewrite rule to every method inkirin.analysis.CallGraph(mt). That graph is built fromfunc.Invoke(call) edges only, so it does not include a method that is reachable solely as thefnoperand of anilist.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 constantMethod, 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.mapif the resolving rewrite runs viaCallGraphPass(or a top-levelWalk) — 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_partneris markedir.Pure, constant folding hoists the enclosingilist.mapclosure (e.g.ilist.map(lambda a: movement.cz_partner(a), locs)) into a standalone constantMethod. The resolver'sWalk— andCallGraphPass— never descend into that method body, socz_partneris never resolved and the map fails to lower.We worked around it by deliberately marking
cz_partnernon-Pure, which keeps the closure inline so the unroller expands it (yielding concrete per-elementcz_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 underlyingCallGraphtraversal) should treat the method referenced by anilist.map'sfnoperand — 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.pykirin.analysis.CallGraph(built fromfunc.Invokeedges). The proper fix may belong upstream in kirin (addilist.map-stylefnedges toCallGraph), orCallGraphPasscould special-case higher-order ilist ops — hencestatus: upstream.Methods makes the buried-statement case the common one.Repro sketch