MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
clos_conv.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4
5#include <fe/container.h>
6#include <fe/worklist.h>
7
8#include <mim/plug/mem/mem.h>
9
10using namespace std::literals;
11
12namespace mim::plug::clos::phase {
13
14namespace {
15
16bool is_memop_res(const Def* fd) {
17 auto proj = fd->isa<Extract>();
18 return proj && mem::has_mem_ty(proj->tuple()->type());
19}
20
21/// The free (non-closed, not-nested) Def%s directly reachable from @p nest's root.
22DefSet free_defs(const Nest& nest) {
24 auto queue = fe::BFSWorklist<DefSet>{nest.root()->mut()};
25
26 while (!queue.empty()) {
27 for (auto op : queue.pop()->deps()) {
28 if (op->is_closed()) continue; // nothing free in here
29 if (nest.contains(op))
30 queue.push(op);
31 else
32 free.emplace(op);
33 }
34 }
35
36 return free;
37}
38
39} // namespace
40
41/*
42 * Free variable analysis
43 */
44
45void FreeDefAna::classify(Node* node, const Def* fd, bool& spawned_pred, NodeQueue& worklist) {
46 assert(!Axm::isa<mem::M>(fd) && "mem tokens must not be free");
47 if (fd->is_closed()) return;
48
49 if (auto [var, lam] = isa_var_proj<Lam>(fd); var && lam) {
50 if (var != lam->ret_var()) node->add_fvs(fd);
51 } else if (auto free_bb = Axm::isa(anno::free_bb, fd)) {
52 node->add_fvs(free_bb);
53 } else if (auto pred = fd->isa_mut()) {
54 // A referenced nested mutable contributes its own free defs (once it is closure-converted).
55 if (pred != node->mut) {
56 auto [pnode, inserted] = build_node(pred, worklist);
57 node->preds.emplace_back(pnode);
58 pnode->succs.emplace_back(node);
59 spawned_pred |= inserted;
60 }
61 } else if (fd->local_vars() && !fd->isa<Tuple>()) {
62 // Note: a Var may still be closed if its type is a mut, so the isa_var_proj case above is not redundant.
63 node->add_fvs(fd);
64 } else if (is_memop_res(fd)) {
65 node->add_fvs(fd); // results of memops must not be floated down
66 } else {
67 for (auto op : fd->ops())
68 classify(node, op, spawned_pred, worklist);
69 }
70}
71
72std::pair<FreeDefAna::Node*, bool> FreeDefAna::build_node(Def* mut, NodeQueue& worklist) {
73 auto [p, inserted] = lam2node_.emplace(mut, nullptr);
74 if (!inserted) return {p->second.get(), false};
75 world().log().d("FVA: create node {}", mut);
76
77 p->second = std::make_unique<Node>(mut);
78 auto node = p->second.get();
79 bool spawned_pred = false;
80 for (auto fd : free_defs(Nest(mut)))
81 classify(node, fd, spawned_pred, worklist);
82
83 // A node with no fresh predecessors is ready to be settled right away.
84 if (!spawned_pred) {
85 worklist.push(node);
86 world().log().d("FVA: init {}", mut);
87 }
88 return {node, true};
89}
90
91void FreeDefAna::propagate(NodeQueue& worklist) {
92 while (!worklist.empty()) {
93 auto node = fe::pop(worklist);
94 if (is_done(node)) continue;
95 auto changed = is_bot(node);
96 mark(node);
97 for (auto pred : node->preds)
98 for (auto pfv : pred->fvs)
99 changed |= node->add_fvs(pfv).second;
100 if (changed)
101 for (auto succ : node->succs)
102 worklist.push(succ);
103 }
104}
105
107 auto worklist = NodeQueue();
108 auto [node, _] = build_node(lam, worklist);
109 if (!is_done(node)) {
110 ++cur_pass_;
111 propagate(worklist);
112 }
113 return node->fvs;
114}
115
116/*
117 * Closure Conversion
118 */
119
121 // Rewrite the deferred closure bodies, each in isolation.
122 while (!body_worklist_.empty()) {
123 auto fn = body_worklist_.front();
124 body_worklist_.pop();
125 auto& stub = closures_.at(fn);
126 auto& vs = old_world().vars();
127 auto vars = Vars();
128 for (auto fv : stub.fvs)
129 vars = vs.merge(vars, fv->free_vars());
130 push(vars);
131 bind_old(stub.old_fn->has_var());
132 rewrite_body(stub);
133 pop();
134 }
135}
136
137const Def* ClosConv::rewrite_imm_Pi(const Pi* pi) {
138 if (is_telescope_cn(pi)) return rewrite_telescope_cn(pi);
139 if (Pi::isa_cn(pi)) return clos_type_of(pi);
140 return RWPhase::rewrite_imm_Pi(pi);
141}
142
144 if (Pi::isa_cn(pi) && !is_telescope_cn(pi)) return clos_type_of(pi);
145 return RWPhase::rewrite_mut_Pi(pi);
146}
147
149 if (!Lam::isa_cn(old_lam) || is_telescope_cn(old_lam->type())) return RWPhase::rewrite_mut_Lam(old_lam);
150
151 auto& w = new_world();
152 auto stub = make_stub(old_lam);
153 auto clos_ty = rewrite(old_lam->type());
154 // Rewrite the individual free defs, not the (possibly normalized) env tuple:
155 // its normal form may reference defs that are not free vars and hence not in the current map.
156 auto env = w.tuple(DefVec(stub.fvs.size(), [&](auto i) { return rewrite(stub.fvs[i]); }));
157 auto closure = clos_pack(env, stub.fn, clos_ty);
158 log().d("pack {} → {}: {}", old_lam, closure, clos_ty);
159 return map(old_lam, closure);
160}
161
163 if (auto a = Axm::isa<anno>(app))
164 if (auto handled = rewrite_anno(a)) return handled;
165
166 auto new_callee = rewrite(app->callee());
167 auto new_arg = rewrite(app->arg());
168 if (new_callee->type()->isa<Sigma>()) return clos_apply(new_callee, new_arg);
169 return new_world().app(new_callee, new_arg);
170}
171
172const Def* ClosConv::rewrite_anno(Axm::IsA<anno, App> a) {
173 auto& w = new_world();
174 switch (a.id()) {
175 case anno::returning:
176 // A return continuation is *not* closure converted; it stays a plain Cn sharing the enclosing scope.
177 // After η-expansion this should be its only occurrence, so mapping it into the current scope suffices.
178 if (auto ret_lam = a->arg()->isa_mut<Lam>()) {
179 auto new_doms = DefVec(ret_lam->num_doms(),
180 [&, nd = ret_lam->num_doms()](auto i) { return rewrite(ret_lam->dom(nd, i)); });
181 auto new_lam = w.mut_lam(w.cn(new_doms))->set(ret_lam->dbg_key());
182 map(ret_lam, new_lam);
183 if (ret_lam->is_set()) new_lam->set(rewrite(ret_lam->filter()), rewrite(ret_lam->body()));
184 return new_lam;
185 }
186 return nullptr;
188 case anno::free_bb: {
189 // A free/first-class basic block captures nothing: it gets an empty environment and its body is
190 // rewritten right here, sharing the enclosing scope (same η-conversion remark as above).
191 auto bb_lam = a->arg()->isa_mut<Lam>();
192 assert(bb_lam && Lam::isa_basicblock(bb_lam));
193 auto stub = make_stub({}, bb_lam);
194 auto pack = clos_pack(w.tuple(), stub.fn, rewrite(bb_lam->type()));
195 map(bb_lam, pack);
196 rewrite_body(stub);
197 return pack;
198 }
199 default: return nullptr;
200 }
201}
202
204 // A closure body may still refer to a ret_var of an *enclosing* Lam: return continuations are not
205 // closure-converted, and the FVA deliberately excludes ret_vars, so they are never captured into an env.
206 // Map such a projection onto the corresponding var of the enclosing Lam's converted stub.
207 // This is a known workaround; the principled fix is to capture escaping enclosing BBs/ret_vars in the
208 // environment (tracked by issue #117).
209 if (auto [var, lam] = isa_var_proj<Lam>(ex); var && lam && lam->ret_var() == var && !is_telescope_cn(lam->type())) {
210 auto new_fn = make_stub(lam).fn;
211 auto new_idx = skip_env(env_param(new_fn->type()->as<Pi>()), Lit::as(var->index()));
212 return new_fn->var(new_fn->num_vars(), new_idx);
213 }
215}
216
217const Pi* ClosConv::rewrite_ret_cn(const Pi* pi) {
218 assert(Pi::isa_basicblock(pi));
219 return new_world().cn(DefVec(pi->num_doms(), [&, nd = pi->num_doms()](auto i) { return rewrite(pi->dom(nd, i)); }));
220}
221
222const Pi* ClosConv::rewrite_telescope_cn(const Pi* pi) {
223 auto& w = new_world();
224 auto sig = pi->dom()->as_mut<Sigma>();
225 if (auto new_sig = lookup(sig)) return w.cn(new_sig);
226
227 auto new_sig = w.mut_sigma(rewrite(sig->type()), sig->num_ops());
228 map(sig, new_sig);
229 auto _ = enter(sig);
230 auto ret = Pi::isa_returning(pi) ? sig->num_ops() - 1 : sig->num_ops();
231 for (size_t i = 0, e = sig->num_ops(); i != e; ++i)
232 new_sig->set(i, i == ret ? rewrite_ret_cn(sig->op(i)->as<Pi>()) : rewrite(sig->op(i)));
233 return w.cn(seal(sig, new_sig));
234}
235
236const Def* ClosConv::clos_type_of(const Pi* pi, const Def* env_type) {
237 if (!env_type)
238 if (auto i = glob_muts_.find(pi); i != glob_muts_.end()) return i->second;
239
240 auto nd = pi->num_doms();
241 auto new_doms = DefVec(nd, [&](auto i) {
242 return (i == nd - 1 && Pi::isa_returning(pi)) ? rewrite_ret_cn(pi->ret_pi()) : rewrite(pi->dom(nd, i));
243 });
244 auto ct = ctype(new_world(), new_doms, env_type);
245 if (!env_type) {
246 glob_muts_.emplace(pi, ct);
247 log().d("closure type: {} → {} (pretyped)", pi, ct);
248 } else {
249 log().d("closure type: {} → {} (env = {})", pi, ct, env_type);
250 }
251 return ct;
252}
253
254ClosConv::Stub ClosConv::make_stub(const DefSet& fvs, Lam* old_lam) {
255 auto& w = new_world();
256 // Sort by gid: fv_vec's order *is* the closure environment layout (see rewrite_body's env_val->proj(i)).
257 // A DefSet iterates in insertion order, which unrelated changes elsewhere silently perturb.
258 auto fv_vec = DefVec(fvs.begin(), fvs.end());
259 std::ranges::sort(fv_vec, GIDLt<const Def*>());
260 auto env_type = rewrite(old_world().tuple(fv_vec)->type());
261 auto new_fn_type = clos_type_of(old_lam->type(), env_type)->as<Pi>();
262 auto new_fn = w.mut_lam(new_fn_type)->set(old_lam->dbg_key());
263
264 if (!old_lam->is_rewritable()) {
265 // External or imported (unset) Lam%s get an η-wrapper that hides the environment.
266 auto ep = env_param(new_fn_type);
267 auto new_ext_type = w.cn(clos_remove_env(ep, new_fn_type->dom()));
268 auto new_ext_lam = w.mut_lam(new_ext_type)->set(old_lam->dbg_key());
269 log().d("wrap external lam {} → stub {}, external {}", old_lam, new_fn, new_ext_lam);
270 if (old_lam->is_set()) {
271 if (old_lam->is_external()) new_ext_lam->externalize();
272 auto env = w.tuple(DefVec(fv_vec.size(), [&](auto i) { return rewrite(fv_vec[i]); }));
273 new_ext_lam->app(false, new_fn, clos_insert_env(ep, env, new_ext_lam->var()));
274 // new_fn's body is rewritten later via the body worklist.
275 } else {
276 new_fn->app(false, new_ext_lam, clos_remove_env(ep, new_fn->var()));
277 }
278 }
279
280 log().d("stub {} → {}", old_lam, new_fn);
281 auto stub = Stub{old_lam, std::move(fv_vec), new_fn};
282 closures_.try_emplace(old_lam, stub);
283 closures_.try_emplace(new_fn, stub);
284 return stub;
285}
286
287ClosConv::Stub ClosConv::make_stub(Lam* old_lam) {
288 if (auto i = closures_.find(old_lam); i != closures_.end()) return i->second;
289 auto stub = make_stub(fva_.run(old_lam), old_lam);
290 body_worklist_.emplace(stub.fn);
291 return stub;
292}
293
294void ClosConv::rewrite_body(const Stub& stub) {
295 auto old_fn = stub.old_fn;
296 if (!old_fn->is_set()) return;
297
298 auto& w = new_world();
299 auto new_fn = stub.fn;
300 auto ep = env_param(new_fn->type()->as<Pi>());
301 auto nv = new_fn->num_vars();
302 auto env_val = new_fn->var(nv, ep)->set("closure_env");
303 log().d("rewrite body of {} → {}", old_fn, new_fn);
304 if (stub.fvs.size() == 1) {
305 map(stub.fvs.front(), env_val);
306 } else {
307 for (size_t i = 0, e = stub.fvs.size(); i != e; ++i) {
308 auto fv = stub.fvs[i];
309 auto sym = w.sym("fv_"s + (fv->sym() ? fv->sym().str() : std::to_string(i)));
310 map(fv, env_val->proj(e, i)->set(sym));
311 }
312 }
313
314 auto params = w.tuple(DefVec(old_fn->num_doms(), [&](auto i) { return new_fn->var(nv, skip_env(ep, i)); }));
315 map(old_fn->var(), params);
316 new_fn->set(rewrite(old_fn->filter()), rewrite(old_fn->body()));
317}
318
319} // namespace mim::plug::clos::phase
const Def * callee() const
Definition lam.h:237
const Def * arg() const
Definition lam.h:251
static auto isa(const Def *def)
Definition axm.h:112
Base class for all Defs.
Definition def.h:289
T * as_mut() const
Asserts that this is a mutable, casts constness away and performs a static_cast to T.
Definition def.h:597
constexpr size_t num_ops() const noexcept
Definition def.h:378
Extracts from a Sigma or Array-typed Extract::tuple the element at position Extract::index.
Definition tuple.h:360
A function.
Definition lam.h:91
static const Lam * isa_cn(const Def *d)
Definition lam.h:125
const Pi * type() const
Definition lam.h:112
static const Lam * isa_basicblock(const Def *d)
Definition lam.h:127
static T as(const Def *def)
Definition def.h:982
Nest(Def *root)
Definition nest.cpp:9
const fe::Log & log() const
Definition phase.h:60
A dependent function type.
Definition lam.h:14
static const Pi * isa_cn(const Def *d)
Definition lam.h:50
static const Pi * isa_basicblock(const Def *d)
Definition lam.h:52
const Def * dom() const
Definition lam.h:38
const Pi * ret_pi() const
Yields the last Pi::dom, if Pi::isa_basicblock.
Definition lam.cpp:13
nat_t num_doms() const noexcept
Definition lam.h:40
static const Pi * isa_returning(const Def *d)
Definition lam.h:51
World & new_world()
Create new Defs into this.
Definition phase.h:379
const Def * lookup(const Def *) override
Lookup old_def by searching in reverse through the stack of maps.
Definition phase.cpp:212
World & old_world()
Get old Defs from here.
Definition phase.h:378
void bind_old(const Var *var)
Adds the old var - if any - to the innermost scope.
Definition rewrite.cpp:40
void pop()
Definition rewrite.h:41
virtual const Def * rewrite_imm_Extract(const Extract *)
const Def * map(const Def *old_def, const Def *new_def)
Definition rewrite.cpp:29
virtual const Def * rewrite_mut_Lam(Lam *)
virtual const Def * rewrite_imm_Pi(const Pi *)
const Def * seal(Def *old_mut, Def *new_mut)
The tail of rewrite_stub: immutabilizes new_mut in hindsight, as rewriting may have made it vacuous.
Definition rewrite.cpp:280
virtual const Def * rewrite(const Def *)
Definition rewrite.cpp:62
auto enter(Def *new_mut)
Updates curr_mut() to new_mut and restores it at the end of the scope.
Definition rewrite.h:115
void push(Vars old_vars={}, Vars new_vars={})
Definition rewrite.h:40
virtual const Def * rewrite_mut_Pi(Pi *)
A dependent tuple type.
Definition tuple.h:39
const Pi * cn()
Definition world.h:398
const Def * app(const Def *callee, const Def *arg)
Definition world.cpp:416
auto & vars()
Definition world.h:714
const Def * rewrite_imm_Extract(const Extract *) final
const Def * rewrite_mut_Pi(Pi *) final
const Def * rewrite_imm_App(const App *) final
const Def * rewrite_imm_Pi(const Pi *) final
void finalize() final
Run after all roots have been walked but still before the two worlds are swapped.
const Def * rewrite_mut_Lam(Lam *) final
const DefSet & run(Lam *lam)
Returns the free defs lam has to capture; see the class description.
bool is_telescope_cn(const Def *def)
Inserting the environment would shift the indices a telescope's Var refers to, so ClosConv leaves suc...
Definition clos.h:80
const Def * clos_remove_env(size_t ep, size_t i, std::function< const Def *(size_t)> f)
Definition clos.cpp:120
const Def * ctype(World &w, Defs doms, const Def *env_type=nullptr)
Builds a closure type from the domains doms of a Cn.
Definition clos.cpp:122
const Def * clos_insert_env(size_t ep, size_t i, const Def *env, std::function< const Def *(size_t)> f)
Definition clos.cpp:116
std::tuple< const Extract *, N * > isa_var_proj(const Def *def)
If def is a projection var#i of the Var of some mutable of type N, returns (projection,...
Definition clos.h:73
size_t skip_env(size_t ep, size_t i)
Same as shift_env, but skips the env param instead.
Definition clos.h:114
const Def * clos_pack(const Def *env, const Def *fn, const Def *ct=nullptr)
Pack a typed closure.
Definition clos.cpp:57
const Def * clos_apply(const Def *closure, const Def *args)
Apply a closure to arguments.
Definition clos.cpp:74
size_t env_param(Defs doms)
Describes where the environment is placed in the argument list: right after a leading mem....
Definition clos.h:107
bool has_mem_ty(const Def *type)
Is type mem.M or a Sigma with a mem.M component?
Definition mem.h:54
fe::Vector< const Def * > DefVec
Definition def.h:98
GIDSet< const Def * > DefSet
Definition def.h:94
fe::Patricia< const Var, DefKey >::Set Vars
Definition def.h:115
Node
Definition def.h:123
@ Pi
Definition def.h:125
@ Extract
Definition def.h:125