MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
seo.cpp
Go to the documentation of this file.
2
3#include <mim/lam.h>
4
5#include "mim/plug/mem/mem.h"
6
7namespace mim::plug::mem::phase {
8
9/*
10 * Helpers
11 */
12
13// All of them track their Vars: liveness and ⊥-site visibility depend on them.
14enum {
15 Proxy_SCCP_Top, // proxy(var) <- var reached ⊤ for propagation but still awaits GVN bundling
16 Proxy_Bundle, // proxy(lam, var1, var2, ..., varn) <- GVN congruence class
17 Proxy_Sloxy, // proxy(lam, ptr) <- slot invoked at lam where ptr is the slot cons's ptr var
18 Proxy_Phi, // proxy(lam_var, sloxy) <- phi we need at lam for sloxy
19};
20
21// vars are the tvars followed by the phis.
22static size_t idx_of(Defs vars, const Def* p) {
23 if (auto [_, i] = Var::isa_tproj(p); i) {
24 assert(vars[*i] == p);
25 return *i;
26 }
27 auto i = std::ranges::find(vars, p);
28 assert(i != vars.end());
29 return i - vars.begin();
30}
31
32static const Proxy* isa_bundle(const Def* def, Lam* lam);
33
36 visited_.clear();
37 lam2sloxy2val_.clear();
38 first_.clear();
39 non_mem_fvs_.clear();
40}
41
42/*
43 * Main Analysis
44 */
45
46// SCCP
47
48const Proxy* SEO::Analysis::mk_sccp_top(const Def* var) {
49 return world().proxy(var->type(), {var}, Proxy_SCCP_Top, true);
50}
51
52const Def* SEO::Analysis::sccp_join(Lam* lam, const Def* var, const Def* def) {
53 log().d("sccp join: {} ⊔ {}", var, def);
54 auto cur = lattice(var);
55 if (cur == var) return var; // ⊤ is final
56
57 // cur ⊔ ⊥ = cur, but a ⊥-site must see what var propagates.
58 // As ⊥ never restarts the join, cur is at worst last round's value; a quiet round thus checks the final one.
59 if (def->isa<Bot>()) {
60 if (!cur) return lattice(var, def), def;
61 if (auto caller = curr_mut(); caller && !Proxy::isa<Proxy_Sloxy>(cur) && !cur->in_scope(caller)) {
62 log().d("cannot propagate {} → {}: invisible at ⊥-site {}", var, cur, caller);
63 return pin(var), var;
64 }
65 return cur;
66 }
67
68 // Pin mem.M-typed vars to top: mem must stay threaded through every lam,
69 // as later stages (clos conversion, ll backend) rely on each lam having its own mem var.
70 if (Axm::isa<mem::M>(var->type())) return pin(var), var;
71
72 // A site passing `var` itself (a backedge threading a loop-invariant argument through) contributes
73 // nothing: the other call sites alone determine the value.
74 if (def == var) return cur ? cur : var;
75
76 // `⊥ ⊔ x` is `x`, but unusable if lam nests it.
77 // A closed def can never be nested, and Def::nests allocates a fresh MutSet per call - so skip it.
78 if (!def->isa<Proxy>() && !def->is_closed() && lam->nests(def)) {
79 log().d("cannot propagate {} → {}: out of scope", var, def);
80 return pin(var), var;
81 }
82
83 // Frozen: a ⊤ / this-lam's bundle wins and is kept across rounds (needs cur to exist).
84 if (cur && Proxy::isa<Proxy_SCCP_Top>(cur)) return cur;
85 if (cur && isa_bundle(cur, lam)) return cur;
86
87 // First touch of `var` this round: restart the join here.
88 // Discarding what earlier sites contributed requires a round that revisits *every* `lam` call site;
89 // apply_known() taints all of lam2callers_ whenever the abstract vars change, so the next round does.
90 if (auto [_, ins] = first_.emplace(var); ins) {
91 log().d("first contribution; restart: {} → {}", var, def);
92 lattice_force(var, def); // may descend from an earlier round's ⊤ - hence force, not lattice()
93 return def;
94 }
95
96 // Every first_ insertion also writes the lattice, so past the first touch `cur` exists;
97 // and it cannot be ⊤, as the `cur == var` check bailed out on entry.
98 assert(cur && cur != var);
99 if (cur == def) return cur; // def ⊔ def = def
100 if (cur->isa<Bot>()) return lattice(var, def), def; // ⊥ ⊔ def = def
101
102 log().d("cannot propagate {} → {}; cur = {}; try GVN", var, def, cur);
103 auto top = mk_sccp_top(var);
104 return lattice(var, top), top;
105}
106
107// GVN
108
109static const Proxy* isa_bundle(const Def* def, Lam* lam) {
110 if (auto bundle = Proxy::isa<Proxy_Bundle>(def); bundle && bundle->op(0) == lam) return bundle;
111 return nullptr;
112}
113
114const Proxy* SEO::Analysis::mk_bundle(Lam* lam, const Def* var, Defs bundle_vars) {
115 return world().proxy(var->type(), Def::cat(lam, bundle_vars), Proxy_Bundle, true)->set(var->dbg_key());
116}
117
118void SEO::Analysis::gvn_bundle(Lam* lam, Defs vars, Defs abstr_args, fe::Span<const Def*> abstr_vars) {
119 auto n = vars.size();
120 auto idxs = fe::Vector<size_t>();
121
122 for (size_t i = 0; i != n; ++i) {
123 if (!Proxy::isa<Proxy_SCCP_Top>(abstr_vars[i])) continue;
124
125 idxs.clear();
126 idxs.emplace_back(i);
127 for (size_t j = i + 1; j != n; ++j)
128 if (Proxy::isa<Proxy_SCCP_Top>(abstr_vars[j]) && abstr_args[j] == abstr_args[i]) idxs.emplace_back(j);
129
130 if (idxs.size() == 1) {
131 pin(vars[i]);
132 abstr_vars[i] = vars[i];
133 } else {
134 auto bundle = mk_bundle(lam, vars[i], DefVec(idxs, [&](size_t j) { return vars[j]; }));
135 for (auto j : idxs)
136 lattice(vars[j], abstr_vars[j] = bundle);
137 log().d("bundle: {}", bundle);
138 }
139 }
140}
141
142void SEO::Analysis::gvn_split(Lam* lam, Defs vars, fe::Span<const Def*> abstr_args, fe::Span<const Def*> abstr_vars) {
143 // E.g.: Say we started with `{a, b, c, d, e}` as a single bundle for all tvars of `lam`.
144 // Now, we see `lam (x, y, x, y, z)`. Then we have to build:
145 // a -> {a, c}
146 // b -> {b, d}
147 // c -> {a, c}
148 // d -> {b, d}
149 // e -> e (top)
150 auto split = DefVec();
151
152 for (size_t i = 0, n = vars.size(); i != n; ++i) {
153 if (auto bundle = isa_bundle(abstr_vars[i], lam)) {
154 auto members = bundle->ops().subspan(1);
155 assert(members.size() > 1 && "a bundle is only ever built for a congruence class");
156
157 split.clear();
158 for (auto p : members)
159 if (abstr_args[idx_of(vars, p)] == abstr_args[i]) split.emplace_back(p);
160
161 if (split.size() != members.size()) {
162 // vars[i] is a member of its own bundle and trivially agrees with itself, so it is in split.
163 if (split.size() == 1) {
164 pin(vars[i]);
165 abstr_vars[i] = vars[i];
166 log().d("GVN single: {}", vars[i]);
167 } else {
168 auto new_bundle = mk_bundle(lam, vars[i], split);
169 log().d("GVN split: {}", new_bundle);
170 for (auto p : split) {
171 auto j = idx_of(vars, p);
172 lattice(vars[j], abstr_vars[j] = new_bundle);
173 }
174 }
175 }
176 }
177 }
178}
179
180// SSA
181
182// lam's Var makes the phi only visible where lam's future param is.
183static const Def* mk_phi(World& w, Lam* lam, const Def* sloxy) {
184 return w.proxy(pointee(sloxy), {lam->var(), sloxy}, Proxy_Phi, true)->set(sloxy->dbg_key());
185}
186
187const Def* SEO::Analysis::lam2sloxy2val(Lam* lam, const Def* sloxy) {
188 if (auto sloxy2val = fe::lookup(lam2sloxy2val_, lam))
189 if (auto val = fe::lookup(*sloxy2val, sloxy)) return val;
190
191 auto phi = mk_phi(world(), lam, sloxy);
192 log().d("no value for sloxy {}; use phi {}", sloxy, phi);
193 if (auto val = lattice(phi); val && !Proxy::isa<Proxy_SCCP_Top>(val)) return val;
194 return nullptr;
195}
196
197bool SEO::Analysis::can_supply(Lam* lam, const Def* sloxy) {
198 if (auto i = lam2callers_.find(lam); i != lam2callers_.end())
199 for (auto caller : i->second)
200 if (auto c = caller->isa_mut<Lam>(); c && !lam2sloxy2val(c, sloxy)) return false;
201 return true;
202}
203
204static const Var* slot_var(const Def* ptr) { return ptr->as<Extract>()->tuple()->as<Var>(); }
205
206/// The sloxy @p abstr_ptr points into, and the literal mem.lea indices on the way, outermost first.
207static std::pair<const Proxy*, DefVec> sloxy_path(const Def* abstr_ptr) {
208 auto path = DefVec();
209 for (auto ptr = abstr_ptr;;) {
210 if (auto sloxy = Proxy::isa<Proxy_Sloxy>(ptr)) {
211 std::ranges::reverse(path);
212 return {sloxy, path};
213 } else if (auto lea = Axm::isa<mem::lea>(ptr)) {
214 auto [base, index] = lea->args<2>();
215 auto agg = pointee(base);
216 auto n = Lit::isa(agg->arity());
217 if (!index->isa<Lit>() || !n || agg->num_tprojs() != *n) return {};
218 path.emplace_back(index);
219 ptr = base;
220 } else
221 return {};
222 }
223}
224
225Vars SEO::Analysis::reachable_slots(VarWorklist& todo) {
226 if (var2slot_.empty()) return {};
227
228 auto& vars = world().vars();
229 auto res = Vars();
230 while (!todo.empty()) {
231 auto var = todo.pop();
232 if (var2slot_.contains(var)) res = vars.insert(res, var);
233 if (auto abstr = lattice(var); abstr && abstr != var) todo.push(non_mem_free_vars(abstr));
234 if (auto stored = fe::lookup(slot2stored_, var)) todo.push(*stored);
235 }
236 return res;
237}
238
239void SEO::Analysis::live(Lam* lam, Defs abstr_args) {
240 auto todo = VarWorklist();
241 todo.push(lam->free_vars());
242 if (auto var = lam->has_var()) todo.push(var);
243 for (auto arg : abstr_args)
244 todo.push(arg->free_vars());
245
246 auto& live = lam2live_[lam];
247 auto grown = world().vars().merge(live, reachable_slots(todo));
248 if (grown != live) live = grown, touch();
249}
250
251Vars SEO::Analysis::non_mem_free_vars(const Def* def) {
252 if (isa_mem(def) || def->is_closed()) return {};
253 if (auto var = def->isa<Var>()) return Vars{var};
254 if (def->isa_mut()) return def->free_vars();
255 if (auto res = fe::lookup(non_mem_fvs_, def)) return *res;
256
257 auto res = Vars();
258 for (auto d : def->deps())
259 res = world().vars().merge(res, non_mem_free_vars(d));
260 return non_mem_fvs_[def] = res;
261}
262
263void SEO::Analysis::pin_reachable_slots(const Def* abstr_callee, const Def* abstr_arg) {
264 if (var2slot_.empty()) return;
265
266 auto fvsc = non_mem_free_vars(abstr_callee);
267 auto fvsa = non_mem_free_vars(abstr_arg);
268 auto todo = VarWorklist();
269 todo.push(world().vars().merge(fvsc, fvsa));
270 for (auto var : reachable_slots(todo))
271 if (auto slot = var2slot_[var]; !is_top(slot)) {
272 log().d("slot var {} reaches a direct-style call", var);
273 pin_slot(slot);
274 }
275}
276
277void SEO::Analysis::pin_slot(const App* slot) {
278 auto [_, ret_lam, __, ptr] = split_slot(slot);
279 pin(slot);
280 pin(ptr);
281 // Once slot is ⊤, apply_known no longer refreshes the whole var, whose stale sloxy would resurface via ptr.
282 pin(ret_lam->var());
283 if (auto i = slot2resolved_.find(ret_lam->has_var()); i != slot2resolved_.end())
284 for (auto concr : i->second)
285 lattice_erase(concr);
286}
287
288void SEO::Analysis::resolve(const Def* sloxy, const Def* concr, const Def* abstr) {
289 lattice(concr, abstr);
290 slot2resolved_[slot_var(sloxy->op(1))].emplace(concr);
291}
292
293void SEO::Analysis::propagate_phis(Lam* lam, DefVec& phis, DefVec& abstr_args) {
294 const auto& live = lam2live_[lam];
295 for (auto ptr : slots()) {
296 if (!live.contains(slot_var(ptr))) {
297 profile_count("seo.phis.dead");
298 } else if (auto sloxy = Proxy::isa<Proxy_Sloxy>(rewrite(ptr))) {
299 if (auto value = sloxy2val(sloxy)) {
300 auto phi = mk_phi(world(), lam, sloxy);
301 phis.emplace_back(phi);
302 abstr_args.emplace_back(value);
303 log().d("propagate phi {} for sloxy {} with value {}", phi, sloxy, value);
304 } else {
305 log().d("no value for sloxy {}", sloxy);
306 }
307 }
308 }
309}
310
311const Def* SEO::Analysis::apply_known(Lam* known, Defs abstr_targs) {
312 auto n = abstr_targs.size();
313 assert(n == known->num_tvars());
314 log().d("known edge: {} → {}", curr_mut(), known);
315 if (auto mut = curr_mut()) lam2callers_[known].emplace(mut);
316 rewrite(known); // enqueue so its body is drained this round; a no-op if already scheduled
317
318 if (known->is_dependent()) {
319 log().d("dependent signature; keeping all vars: {}", known);
320 for (auto var : known->tvars())
321 pin(var);
322 return abstract_app(known, world().tuple(abstr_targs));
323 }
324
325 auto v = version();
326
327 auto all_vars = known->tvars();
328 DefVec all_abstr_args(n, [&](size_t i) {
329 // Strips EtaConv's per-occurrence wrappers so that call sites passing the same continuation still agree.
330 if (auto f = Lam::eta_reduce(abstr_targs[i])) return profile_count("seo.eta.canonicalized"), f;
331 return abstr_targs[i];
332 });
333
334 live(known, all_abstr_args);
335 propagate_phis(known, all_vars, all_abstr_args);
336
337 auto all_abstr_vars = DefVec(all_vars.size());
338 for (size_t i = 0, e = all_vars.size(); i != e; ++i)
339 all_abstr_vars[i] = sccp_join(known, all_vars[i], all_abstr_args[i]);
340
341 gvn_bundle(known, all_vars, all_abstr_args, all_abstr_vars);
342 gvn_split(known, all_vars, all_abstr_args, all_abstr_vars);
343
344 lattice(known->var(), world().tuple(all_abstr_vars.span().subspan(0, n)));
345
346 for (size_t i = n, e = all_vars.size(); i != e; ++i)
347 lattice(all_vars[i], all_abstr_vars[i]);
348
349 // Something about known's abstract vars/phis changed: the next round must re-join them from *all* call
350 // sites - sccp_join's first_-restart discards any unvisited site's contribution, so re-visiting only the
351 // writer would be unsound in a sparse round (e.g. it would constant-fold a loop's backedge forever).
352 if (version() != v)
353 for (auto caller : lam2callers_[known])
354 taint(caller);
355
356 return abstract_app(known, world().tuple(all_abstr_args.span().subspan(0, n)));
357}
358
359const Def* SEO::Analysis::abstract_app(const Def* callee, const Def* arg) {
360 // Only an Axm keeps the checked, normalizing World::app: its normalizer is what folds a propagated
361 // expression. Everywhere else abstract operands (⊤/⊥/proxies) need not be assignable to the domain -
362 // and World::app would either throw or partially evaluate the callee.
363 if (std::get<0>(Axm::next(callee))) return world().app(callee, arg);
364 auto pi = callee->type()->isa<Pi>();
365 return world().raw_app(pi ? pi->reduce(arg) : arg->type(), callee, arg);
366}
367
368const Def* SEO::Analysis::rewrite_imm_App(const App* app) {
369 if (auto slot = Axm::isa<mem::slot>(app)) {
370 if (!is_top(slot)) {
371 if (auto [mem, ret_lam, _, ptr] = split_slot(slot); ret_lam) {
372 auto abstr_mem = rewrite(mem);
373 auto sloxy = world().proxy(ptr->type(), {curr_mut(), ptr}, Proxy_Sloxy, true)->set(slot->dbg_key());
374 slots_.emplace(ptr);
375 var2slot_[ret_lam->has_var()] = slot;
376 log().d("slot {} → sloxy {}", ptr, sloxy);
377 // The slot is ptr's *defining* site: mark first_ so the ⊥ joined below cannot restart it away.
378 fe::assert_emplace(first_, ptr);
379 lattice(ptr, sloxy);
380 sloxy2val(sloxy, world().bot(pointee(sloxy))); // a load before any store yields undef
381 // Treat the slot jump like an app of `ret_lam` so mem and existing phis flow across the edge.
382 // The ptr var is defined *by* the slot, so pass ⊥ (not the sloxy) as its abstract argument:
383 // this keeps the sloxy out of the abstract body, so it only survives if an unresolved
384 // load/store actually references it - `lattice(ptr, sloxy)` above still drives that resolution.
385 return apply_known(ret_lam, {abstr_mem, world().bot(ptr->type())});
386 }
387 }
388 } else if (auto store = Axm::isa<mem::store>(app)) {
389 auto [mem, ptr, val] = store->args<3>();
390 auto abstr_mem = rewrite(mem);
391 auto abstr_ptr = rewrite(ptr);
392 auto abstr_val = rewrite(val);
393
394 if (auto [sloxy, path] = sloxy_path(abstr_ptr); sloxy) {
395 if (auto agg = path.empty() ? abstr_val : sloxy2val(sloxy)) {
396 auto& stored = slot2stored_[slot_var(sloxy->op(1))];
397 if (auto grown = world().vars().merge(stored, non_mem_free_vars(abstr_val)); grown != stored)
398 stored = grown, touch();
399 if (!path.empty()) resolve(sloxy, ptr, abstr_ptr); // lets the rewrite recognize a promoted lea
400 sloxy2val(sloxy, path.empty() ? abstr_val : world().insert(agg, world().tuple(path), abstr_val));
401 log().d("store: {} ← {}", abstr_ptr, abstr_val);
402 return abstr_mem;
403 }
404 }
405 log().d("unresolved store: {} ← {}", abstr_ptr, abstr_val);
406 } else if (auto load = Axm::isa<mem::load>(app)) {
407 auto [mem, ptr] = load->args<2>();
408 auto [_, val] = load->projs<2>();
409 auto abstr_mem = rewrite(mem);
410 auto abstr_ptr = rewrite(ptr);
411
412 if (auto [sloxy, path] = sloxy_path(abstr_ptr); sloxy) {
413 if (auto agg = sloxy2val(sloxy)) {
414 auto abstr_val = path.empty() ? agg : world().extract(agg, world().tuple(path));
415 log().d("load: {} → {}", abstr_ptr, abstr_val);
416 if (!path.empty()) resolve(sloxy, ptr, abstr_ptr);
417 resolve(sloxy, val, abstr_val);
418 return world().tuple({abstr_mem, abstr_val});
419 }
420 log().d("load of unknown value: {}", sloxy);
421 } else {
422 log().d("load from unknown ptr: {}", abstr_ptr);
423 }
424 } else {
425 auto abstr_callee = rewrite(app->callee());
426 auto abstr_arg = rewrite(app->arg());
427 // A direct-style call's effects bypass the continuation the per-Lam store state follows.
428 if (has_mem_ty(app->type())) pin_reachable_slots(abstr_callee, abstr_arg);
429 if (auto known = Lam::isa_rewritable(abstr_callee)) {
430 auto n = known->num_tvars();
431 return apply_known(known, abstr_arg->projs(n));
432 }
433 }
434
435 // Rewrite the arg before the callee; see Rewriter::rewrite_imm_App.
436 auto new_arg = rewrite(app->arg());
437 auto new_callee = rewrite(app->callee());
438 return abstract_app(new_callee, new_arg);
439}
440
442 if (auto src = curr_mut()->isa<Lam>(); src && src->is_set()) {
443 auto phi_dsts = fe::Vector<Lam*>();
444 auto phi_vars = DefVec();
445 auto phi_abstr_args = DefVec();
446
447 auto propagate_unknowns = [this, &phi_dsts, &phi_vars, &phi_abstr_args, src](const Def* abstr) {
448 for (auto mut : abstr->local_muts())
449 if (auto dst = mut->isa<Lam>(); dst && dst->is_open()) {
450 log().d("unknown edge: {} → {}", src, dst);
451 live(dst, {});
452 propagate_phis(dst, phi_vars, phi_abstr_args);
453 phi_dsts.resize(phi_vars.size(), dst);
454 }
455 };
456
457 auto abstr = rewrite(src->body());
458 if (auto app = abstr->isa<App>()) {
459 if (!app->callee()->isa<Lam>()) propagate_unknowns(app->callee());
460 propagate_unknowns(app->arg());
461 }
462
463 // Several unknown edges may reach the same phi in one round.
464 for (size_t i = 0, e = phi_vars.size(); i != e; ++i)
465 sccp_join(phi_dsts[i], phi_vars[i], phi_abstr_args[i]);
466 }
467}
468
469/*
470 * Post-Analysis:
471 * Finds sloxies that are still present + unknown lambdas
472 */
473
474static bool keep(Lam* lam, const Def* old_var, const Def* abstr) {
475 if (!abstr) return true; // no info -> keep
476 if (old_var == abstr) return true; // ⊤ -> keep
477 if (abstr->isa<Bot>()) return false; // ⊥ -> nuke
478 if (Proxy::isa<Proxy_SCCP_Top>(abstr)) return true; // pending ⊤: nothing was propagated -> keep
479 if (auto bundle = isa_bundle(abstr, lam)) return bundle->op(1) == old_var; // use first in GVN bundle
480 return false; // propagated value -> nuke
481}
482
484 for (auto def : world().externals().muts())
485 analyze(def);
486}
487
488void SEO::Analysis::analyze(const Def* def) {
489 if (def->isa<Var>()) return; // do not run escape analysis through a Var (would remap it via lookup)
490 if (auto [_, ins] = visited_.emplace(def); !ins) return;
491 if (auto l = lookup(def)) def = l; // get abstracted value of def
492
493 if (auto proxy = def->isa<Proxy>()) {
494 if (proxy->tag() == Proxy_Sloxy) {
495 auto slot = var2slot_[slot_var(proxy->op(1))];
496 log().d("sloxy {} survived; pinning slot {} to ⊤", proxy, slot);
497 pin_slot(slot);
498 }
499 return; // never walk a proxy's deps (would drag in meta info)
500 }
501
502 // A Lam is unknown (and hence its vars must go to top) iff it is reached as a *value*.
503 if (auto app = def->isa<App>()) {
504 if (auto slot = Axm::isa<mem::slot>(app)) {
505 // The slot jump applies its continuation, so `ret_lam` is known - not reached as a value.
506 if (auto [mem, ret_lam, _, __] = split_slot(slot); ret_lam) {
507 analyze(app->type());
508 analyze(mem); // the ptr var has no argument - the slot itself defines it
509 for (auto d : ret_lam->deps())
510 analyze(d);
511 return;
512 }
513 }
514
515 if (auto lam = Lam::isa_rewritable(app->callee())) {
516 // lam is applied here, it's known: traverse its body without pinning its vars to top
517 analyze(app->type());
518
519 // only analyze args that we keep
520 for (size_t i = 0, e = lam->num_tvars(); i != e; ++i)
521 if (auto old_var = lam->tvar(i); keep(lam, old_var, lattice(old_var))) analyze(app->targ(i));
522
523 for (auto d : lam->deps())
524 analyze(d);
525
526 return;
527 }
528 } else if (auto [lam, var] = def->isa_binder<Lam>(); lam) {
529 log().d("unknown lam: {}", lam);
530 unknowns_.emplace(lam);
531 pin(var); // apply_known's whole-var tuple would otherwise resurface an earlier round's value via repr
532 for (auto v : var->tprojs())
533 pin(v);
534 }
535
536 for (auto d : def->deps())
537 analyze(d);
538}
539
540/*
541 * Transformation:
542 * Apply analysis info to code
543 */
544
545const Def* SEO::isa_promoted(const Def* old_ptr) const {
546 if (auto abstr = lattice(old_ptr))
547 if (auto [sloxy, _] = sloxy_path(abstr); sloxy && lattice(sloxy->op(1)) == sloxy) return sloxy;
548 return nullptr;
549}
550
552 if (Super::analyze()) return true;
553
554 for (auto ptr : analysis_.slots())
555 if (auto sloxy = isa_promoted(ptr)) sloxies_.emplace_back(sloxy);
556 return false;
557}
558
559const SEO::Sig& SEO::sig_of(Lam* old_lam) {
560 auto [i, ins] = lam2sig_.emplace(old_lam, Sig());
561 auto& sig = i->second;
562 if (!ins) return sig;
563
564 auto var = old_lam->has_var();
565
566 sig.sieve = Sieve(old_lam->tvars(), [this, old_lam](const Def* var) { return keep(old_lam, var, lattice(var)); });
567
568 for (auto sloxy : sloxies_) {
569 auto phi = mk_phi(old_world(), old_lam, sloxy);
570 if (var && phi->type()->has_free_var(var)) continue; // not expressible in old_lam's signature
571 auto val = lattice(phi);
572 if (!val || Proxy::isa<Proxy_SCCP_Top>(val)) continue;
573 // build_args() needs an argument at *every* call site; else the slot has to survive.
574 if (analysis_.can_supply(old_lam, sloxy)) sig.phis.emplace_back(sloxy, phi, val);
575 }
576
577 sig.sieve.cat(sig.phis.size(), [old_lam, &sig](size_t i) {
578 auto [_, phi, val] = sig.phis[i];
579 return keep(old_lam, phi, val);
580 });
581
582 // A new signature is needed iff some var is dropped/propagated/merged or some phi has to be threaded in -
583 // except for an unknown lam, which is used as a value somewhere and hence must keep its signature as is.
584 sig.todo = !sig.sieve.all() && !analysis_.unknowns().contains(old_lam);
585
586 return sig;
587}
588
589const Def* SEO::rewrite_imm_App(const App* old_app) {
590 if (auto slot = Axm::isa<mem::slot>(old_app)) {
591 auto [mem, ret_lam, _, ptr] = split_slot(slot);
592 if (!ret_lam) return Super::rewrite_imm_App(old_app);
593
594 if (isa_promoted(ptr)) {
595 // The slot was promoted away: jump straight to the (rebuilt) continuation, dropping the ptr var.
596 profile_count("seo.slots.eliminated");
597 assert(!analysis_.unknowns().contains(ret_lam)); // promoted -> ret_lam was never reached as a value
598 auto new_lam = build_lam(ret_lam);
599 auto new_args = build_args(ret_lam, {mem, ptr});
600 return map(old_app, new_world().app(new_lam, new_args));
601 }
602
603 // The slot survives: keep the allocation, forwarding the continuation.
604 auto [T, a] = slot->decurry()->args<2>();
605 auto new_mem = rewrite(mem);
606 auto new_ret_lam = rewrite(ret_lam)->as_mut<Lam>();
607 return map(old_app, mem::op_slot(rewrite(T), rewrite(a), new_mem, new_ret_lam));
608 } else if (auto store = Axm::isa<mem::store>(old_app)) {
609 auto [mem, ptr, val] = store->args<3>();
610 if (isa_promoted(ptr)) return rewrite(mem);
611 } else if (auto load = Axm::isa<mem::load>(old_app)) {
612 auto [res_mem, res_val] = load->projs<2>();
613 auto [mem, ptr] = load->args<2>();
614 if (auto sloxy = isa_promoted(ptr)) {
615 auto abstr_val = abstracted(res_val);
616 assert(abstr_val && "a promoted slot implies every load from it resolved");
617 log().d("load from {} resolved to {}", sloxy, abstr_val);
618 auto new_mem = rewrite(mem);
619 return new_world().tuple({new_mem, rewrite(abstr_val)});
620 }
621 } else {
622 auto old_lam = old_app->callee()->isa_mut<Lam>();
623 if (!old_lam) {
624 // The callee may fold to a rebuilt lam in the new world only,
625 // e.g. a branch `(f, t)#cond` whose cond becomes constant after GVN merged vars.
626 if (auto new_lam = rewrite(old_app->callee())->isa_mut<Lam>())
627 if (auto ol = fe::lookup(lam_new2old_, new_lam)) old_lam = ol;
628 }
629
630 if (old_lam) {
631 log().d("in {}: app of {}", curr_mut(), old_lam);
632
633 if (sig_of(old_lam).todo) {
634 log().d("needs seo: {}", old_lam);
635 auto new_lam = build_lam(old_lam);
636 auto new_args = build_args(old_lam, old_app->targs());
637 return map(old_app, new_world().app(new_lam, new_args));
638 }
639 }
640 }
641
642 return Super::rewrite_imm_App(old_app);
643}
644
645const Def* SEO::rewrite_mut_Lam(Lam* old_lam) {
646 // A lam that gets a new signature must never be rebuilt generically:
647 // otherwise two new versions of old_lam exist and their (hash-consed, cached) body defs
648 // reference whichever version was rewritten first - leaving free vars in the other one.
649 if (sig_of(old_lam).todo) return build_lam(old_lam);
650 return Super::rewrite_mut_Lam(old_lam);
651}
652
653const Def* SEO::rewrite_imm_Var(const Var* var) {
654 // The generic Var rewrite fabricates var(new_binder) - the wrong arity for a signature build_lam()
655 // narrowed. Reachable before build_lam() maps the whole var, e.g. from rewriting a propagated value.
656 if (auto old_lam = var->binder()->isa_mut<Lam>())
657 if (sig_of(old_lam).todo) return var_of(old_lam);
658 return Super::rewrite_imm_Var(var);
659}
660
661const Def* SEO::var_of(Lam* old_lam) {
662 auto new_lam = build_lam(old_lam);
663 const auto& sig = sig_of(old_lam);
664 size_t n = old_lam->num_tvars();
665 auto elems = DefVec(n);
666
667 for (size_t i = 0; i != n; ++i)
668 if (auto j = sig.sieve[i]; j != Sieve::Gone)
669 elems[i] = new_lam->var(sig.sieve.num_new(), j);
670 else if (auto abstr = lattice(old_lam->tvar(i)); sloxy_path(abstr).first)
671 elems[i] = new_world().bot(rewrite(old_lam->tdom(i))); // a promoted slot carries no value
672 else
673 elems[i] = rewrite(abstr); // SCCP propagate
674
675 return new_world().tuple(elems);
676}
677
678Lam* SEO::build_lam(Lam* old_lam) {
679 assert(!old_lam->is_dependent() && "apply_known pins a dependent signature to \u22a4");
680 if (auto memo = fe::lookup(lam_old2new_, old_lam)) return memo;
681
682 log().d("build new lam for {}", old_lam);
683 invalidate();
684 const auto& sig = sig_of(old_lam);
685 size_t n = old_lam->num_tvars();
686 auto new_doms = DefVec();
687
688 for (size_t i = 0; i != n; ++i)
689 if (sig.sieve[i] != Sieve::Gone)
690 new_doms.emplace_back(rewrite(old_lam->tdom(i)));
691 else if (auto abstr = lattice(old_lam->tvar(i)); isa_bundle(abstr, old_lam))
692 profile_count("seo.gvn.vars_merged");
693 else if (!sloxy_path(abstr).first)
694 profile_count("seo.sccp.vars_eliminated");
695 for (size_t i = 0, e = sig.phis.size(); i != e; ++i)
696 if (sig.sieve.cat(i) != Sieve::Gone) new_doms.emplace_back(rewrite(sig.phis[i].phi->type()));
697
698 auto new_lam = new_world().mut_lam(new_doms, rewrite(old_lam->codom()))->set(old_lam->dbg_key());
699 lam_old2new_[old_lam] = new_lam;
700 lam_new2old_[new_lam] = old_lam;
701
702 // Map all *kept* vars/phis before rewriting any propagated value below: that rewrite may recursively
703 // re-enter old_lam and must be able to resolve them - see var_of().
704 for (size_t i = 0; i != n; ++i)
705 if (auto j = sig.sieve[i]; j != Sieve::Gone) {
706 auto old_var = old_lam->tvar(i);
707 auto v = new_lam->var(sig.sieve.num_new(), j)->set(old_var->dbg_key());
708 map(old_var, v);
709 if (auto bundle = isa_bundle(lattice(old_var), old_lam)) map(bundle, v); // GVN bundle
710 }
711
712 for (size_t i = 0, e = sig.phis.size(); i != e; ++i)
713 if (auto j = sig.sieve.cat(i); j != Sieve::Gone) {
714 const auto& p = sig.phis[i];
715 auto v = new_lam->var(sig.sieve.num_new(), j);
716 profile_count("phis.materialized");
717 log().d("map phi {} → {}", p.phi, v);
718 map(p.phi, v);
719 if (p.val != p.phi) map(p.val, v); // phi is part of a GVN bundle
720 }
721
722 // Map the whole var *before* rewriting a dropped phi's value below: such a value may project a *dropped*
723 // var of old_lam (e.g. its now-removed empty closure env), which only the whole var resolves.
724 map(old_lam->var(), var_of(old_lam));
725
726 for (size_t i = 0, e = sig.phis.size(); i != e; ++i)
727 if (sig.sieve.cat(i) == Sieve::Gone) {
728 const auto& p = sig.phis[i];
729 log().d("map phi {} → its propagated value {}", p.phi, p.val);
730 map(p.phi, rewrite(p.val));
731 }
732
733 auto _ = enter(old_lam);
734 new_lam->set(rewrite(old_lam->filter()), rewrite(old_lam->body()));
735 return new_lam;
736}
737
738DefVec SEO::build_args(Lam* old_lam, Defs old_targs) {
739 const auto& sig = sig_of(old_lam);
740 size_t n = old_lam->num_tvars();
741 assert(old_targs.size() == n);
742 auto new_args = DefVec();
743
744 for (size_t i = 0; i != n; ++i)
745 if (sig.sieve[i] != Sieve::Gone) new_args.emplace_back(rewrite(old_targs[i]));
746
747 for (size_t i = 0, e = sig.phis.size(); i != e; ++i)
748 if (sig.sieve.cat(i) != Sieve::Gone) {
749 auto sloxy = sig.phis[i].sloxy;
750 auto arg = analysis_.lam2sloxy2val(curr_mut<Lam>(), sloxy);
751 assert(arg);
752 log().d("wire up phi argument {} for sloxy {}", arg, sloxy);
753 new_args.emplace_back(rewrite(arg));
754 }
755
756 return new_args;
757}
758
759} // namespace mim::plug::mem::phase
virtual void leave()
Called after curr_mut() has been completely dealt with.
Definition phase.h:292
virtual void reset()
Clears the rewriter map and resets Phase::todo() for the next fixed-point iteration.
Definition phase.cpp:68
virtual void finalize()
Run after the main analysis - only in full rounds, so it always sees the complete abstract World.
Definition phase.h:268
auto targs(Projector auto f) const noexcept
Definition lam.h:252
const Def * callee() const
Definition lam.h:237
static std::tuple< const Axm *, u8, u8 > next(const Def *callee)
Like Axm::get, but advances the counter as one more App is about to be built on top of callee.
Definition axm.cpp:44
static auto isa(const Def *def)
Definition axm.h:112
Base class for all Defs.
Definition def.h:289
Def * set(size_t i, const Def *)
Successively set from left to right.
Definition def.cpp:188
T * as_mut() const
Asserts that this is a mutable, casts constness away and performs a static_cast to T.
Definition def.h:597
Defs deps() const noexcept
Definition def.cpp:486
nat_t num_tprojs() const
As above but yields 1, if an Arr/Pack exceeds Flags::scalarize_threshold.
Definition def.cpp:642
auto tprojs(Projector auto f) const
Definition def.h:469
bool in_scope(Def *mut) const
Is every free Var of this bound by mut or by a mutable that nests mut?
Definition def.cpp:399
const Def * tvar(nat_t i) noexcept
Definition def.h:492
T * isa_mut() const
If this is mutable, it will cast constness away and perform a dynamic_cast to T.
Definition def.h:588
static DefVec cat(Defs a, Defs b)
Definition tuple.cpp:138
DbgKey dbg_key() const
Cheap handle for other->set(this->dbg_key()).
Definition def.h:631
std::pair< D *, const Var * > isa_binder() const
Is this a mutable that introduces a Var?
Definition def.h:503
const Def * var(nat_t a, nat_t i) noexcept
Definition def.h:492
const Def * type() const noexcept
Yields the "raw" type of this Def (maybe nullptr).
Definition def.h:1119
bool nests(Def *mut)
Does this nest mut?
Definition def.cpp:382
nat_t num_tvars() noexcept
Definition def.h:492
auto tvars(Projector auto f) noexcept
Definition def.h:492
bool is_closed() const
Same as !has_free_vars().
Definition def.cpp:357
const Var * has_var()
Only returns not nullptr, if Var of this mutable has ever been created.
Definition def.h:496
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
const Def * filter() const
Definition lam.h:103
Lam * set(Filter filter, const Def *body)
Definition lam.cpp:34
const Def * tdom(nat_t i) const noexcept
Definition lam.h:117
static const Def * eta_reduce(const Def *def)
Fast-forwards def along its chain of η-redexes λ x = f x where x ∉ FV(f).
Definition lam.cpp:76
bool is_dependent() const
Does this type() itself or dom() has a Var?
Definition lam.h:116
const Def * body() const
Definition lam.h:104
static Lam * isa_rewritable(const Def *def)
Yields def as a mutable Lam, if it is_rewritable() and nullptr otherwise.
Definition lam.h:193
const Def * codom() const
Definition lam.h:114
static std::optional< T > isa(const Def *def)
Definition def.h:976
void invalidate(bool todo=true)
Signals that another round of fixed-point iteration is required, either as part of.
Definition phase.h:79
const fe::Log & log() const
Definition phase.h:60
void profile_count(std::string_view key, uint64_t n=1)
Adds n to the custom fe::Profiler counter key of the current run; no-op unless profiling is enabled.
Definition phase.cpp:60
bool todo() const
Definition phase.h:71
World & world()
Definition phase.h:58
Used as intermediate value during optimizatinos such as Analysis.
Definition def.h:1065
static const Proxy * isa(const Def *def)
Definition def.h:1089
virtual bool analyze()
Runs the optional pre-analysis on old_world(), typically to a fixed point, before rewriting begins.
Definition phase.cpp:202
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
const Def * abstracted(const Def *old_def) const
Returns lattice(old_def) if it differs from old_def (i.e. we learned something), otherwise nullptr.
Definition phase.h:350
bool is_top(const Def *old_def) const
Definition phase.h:355
World & world()=delete
Hides both and forbids direct access.
const Def * lattice(const Def *old_def) const
Returns the abstract value computed by the associated Analysis for the given old-world Def,...
Definition phase.h:347
World & old_world()
Get old Defs from here.
Definition phase.h:378
const Def * rewrite_imm_App(const App *) override
Definition phase.cpp:223
D * curr_mut() const
Definition rewrite.h:98
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_Var(const Var *)
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
Sieve(size_t num)
Definition tuple.h:123
static constexpr size_t Gone
Definition tuple.h:118
A variable introduced by a binder (mutable).
Definition def.h:857
static std::pair< const Var *, std::optional< nat_t > > isa_tproj(const Def *def)
Matches a Var v itself or an Extract v#j of it.
Definition def.cpp:223
Def * binder() const
The binder of this Var.
Definition def.h:867
The World represents the whole program and manages creation of MimIR nodes (Defs).
Definition world.h:43
const Def * app(const Def *callee, const Def *arg)
Definition world.cpp:416
const Def * bot(const Def *type)
Definition world.h:604
const Def * tuple(Defs ops)
Definition world.cpp:516
const Def * extract(const Def *d, const Def *i)
Definition world.cpp:598
const Proxy * proxy(const Def *type, Defs ops, u32 tag, bool vars=false, bool muts=false)
Definition world.h:357
const Def * raw_app(const Axm *axm, u8 curry, u8 trip, const Def *type, const Def *callee, const Def *arg)
Definition world.cpp:501
auto & vars()
Definition world.h:714
Lam * mut_lam(const Pi *pi)
Definition world.h:415
bool analyze() final
Runs the optional pre-analysis on old_world(), typically to a fixed point, before rewriting begins.
Definition seo.cpp:551
const Def * rewrite_imm_Var(const Var *) final
Definition seo.cpp:653
const Def * rewrite_imm_App(const App *) final
Definition seo.cpp:589
const Def * rewrite_mut_Lam(Lam *) final
Definition seo.cpp:645
@ bot
Alias for Mode::fast.
Definition math.h:38
static std::pair< const Proxy *, DefVec > sloxy_path(const Def *abstr_ptr)
The sloxy abstr_ptr points into, and the literal mem.lea indices on the way, outermost first.
Definition seo.cpp:207
static bool keep(Lam *lam, const Def *old_var, const Def *abstr)
Definition seo.cpp:474
static const Proxy * isa_bundle(const Def *def, Lam *lam)
Definition seo.cpp:109
static const Def * mk_phi(World &w, Lam *lam, const Def *sloxy)
Definition seo.cpp:183
static const Var * slot_var(const Def *ptr)
Definition seo.cpp:204
static size_t idx_of(Defs vars, const Def *p)
Definition seo.cpp:22
The mem Plugin
bool has_mem_ty(const Def *type)
Is type mem.M or a Sigma with a mem.M component?
Definition mem.h:54
const App * isa_mem(const Def *def)
If def is a mem.M-typed value, yields its memory type mem.M a; otherwise nullptr.
Definition mem.h:25
std::tuple< const Def *, Lam *, const Def *, const Def * > split_slot(const App *slot)
Decomposes the continuation-based slot into (mem, ret_lam, ret_mem, ptr), where ret_mem is the contin...
Definition mem.h:127
const Def * pointee(const Def *ptr)
Definition mem.h:81
const Def * op_slot(const Def *type, const Def *as, const Def *mem, const Def *ret)
Definition mem.h:119
The tuple Plugin
fe::View< const Def * > Defs
Definition def.h:96
fe::Vector< const Def * > DefVec
Definition def.h:98
fe::Patricia< const Var, DefKey >::Set Vars
Definition def.h:115
TExt< false > Bot
Definition union.h:39
@ Pi
Definition def.h:125