MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
scalarize.cpp
Go to the documentation of this file.
2
3#include "mim/lam.h"
4#include "mim/tuple.h"
5#include "mim/world.h"
6
7namespace mim {
8
9/// Number of params a single `u64` keep-bitmask can represent; wider doms fall back to the ⊤ sentinel.
10static constexpr auto BitmaskWidth = sizeof(u64) * 8;
11
12/// The only Pi%s we ever reshape: immutable (non-dependent) continuations.
13static const Pi* isa_flattenable(const Def* def) {
14 if (auto pi = Pi::isa_cn(def); pi && pi->isa_imm()) return pi;
15 return nullptr;
16}
17
18/*
19 * Analysis - optimistic: every immutable Cn is flattenable until proven otherwise.
20 */
21
22bool Scalarize::Analysis::kept(const Pi* pi, size_t dom) const {
23 auto abstr = lattice(pi);
24 if (!abstr) return false;
25 // Too wide for the u64 bitmask: keep() pins the whole Pi for dom >= 64, so a param this wide
26 // can only survive as a u64 entry if it was never recorded - conservatively treat it as kept.
27 if (dom >= BitmaskWidth) return true;
28 if (auto mask = Lit::isa<u64>(abstr)) return (*mask >> dom) & 1; // per-param bitmask
29 return true; // ⊤ sentinel: whole Pi pinned
30}
31
32void Scalarize::Analysis::keep(const Pi* pi, size_t dom) {
33 // Out of the thresholded arity: plan() only iterates [0, num_tdoms), so such a parameter is never a
34 // split candidate anyway - nothing to record.
35 if (dom >= pi->num_tdoms()) return;
36 auto cur = lattice(pi);
37 if (cur && !Lit::isa<u64>(cur)) return; // already ⊤ - monotone, do not downgrade
38 // Too wide for the u64 bitmask (only with an unusually large scalarize threshold): conservatively pin
39 // the whole Pi rather than risk splitting a dynamically-indexed parameter.
40 if (dom >= BitmaskWidth) {
41 pin(pi);
42 } else {
43 auto mask = cur ? Lit::as<u64>(cur) : u64(0);
44 auto next = mask | (u64(1) << dom);
45 if (next == mask) return;
46 lattice_force(pi, world().lit_nat(next));
47 DLOG("keep: {} #{}", pi, dom);
48 }
49}
50
51/// Collects @p def%'s immutable subtree into @p set; stops at mutables.
52static void collect(DefSet& set, const Def* def) {
53 if (!set.emplace(def).second) return;
54 if (def->isa_mut()) return;
55 for (auto d : def->deps())
56 collect(set, d);
57}
58
59void Scalarize::Analysis::pin_tree(const Def* def) {
60 auto visited = DefSet();
61 pin_tree(def, visited);
62}
63
64void Scalarize::Analysis::pin_tree(const Def* def, DefSet& visited) {
65 if (!visited.emplace(def).second) return;
66 if (auto pi = isa_flattenable(def)) pin(pi);
67 for (auto d : def->deps())
68 pin_tree(d, visited);
69}
70
71void Scalarize::Analysis::inspect(const Def* def) {
72 // Everything reachable from an annex is interface: normalizers and backends rely on its exact shape.
73 if (is_bootstrapping()) {
74 if (auto pi = isa_flattenable(def)) pin(pi);
75 return;
76 }
77
78 if (auto app = def->isa<App>()) {
79 // The shapes an Axm's signature *itself* dictates are interface - never reshape them
80 // (e.g. `%affine.For`'s body): normalizers and plugin phases match on their exact shape,
81 // and rebuilding the App re-derives them from the Axm's generic type.
82 // Subtrees merely *substituted* in via earlier (type) arguments impose no shape, though -
83 // they rewrite consistently with the rest of the World (e.g. `T` in `%mem.store T`):
84 // seed the walk's visited set with them so they stay flattenable.
85 if (app->uncurry_callee()->isa<Axm>()) {
86 if (auto pi = isa_flattenable(app->callee_type())) pin(pi); // this very application's shape
87 auto skips = DefSet();
88 for (auto d = def; auto a = d->isa<App>(); d = a->callee())
89 collect(skips, a->arg());
90 pin_tree(app->callee_type()->dom(), skips); // what the Axm consumes (e.g. `%affine.For`'s body)
91 pin_tree(app->type(), skips); // what the Axm produces (e.g. `%autodiff.ad f`)
92
93 // An Axm taking a *bare* function argument (`%autodiff.ad f`) is higher-order machinery that
94 // will inspect and call that function by its own convention - keep the function's shape even
95 // where the Axm's signature is fully polymorphic (`{T: *} → T → ...`).
96 // A function buried inside a tuple argument (`%mem.store (mem, ptr, f)`) is just data, though.
97 if (auto lam = app->arg()->isa_mut<Lam>()) pin_tree(lam->type());
98 }
99
100 // Assignability is alpha-equivalence, so an App may connect a dom and an arg whose types are
101 // *distinct* defs (e.g. one with a mutable ret-Pi, one immutable). Flattening decides per def,
102 // which would tear such an edge apart - pin both sides.
103 // Only for immutable Pis: a dependent dom legitimately differs from the instantiated arg type.
104 if (auto pi = app->callee_type(); pi->isa_imm() && pi->dom() != app->arg()->type()) {
105 pin_tree(pi->dom());
106 pin_tree(app->arg()->type());
107 }
108 }
109
110 if (def->isa<Var>()) return; // a Var references its mut as op; that is not a use of the Lam
111
112 // A value inside a *dependently*-typed aggregate - e.g. a typed closure `(T, code, env)` of type
113 // `[T: *, Cn [.., T, ..], T]` - has its type dictated by the Sigma's binder:
114 // reshaping it would tear the aggregate's typing apart (and clos machinery rebuilds against it).
115 if (auto tuple = def->isa<Tuple>(); tuple && tuple->type()->isa_mut())
116 for (auto op : tuple->ops())
117 pin_tree(op->type());
118
119 // An interface Lam (external, annex, or unset declaration) keeps its whole signature:
120 // pin everything its type mentions (its ret Pi, callback params, a polymorphic Lam's inner Cn, ...) -
121 // other code (plugin phases, foreign callers) builds against these shapes.
122 // Only the interface's own (top-level) Pi stays flattenable: it may be shared with internal values
123 // (Scalarize::rewrite_mut_Lam preserves the interface's top level by hand).
124 if (auto lam = def->isa_mut<Lam>(); lam && !isa_optimizable(lam)) {
125 auto visited = DefSet();
126 visited.emplace(lam->type());
127 for (auto d : lam->type()->deps())
128 pin_tree(d, visited);
129 // A curried interface (e.g. `fun extern f {s: Nat} (ab: ...)`) reduces to its inner Lam%s
130 // upon application; their types are the interface's *instantiated* codomains - distinct defs
131 // from the Pi-side codomains pinned above (Lam var vs Pi var) - so pin them whole, too.
132 if (lam->is_set())
133 for (auto inner = lam->body()->isa_mut<Lam>(); inner;) {
134 pin_tree(inner->type(), visited);
135 inner = inner->is_set() ? inner->body()->isa_mut<Lam>() : nullptr;
136 }
137 }
138
139 // As long as an interface Lam is merely called that is a local affair (see Scalarize::rewrite_imm_App),
140 // but escaping as a *value* it would meet flattened values of the same type - so pin the whole type.
141 for (size_t i = 0, e = def->num_ops(); i != e; ++i) {
142 auto op = def->op(i);
143 auto lam = op ? op->isa_mut<Lam>() : nullptr;
144 if (!lam || isa_optimizable(lam)) continue;
145 if (auto pi = isa_flattenable(lam->type()); pi && !(def->isa<App>() && i == 0)) pin(pi);
146 }
147
148 // A parameter that is Extract%ed / Insert%ed via a non-constant index must not be split.
149 const Def* idx_tuple = nullptr;
150 const Def* idx = nullptr;
151 if (auto ex = def->isa<Extract>())
152 idx_tuple = ex->tuple(), idx = ex->index();
153 else if (auto in = def->isa<Insert>())
154 idx_tuple = in->tuple(), idx = in->index();
155 if (!idx_tuple || Lit::isa(idx)) return;
156
157 if (auto var = idx_tuple->isa<Var>()) {
158 if (auto pi = isa_flattenable(var->binder()->type())) pin(pi); // whole var indexed dynamically
159 } else if (auto proj = idx_tuple->isa<Extract>()) {
160 if (auto var = proj->tuple()->isa<Var>()) {
161 if (auto pi = isa_flattenable(var->binder()->type())) {
162 if (auto i = Lit::isa(proj->index()))
163 keep(pi, *i);
164 else
165 pin(pi);
166 }
167 }
168 }
169}
170
171const Def* Scalarize::Analysis::rewrite(const Def* old) {
172 // Visit the subtree *before* inspecting: pin() seeds `pi ↦ pi` into the rewriter map (via pin),
173 // which would short-circuit the traversal into `old` and skip its subtree for the rest of the round -
174 // during bootstrapping this would hide nested Pis from the blanket annex pin.
175 auto res = mim::Analysis::rewrite(old);
176 inspect(old);
177 return res;
178}
179
180Vector<bool> Scalarize::Analysis::plan(const Def* type) const {
181 auto mask = Vector<bool>();
182 if (auto pi = isa_flattenable(type)) {
183 auto n = pi->num_tdoms();
184 auto any = false;
185 mask.assign(n, false);
186 for (size_t i = 0; i != n; ++i) {
187 // Only split immutable types: a mutable Sigma's element types may reference
188 // the Sigma's own var (e.g. a typed closure `[T: *, Cn [.., T, ..], T]`),
189 // which splitting would leave unbound.
190 auto t = pi->tdom(i);
191 if (!kept(pi, i) && t->isa_imm() && t->num_tprojs() > 1) mask[i] = any = true;
192 }
193 if (!any) mask.clear();
194 }
195 return mask;
196}
197
198/*
199 * Scalarize
200 */
201
203 auto mask = is_bootstrapping() ? Vector<bool>() : analysis_.plan(pi);
204 if (mask.empty()) return RWPhase::rewrite_imm_Pi(pi);
205
206 // build the flattened (one level) domain
207 auto doms = DefVec();
208 for (size_t i = 0, n = pi->num_tdoms(); i != n; ++i) {
209 auto t = rewrite(pi->tdom(i));
210 if (mask[i]) {
211 auto pieces = t->tprojs();
212 doms.insert(doms.end(), pieces.begin(), pieces.end());
213 } else {
214 doms.emplace_back(t);
215 }
216 }
217
218 auto& w = new_world();
219 auto sca = w.pi(w.sigma(doms), rewrite(pi->codom()), pi->is_implicit());
220 DLOG("scalarize {} ~> {}", pi, sca);
221 invalidate();
222 return sca;
223}
224
226 auto mask = is_bootstrapping() ? Vector<bool>() : analysis_.plan(old->type());
227 if (mask.empty()) return RWPhase::rewrite_mut_Lam(old);
228
229 if (!isa_optimizable(old)) {
230 // An interface Lam's signature is ABI: rebuild its Pi via the generic hook -
231 // top-level shape preserved, inner types still flattened - instead of rewrite(),
232 // which would hand back the flattened Pi.
233 auto pi = RWPhase::rewrite_imm_Pi(old->type())->as<Pi>();
234 return rewrite_stub(old, new_world().mut_lam(pi));
235 }
236
237 auto& w = new_world();
238 auto sca = w.mut_lam(rewrite(old->type())->as<Pi>())->set(old->dbg());
239 DLOG("scalarize {} : {} ~> {} : {}", old, old->type(), sca, sca->type());
240 map(old, sca);
241
242 // reassemble the old var one level from the fresh scalar vars
243 auto n = old->num_tvars();
244 auto params = DefVec();
245 params.reserve(n);
246 for (size_t i = 0, v = 0; i != n; ++i) {
247 auto t = rewrite(old->tvar(i)->type());
248 if (mask[i]) {
249 auto pieces = DefVec(t->num_tprojs(), [&](size_t) { return sca->var(v++); });
250 params.emplace_back(w.tuple(t, pieces));
251 } else {
252 params.emplace_back(sca->var(v++));
253 }
254 }
255 map(old->var(), w.tuple(params));
256
257 sca->set(rewrite(old->filter()), rewrite(old->body()));
258 return sca;
259}
260
261DefVec Scalarize::flatten_args(const App* app, const Vector<bool>& mask) {
262 auto args = DefVec();
263 for (size_t i = 0, n = app->num_targs(); i != n; ++i) {
264 auto arg = rewrite(app->targ(i));
265 if (i < mask.size() && mask[i]) {
266 auto pieces = arg->tprojs();
267 args.insert(args.end(), pieces.begin(), pieces.end());
268 } else {
269 args.emplace_back(arg);
270 }
271 }
272 return args;
273}
274
276 if (!is_bootstrapping()) {
277 // A direct call to an interface Lam keeps its argument shape - the callee's signature stays put.
278 auto lam = app->callee()->isa_mut<Lam>();
279 if (!lam || isa_optimizable(lam))
280 if (auto mask = analysis_.plan(app->callee_type()); !mask.empty())
281 return new_world().app(rewrite(app->callee()), flatten_args(app, mask));
282 }
283
284 return RWPhase::rewrite_imm_App(app);
285}
286
287} // namespace mim
const auto & lattice() const
The whole map; used e.g. to diff two fixed-point runs.
Definition phase.h:188
const Pi * callee_type() const
Definition lam.h:278
const Def * callee() const
Definition lam.h:276
Base class for all Defs.
Definition def.h:261
Defs deps() const noexcept
Definition def.cpp:514
const Def * tvar(nat_t i) noexcept
Definition def.h:441
T * isa_mut() const
If this is mutable, it will cast constness away and perform a dynamic_cast to T.
Definition def.h:527
const Def * var(nat_t a, nat_t i) noexcept
Definition def.h:441
const Def * type() const noexcept
Yields the "raw" type of this Def (maybe nullptr).
Definition def.cpp:491
nat_t num_tvars() noexcept
Definition def.h:441
Dbg dbg() const
Definition def.h:556
A function.
Definition lam.h:110
const Def * filter() const
Definition lam.h:122
const Pi * type() const
Definition lam.h:130
const Def * body() const
Definition lam.h:123
static std::optional< T > isa(const Def *def)
Definition def.h:878
static T as(const Def *def)
Definition def.h:884
void invalidate(bool todo=true)
Signals that another round of fixed-point iteration is required, either as part of.
Definition phase.h:98
const Vector< std::string > & args()
Command-line arguments passed to this Phase's plugin via -X <plugin>:<arg>.
Definition phase.cpp:23
A dependent function type.
Definition lam.h:14
static const Pi * isa_cn(const Def *d)
Is this a continuation - i.e. is the Pi::codom mim::Bottom?
Definition lam.h:47
bool is_implicit() const
Definition lam.h:26
const Def * codom() const
Definition lam.h:36
World & new_world()
Create new Defs into this.
Definition phase.h:368
bool is_bootstrapping() const
Returns whether we are currently bootstrapping (rewriting annexes).
Definition phase.h:356
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:333
virtual const Def * rewrite_stub(Def *, Def *)
Definition rewrite.cpp:267
virtual const Def * map(const Def *old_def, const Def *new_def)
Definition rewrite.h:45
virtual const Def * rewrite(const Def *)
Definition rewrite.cpp:56
const Def * rewrite_imm_Pi(const Pi *) final
const Def * rewrite_imm_App(const App *) final
const Def * rewrite_mut_Lam(Lam *) final
This is a thin wrapper for absl::InlinedVector<T, N, A> which is a drop-in replacement for std::vecto...
Definition vector.h:18
const Def * app(const Def *callee, const Def *arg)
Definition world.cpp:205
#define DLOG(...)
Vaporizes to nothingness in Debug build.
Definition log.h:94
Definition ast.h:14
Vector< const Def * > DefVec
Definition def.h:79
static void collect(DefSet &set, const Def *def)
Collects def's immutable subtree into set; stops at mutables.
Definition scalarize.cpp:52
Lam * isa_optimizable(Lam *lam)
These are Lams that are.
Definition lam.h:352
static const Pi * isa_flattenable(const Def *def)
The only Pis we ever reshape: immutable (non-dependent) continuations.
Definition scalarize.cpp:13
GIDSet< const Def * > DefSet
Definition def.h:76
static constexpr auto BitmaskWidth
Number of params a single u64 keep-bitmask can represent; wider doms fall back to the ⊤ sentinel.
Definition scalarize.cpp:10
uint64_t u64
Definition types.h:27
@ Pi
Definition def.h:109
@ Lam
Definition def.h:109
@ Var
Definition def.h:109
@ Axm
Definition def.h:109
@ Extract
Definition def.h:109
@ Insert
Definition def.h:109
@ App
Definition def.h:109
@ Tuple
Definition def.h:109
Vector(I, I, A=A()) -> Vector< typename std::iterator_traits< I >::value_type, Default_Inlined_Size< typename std::iterator_traits< I >::value_type >, A >