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
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 if (!proj) return false;
19 auto types = proj->tuple()->type()->ops();
20 return std::ranges::any_of(types, [](auto d) { return Axm::isa<mem::M>(d); });
21}
22
23/// The free (non-closed, not-nested) Def%s directly reachable from @p nest's root.
24DefSet free_defs(const Nest& nest) {
26 auto queue = fe::BFSWorklist<DefSet>{nest.root()->mut()};
27
28 while (!queue.empty()) {
29 for (auto op : queue.pop()->deps()) {
30 if (op->is_closed()) continue; // nothing free in here
31 if (nest.contains(op))
32 queue.push(op);
33 else
34 free.emplace(op);
35 }
36 }
37
38 return free;
39}
40
41} // namespace
42
43/*
44 * Free variable analysis
45 */
46
47void FreeDefAna::classify(Node* node, const Def* fd, bool& spawned_pred, NodeQueue& worklist) {
48 assert(!Axm::isa<mem::M>(fd) && "mem tokens must not be free");
49 if (fd->is_closed()) return;
50
51 if (auto [var, lam] = isa_var_proj<Lam>(fd); var && lam) {
52 if (var != lam->ret_var()) node->add_fvs(fd);
53 } else if (auto free_bb = Axm::isa(attr::free_bb, fd)) {
54 node->add_fvs(free_bb);
55 } else if (auto pred = fd->isa_mut()) {
56 // A referenced nested mutable contributes its own free defs (once it is closure-converted).
57 if (pred != node->mut) {
58 auto [pnode, inserted] = build_node(pred, worklist);
59 node->preds.emplace_back(pnode);
60 pnode->succs.emplace_back(node);
61 spawned_pred |= inserted;
62 }
63 } else if (fd->has_dep(Dep::Var) && !fd->isa<Tuple>()) {
64 // Note: a Var may still be closed if its type is a mut, so the isa_var_proj case above is not redundant.
65 node->add_fvs(fd);
66 } else if (is_memop_res(fd)) {
67 node->add_fvs(fd); // results of memops must not be floated down
68 } else {
69 for (auto op : fd->ops())
70 classify(node, op, spawned_pred, worklist);
71 }
72}
73
74std::pair<FreeDefAna::Node*, bool> FreeDefAna::build_node(Def* mut, NodeQueue& worklist) {
75 auto [p, inserted] = lam2node_.emplace(mut, nullptr);
76 if (!inserted) return {p->second.get(), false};
77 world().log().d("FVA: create node {}", mut);
78
79 p->second = std::make_unique<Node>(mut);
80 auto node = p->second.get();
81 bool spawned_pred = false;
82 for (auto fd : free_defs(Nest(mut)))
83 classify(node, fd, spawned_pred, worklist);
84
85 // A node with no fresh predecessors is ready to be settled right away.
86 if (!spawned_pred) {
87 worklist.push(node);
88 world().log().d("FVA: init {}", mut);
89 }
90 return {node, true};
91}
92
93void FreeDefAna::propagate(NodeQueue& worklist) {
94 while (!worklist.empty()) {
95 auto node = fe::pop(worklist);
96 if (is_done(node)) continue;
97 auto changed = is_bot(node);
98 mark(node);
99 for (auto pred : node->preds)
100 for (auto pfv : pred->fvs)
101 changed |= node->add_fvs(pfv).second;
102 if (changed)
103 for (auto succ : node->succs)
104 worklist.push(succ);
105 }
106}
107
109 auto worklist = NodeQueue();
110 auto [node, _] = build_node(lam, worklist);
111 if (!is_done(node)) {
112 ++cur_pass_;
113 propagate(worklist);
114 }
115 return node->fvs;
116}
117
118/*
119 * Closure Conversion
120 */
121
123 push(); // each external gets its own substitution scope
124 auto new_def = rewrite_root(old_mut);
125 pop();
126 // Non-closure externals (e.g. data) carry their external-ness over directly;
127 // converted Lam%s are externalized through their wrapper inside make_stub instead.
128 if (auto new_mut = new_def->isa_mut(); new_mut && old_mut->is_external() && !new_mut->is_external())
129 new_mut->externalize();
130}
131
133 // Rewrite the deferred closure bodies, each in isolation.
134 while (!body_worklist_.empty()) {
135 auto fn = body_worklist_.front();
136 body_worklist_.pop();
137 push();
138 rewrite_body(closures_.at(fn));
139 pop();
140 }
141}
142
143const Def* ClosConv::rewrite_imm_Pi(const Pi* pi) {
144 if (!is_bootstrapping() && Pi::isa_cn(pi)) return clos_type_of(pi);
145 return RWPhase::rewrite_imm_Pi(pi);
146}
147
149 if (!is_bootstrapping() && Pi::isa_cn(pi)) return clos_type_of(pi);
150 return RWPhase::rewrite_mut_Pi(pi);
151}
152
154 if (is_bootstrapping() || !Lam::isa_cn(old_lam)) return RWPhase::rewrite_mut_Lam(old_lam);
155
156 auto& w = new_world();
157 auto stub = make_stub(old_lam);
158 auto clos_ty = rewrite(old_lam->type());
159 // Rewrite the individual free defs, not the (possibly normalized) env tuple:
160 // its normal form may reference defs that are not free vars and hence not in the current map.
161 auto env = w.tuple(DefVec(stub.fvs.size(), [&](auto i) { return rewrite(stub.fvs[i]); }));
162 auto closure = clos_pack(env, stub.fn, clos_ty);
163 log().d("pack {} → {}: {}", old_lam, closure, clos_ty);
164 return map(old_lam, closure);
165}
166
168 if (is_bootstrapping()) return RWPhase::rewrite_imm_App(app);
169
170 if (auto a = Axm::isa<attr>(app))
171 if (auto handled = rewrite_attr(a)) return handled;
172
173 auto new_callee = rewrite(app->callee());
174 auto new_arg = rewrite(app->arg());
175 if (new_callee->type()->isa<Sigma>()) return clos_apply(new_callee, new_arg);
176 return new_world().app(new_callee, new_arg);
177}
178
179const Def* ClosConv::rewrite_attr(Axm::IsA<attr, App> a) {
180 auto& w = new_world();
181 switch (a.id()) {
182 case attr::returning:
183 // A return continuation is *not* closure converted; it stays a plain Cn sharing the enclosing scope.
184 // After η-expansion this should be its only occurrence, so mapping it into the current scope suffices.
185 if (auto ret_lam = a->arg()->isa_mut<Lam>()) {
186 auto new_doms = DefVec(ret_lam->num_doms(), [&](auto i) { return rewrite(ret_lam->dom(i)); });
187 auto new_lam = w.mut_lam(w.cn(new_doms))->set(ret_lam->dbg_key());
188 map(ret_lam, new_lam);
189 if (ret_lam->is_set()) new_lam->set(rewrite(ret_lam->filter()), rewrite(ret_lam->body()));
190 return new_lam;
191 }
192 return nullptr;
194 case attr::free_bb: {
195 // A free/first-class basic block captures nothing: it gets an empty environment and its body is
196 // rewritten right here, sharing the enclosing scope (same η-conversion remark as above).
197 auto bb_lam = a->arg()->isa_mut<Lam>();
198 assert(bb_lam && Lam::isa_basicblock(bb_lam));
199 auto stub = make_stub({}, bb_lam);
200 auto pack = clos_pack(w.tuple(), stub.fn, rewrite(bb_lam->type()));
201 map(bb_lam, pack);
202 rewrite_body(stub);
203 return pack;
204 }
205 default: return nullptr;
206 }
207}
208
210 // A closure body may still refer to a ret_var of an *enclosing* Lam: return continuations are not
211 // closure-converted, and the FVA deliberately excludes ret_vars, so they are never captured into an env.
212 // Map such a projection onto the corresponding var of the enclosing Lam's converted stub.
213 // This is a known workaround; the principled fix is to capture escaping enclosing BBs/ret_vars in the
214 // environment (tracked by issue #117).
215 if (!is_bootstrapping())
216 if (auto [var, lam] = isa_var_proj<Lam>(ex); var && lam && lam->ret_var() == var) {
217 auto new_fn = make_stub(lam).fn;
218 auto new_idx = skip_env(env_param(new_fn->type()->as<Pi>()), Lit::as(var->index()));
219 return new_fn->var(new_idx);
220 }
221 return RWPhase::rewrite_imm_Extract(ex);
222}
223
225 // Globals are rewritten once and shared, in isolation from any surrounding continuation scope.
226 if (auto i = glob_muts_.find(global); i != glob_muts_.end()) return i->second;
227 push();
228 auto new_global = RWPhase::rewrite_mut_Global(global);
229 pop();
230 return glob_muts_[global] = new_global;
231}
232
233const Pi* ClosConv::rewrite_ret_cn(const Pi* pi) {
234 assert(Pi::isa_basicblock(pi));
235 return new_world().cn(DefVec(pi->num_doms(), [&](auto i) { return rewrite(pi->dom(i)); }));
236}
237
238const Def* ClosConv::clos_type_of(const Pi* pi, const Def* env_type) {
239 if (!env_type)
240 if (auto i = glob_muts_.find(pi); i != glob_muts_.end()) return i->second;
241
242 auto new_doms = DefVec(pi->num_doms(), [&](auto i) {
243 return (i == pi->num_doms() - 1 && Pi::isa_returning(pi)) ? rewrite_ret_cn(pi->ret_pi()) : rewrite(pi->dom(i));
244 });
245 auto ct = ctype(new_world(), new_doms, env_type);
246 if (!env_type) {
247 glob_muts_.emplace(pi, ct);
248 log().d("closure type: {} → {} (pretyped)", pi, ct);
249 } else {
250 log().d("closure type: {} → {} (env = {})", pi, ct, env_type);
251 }
252 return ct;
253}
254
255ClosConv::Stub ClosConv::make_stub(const DefSet& fvs, Lam* old_lam) {
256 auto& w = new_world();
257 // Sort by gid: fv_vec's order *is* the closure environment layout (see rewrite_body's env_val->proj(i)),
258 // and iterating a DefSet (absl::flat_hash_set) leaves it at the mercy of hash-table insertion order -
259 // so unrelated changes elsewhere silently permuted the env slots.
260 auto fv_vec = DefVec(fvs.begin(), fvs.end());
261 std::ranges::sort(fv_vec, GIDLt<const Def*>());
262 auto env_type = rewrite(old_world().tuple(fv_vec)->type());
263 auto new_fn_type = clos_type_of(old_lam->type(), env_type)->as<Pi>();
264 auto new_fn = w.mut_lam(new_fn_type)->set(old_lam->dbg_key());
265
266 if (!isa_optimizable(old_lam)) {
267 // External or imported (unset) Lam%s get an η-wrapper that hides the environment.
268 auto ep = env_param(new_fn_type);
269 auto new_ext_type = w.cn(clos_remove_env(ep, new_fn_type->dom()));
270 auto new_ext_lam = w.mut_lam(new_ext_type)->set(old_lam->dbg_key());
271 log().d("wrap external lam {} → stub {}, external {}", old_lam, new_fn, new_ext_lam);
272 if (old_lam->is_set()) {
273 if (old_lam->is_external()) new_ext_lam->externalize();
274 auto env = w.tuple(DefVec(fv_vec.size(), [&](auto i) { return rewrite(fv_vec[i]); }));
275 new_ext_lam->app(false, new_fn, clos_insert_env(ep, env, new_ext_lam->var()));
276 // new_fn's body is rewritten later via the body worklist.
277 } else {
278 new_ext_lam->unset();
279 new_fn->app(false, new_ext_lam, clos_remove_env(ep, new_fn->var()));
280 }
281 }
282
283 log().d("stub {} → {}", old_lam, new_fn);
284 auto stub = Stub{old_lam, std::move(fv_vec), new_fn};
285 closures_.try_emplace(old_lam, stub);
286 closures_.try_emplace(new_fn, stub);
287 return stub;
288}
289
290ClosConv::Stub ClosConv::make_stub(Lam* old_lam) {
291 if (auto i = closures_.find(old_lam); i != closures_.end()) return i->second;
292 auto stub = make_stub(fva_.run(old_lam), old_lam);
293 body_worklist_.emplace(stub.fn);
294 return stub;
295}
296
297void ClosConv::rewrite_body(const Stub& stub) {
298 auto old_fn = stub.old_fn;
299 if (!old_fn->is_set()) return;
300
301 auto& w = new_world();
302 auto new_fn = stub.fn;
303 auto ep = env_param(new_fn->type()->as<Pi>());
304 auto env_val = new_fn->var(ep)->set("closure_env");
305 log().d("rewrite body of {} → {}", old_fn, new_fn);
306 if (stub.fvs.size() == 1) {
307 map(stub.fvs.front(), env_val);
308 } else {
309 for (size_t i = 0, e = stub.fvs.size(); i != e; ++i) {
310 auto fv = stub.fvs[i];
311 auto sym = w.sym("fv_"s + (fv->sym() ? fv->sym().str() : std::to_string(i)));
312 map(fv, env_val->proj(i)->set(sym));
313 }
314 }
315
316 auto params = w.tuple(DefVec(old_fn->num_doms(), [&](auto i) { return new_fn->var(skip_env(ep, i)); }));
317 map(old_fn->var(), params);
318 new_fn->set(rewrite(old_fn->filter()), rewrite(old_fn->body()));
319}
320
321} // namespace mim::plug::clos::phase
const Def * callee() const
Definition lam.h:275
const Def * arg() const
Definition lam.h:284
static auto isa(const Def *def)
Definition axm.h:112
Base class for all Defs.
Definition def.h:273
bool is_external() const noexcept
Definition def.h:553
Extracts from a Sigma or Array-typed Extract::tuple the element at position Extract::index.
Definition tuple.h:161
A function.
Definition lam.h:113
static const Lam * isa_cn(const Def *d)
Definition lam.h:144
const Pi * type() const
Definition lam.h:133
static const Lam * isa_basicblock(const Def *d)
Definition lam.h:145
static T as(const Def *def)
Definition def.h:943
Nest(Def *root)
Definition nest.cpp:9
const fe::Log & log() const
Definition phase.h:79
A dependent function type.
Definition lam.h:14
static const Pi * isa_cn(const Def *d)
Definition lam.h:46
static const Pi * isa_basicblock(const Def *d)
Is this a continuation (Pi::isa_cn) that is not Pi::isa_returning?
Definition lam.h:56
bool is_bootstrapping() const
Returns whether we are currently bootstrapping (rewriting annexes).
Definition phase.h:403
virtual const Def * rewrite_root(const Def *def)
Rewrites a root - i.e. an annex or an external.
Definition phase.h:411
World & new_world()
Create new Defs into this.
Definition phase.h:452
World & old_world()
Get old Defs from here.
Definition phase.h:451
virtual void push()
Definition rewrite.h:40
virtual const Def * map(const Def *old_def, const Def *new_def)
Definition rewrite.h:47
virtual void pop()
Definition rewrite.h:41
virtual const Def * rewrite(const Def *)
Definition rewrite.cpp:55
A dependent tuple type.
Definition tuple.h:23
const Pi * cn()
Definition world.h:385
const Def * app(const Def *callee, const Def *arg)
Definition world.cpp:237
const Def * rewrite_mut_Global(Global *) final
void rewrite_external(Def *) final
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 for an RWPhase 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.
const Def * clos_remove_env(size_t ep, size_t i, std::function< const Def *(size_t)> f)
Definition clos.cpp:124
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:126
const Def * clos_insert_env(size_t ep, size_t i, const Def *env, std::function< const Def *(size_t)> f)
Definition clos.cpp:120
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:108
const Def * clos_pack(const Def *env, const Def *fn, const Def *ct=nullptr)
Pack a typed closure.
Definition clos.cpp:61
const Def * clos_apply(const Def *closure, const Def *args)
Apply a closure to arguments.
Definition clos.cpp:78
size_t env_param(Defs doms)
Describes where the environment is placed in the argument list: right after a leading mem....
Definition clos.h:101
@ Var
Depends on a Var.
Definition def.h:136
Lam * isa_optimizable(Lam *lam)
These are Lams that are.
Definition lam.h:349
fe::Vector< const Def * > DefVec
Definition def.h:93
GIDSet< const Def * > DefSet
Definition def.h:89
Node
Definition def.h:120
@ Extract
Definition def.h:122