MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
lower_index.cpp
Go to the documentation of this file.
2
3#include <cstdint>
4#include <cstdlib>
5
6#include <algorithm>
7#include <array>
8#include <functional>
9#include <ranges>
10
11#include <mim/def.h>
12#include <mim/lam.h>
13#include <mim/tuple.h>
14
15#include <mim/plug/core/core.h>
16#include <mim/plug/mem/mem.h>
18
20
22
23namespace {
24
25/// Bound on the magnitude of a proven range: below it, sums and products of proven ranges cannot wrap an i64.
26constexpr int64_t Limit = int64_t{1} << 61;
27
28std::optional<int64_t> const_val(const Def* d) {
29 if (auto bc = Axm::isa<core::bitcast>(d)) d = bc->arg();
30 if (auto lit = Lit::isa(d); lit && *lit <= u64(Limit)) return int64_t(*lit);
31 return {};
32}
33
34/// Splits x * k into (x, k).
35std::optional<std::pair<const Def*, int64_t>> as_scaled(const Def* d) {
36 auto wrap = Axm::isa<core::wrap>(d);
37 if (!wrap || wrap.id() != core::wrap::mul) return {};
38 auto [a, b] = wrap->arg()->projs<2>();
39 if (auto k = const_val(b)) return std::pair{a, *k};
40 if (auto k = const_val(a)) return std::pair{b, *k};
41 return {};
42}
43
44/// True when @p d is a multiple of @p c for every value of its free indices.
45bool divides(const Def* d, int64_t c) {
46 if (c == 0) return false;
47 if (auto k = const_val(d)) return *k % c == 0;
48 if (auto s = as_scaled(d)) return s->second % c == 0 || divides(s->first, c);
49 auto wrap = Axm::isa<core::wrap>(d);
50 if (!wrap || (wrap.id() != core::wrap::add && wrap.id() != core::wrap::sub)) return false;
51 auto [a, b] = wrap->arg()->projs<2>();
52 return divides(a, c) && divides(b, c);
53}
54
55const Def* times(World& w, const Def* d, int64_t k) {
56 return k == 1 ? d : w.call(core::wrap::mul, core::Mode::none, Defs{d, w.lit_i64(u64(k))});
57}
58
59/// A linearized index scales whole sums by a stride, so the folds must not depend on how the sum was nested.
60void flatten_add(World& w, const Def* d, int64_t k, DefVec& terms) {
61 if (auto wrap = Axm::isa<core::wrap>(d); wrap && wrap.id() == core::wrap::add) {
62 auto [a, b] = wrap->arg()->projs<2>();
63 flatten_add(w, a, k, terms);
64 flatten_add(w, b, k, terms);
65 } else if (auto s = as_scaled(d); s && s->second != 0 && std::abs(k) <= Limit / std::abs(s->second)) {
66 flatten_add(w, s->first, k * s->second, terms);
67 } else {
68 terms.emplace_back(times(w, d, k));
69 }
70}
71
72/// @p d divided by @p c, for a @p d that divides established is an exact multiple.
73const Def* exact_div(World& w, const Def* d, int64_t c) {
74 if (c == 1) return d;
75 if (auto k = const_val(d)) return w.lit_i64(u64(*k / c));
76 if (auto s = as_scaled(d)) {
77 auto [x, k] = *s;
78 return k % c == 0 ? times(w, x, k / c) : times(w, exact_div(w, x, c), k);
79 }
80 auto wrap = Axm::isa<core::wrap>(d); // add or sub, both of whose sides divide
81 auto [a, b] = wrap->arg()->projs<2>();
82 return w.call(wrap.id(), core::Mode::none, Defs{exact_div(w, a, c), exact_div(w, b, c)});
83}
84
85const Def* sum(World& w, Defs terms) {
86 const Def* acc = nullptr;
87 for (auto t : terms)
88 acc = acc ? w.call(core::wrap::add, core::Mode::none, Defs{acc, t}) : t;
89 return acc ? acc : w.lit_i64(0);
90}
91
92/// Emits `core.div.<op> (mem, (x, c))` on the `Idx 0` carrier, advancing the threaded @p mem and yielding the value.
93const Def* emit_div(World& w, const Def*& mem, core::div op, const Def* x, const Def* c) {
94 auto [m, v] = w.call(op, Defs{mem, w.tuple({x, c})})->projs<2>();
95 mem = m;
96 return v;
97}
98
99} // namespace
100
101std::optional<LowerIndex::Range> LowerIndex::range_of(const Def* d) const {
102 auto bounded = [](int64_t lo, int64_t hi) -> std::optional<Range> {
103 if (lo < -Limit || hi > Limit) return {};
104 return Range{lo, hi};
105 };
106
107 if (auto k = const_val(d)) return Range{*k, *k};
108
109 if (auto ex = d->isa<Extract>()) {
110 auto i = Lit::isa(ex->index());
111 if (ex->tuple() == dom_.idxs && i && *i < dom_.extents.size())
112 if (auto n = dom_.extents[*i]; n && *n > 0) return Range{0, *n - 1};
113
114 if (auto div = Axm::isa<core::div>(ex->tuple()); div && i == 1) {
115 // `core.div` takes `(mem, (x, c))`.
116 auto [x, c] = div->arg(2, 1)->projs<2>();
117 auto k = const_val(c);
118 if (!k || *k <= 0) return {};
119 // `urem` is bounded by its divisor whatever its operand; `udiv` only by a non-negative operand's range.
120 if (div.id() == core::div::urem) return Range{0, *k - 1};
121 if (auto l = range_of(x); l && l->lo >= 0 && div.id() == core::div::udiv)
122 return Range{l->lo / *k, l->hi / *k};
123 }
124 }
125
126 if (auto wrap = Axm::isa<core::wrap>(d)) {
127 auto [a, b] = wrap->arg()->projs<2>();
128 auto l = range_of(a), r = range_of(b);
129 if (!l || !r) return {};
130 switch (wrap.id()) {
131 case core::wrap::add: return bounded(l->lo + r->lo, l->hi + r->hi);
132 case core::wrap::sub: return bounded(l->lo - r->hi, l->hi - r->lo);
133 case core::wrap::mul: {
134 std::array cands{
135 std::pair{l->lo, r->lo},
136 std::pair{l->lo, r->hi},
137 std::pair{l->hi, r->lo},
138 std::pair{l->hi, r->hi}
139 };
140 for (auto [x, y] : cands)
141 if (x != 0 && std::abs(y) > Limit / std::abs(x)) return {};
142 auto prods = cands | std::views::transform([](auto p) { return p.first * p.second; });
143 return bounded(std::ranges::min(prods), std::ranges::max(prods));
144 }
145 default: break;
146 }
147 }
148
149 return {};
150}
151
152LowerIndex::Split LowerIndex::split(const Def* x, int64_t c) {
153 DefVec terms;
154 flatten_add(new_world(), x, 1, terms);
155
156 Split split;
157 std::optional<Range> rest = Range{0, 0};
158 for (auto t : terms) {
159 if (divides(t, c)) {
160 split.even.emplace_back(t);
161 } else {
162 split.rest.emplace_back(t);
163 auto r = range_of(t);
164 if (rest && r && std::abs(rest->lo) <= Limit && std::abs(rest->hi) <= Limit)
165 rest = Range{rest->lo + r->lo, rest->hi + r->hi};
166 else
167 rest = std::nullopt;
168 }
169 }
170 split.carries = !rest || rest->lo < 0 || rest->hi >= c;
171 return split;
172}
173
174/// Splits x at the largest proper divisor k of c its addends do not carry across: x floordiv c = q floordiv (c/k).
175std::optional<LowerIndex::Peel> LowerIndex::peel(const Def* x, int64_t c) {
176 auto& w = new_world();
177 if (c <= 1) return {};
178 auto r = range_of(x);
179 if (!r || r->lo < 0) return {};
180
181 fe::Vector<int64_t> divisors;
182 for (int64_t d = 2; d * d <= c; ++d)
183 if (c % d == 0) divisors.emplace_back(d), divisors.emplace_back(c / d);
184 std::ranges::sort(divisors, std::greater{});
185
186 for (auto k : divisors) {
187 auto s = split(x, k);
188 if (s.even.empty() || s.carries) continue;
189 auto q = sum(w, DefVec(s.even.size(), [&](size_t i) { return exact_div(w, s.even[i], k); }));
190 return Peel{q, sum(w, s.rest), k};
191 }
192 return {};
193}
194
195/// x floordiv c without a division, or nullptr when the range analysis cannot justify one.
196const Def* LowerIndex::fold_udiv(const Def* x, int64_t c) {
197 auto& w = new_world();
198 if (c <= 0) return nullptr;
199 if (c == 1) return x;
200 if (auto k = const_val(x)) return w.lit_i64(u64(*k / c));
201
202 // The carrier wraps, so a fold needs all of x proven in range; an unknown term keeps the division.
203 auto r = range_of(x);
204 if (!r || r->lo < 0) return nullptr;
205 if (r->hi < c) return w.lit_i64(0);
206
207 auto s = split(x, c);
208 if (s.even.empty() || s.carries) return nullptr;
209 return sum(w, DefVec(s.even.size(), [&](size_t i) { return exact_div(w, s.even[i], c); }));
210}
211
212/// x mod c without a division, or nullptr when the range analysis cannot justify one.
213const Def* LowerIndex::fold_urem(const Def* x, int64_t c) {
214 auto& w = new_world();
215 if (c <= 0) return nullptr;
216 if (c == 1) return w.lit_i64(0);
217 if (auto k = const_val(x)) return w.lit_i64(u64(*k % c));
218
219 auto r = range_of(x);
220 if (!r || r->lo < 0) return nullptr;
221 if (r->hi < c) return x;
222 if (divides(x, c)) return w.lit_i64(0);
223
224 auto s = split(x, c);
225 return s.even.empty() || s.carries ? nullptr : sum(w, s.rest);
226}
227
228/// The carry-free low part of a peeled dividend stays out of the division.
229const Def* LowerIndex::udiv(const Def* x, const Def* c) {
230 auto& w = new_world();
231 if (auto k = const_val(c)) {
232 if (auto folded = fold_udiv(x, *k)) return folded;
233 if (auto p = peel(x, *k)) return udiv(p->q, w.lit_i64(u64(*k / p->k)));
234 }
235 return emit_div(w, mem_, core::div::udiv, x, c);
236}
237
238const Def* LowerIndex::urem(const Def* x, const Def* c) {
239 auto& w = new_world();
240 if (auto k = const_val(c)) {
241 if (auto folded = fold_urem(x, *k)) return folded;
242 if (auto p = peel(x, *k)) {
243 auto hi = urem(p->q, w.lit_i64(u64(*k / p->k)));
244 hi = w.call(core::wrap::mul, core::Mode::none, Defs{hi, w.lit_i64(u64(p->k))});
245 return w.call(core::wrap::add, core::Mode::none, Defs{hi, p->r});
246 }
247 }
248 return emit_div(w, mem_, core::div::urem, x, c);
249}
250
251const Def* LowerIndex::rewrite(const Def* def) {
252 // The opaque affine index type lowers to the wide `Idx 0` (i64) carrier.
253 if (Axm::isa<affine::Index>(def)) return new_world().type_i64();
254 return RWPhase::rewrite(def);
255}
256
258 auto& w = new_world();
259
260 // The affine index algebra is computed on the wide `Idx 0` carrier with wrap-around (`Mode::none`) arithmetic, so
261 // that negation/subtraction are correct via two's complement; the boundary `affine.map` casts in/out with
262 // `core.conv.u`.
263
264 // affine.lit n ↦ the `Nat` n reinterpreted as an `Idx 0`.
265 if (Axm::isa<affine::lit>(app)) return w.call<core::bitcast>(w.type_i64(), rewrite(app->arg()));
266
267 if (auto op = Axm::isa<affine::op>(app)) {
268 switch (op.id()) {
269 case affine::op::add: {
270 auto [a, b] = rewrite(app->arg())->projs<2>();
271 return w.call(core::wrap::add, core::Mode::none, Defs{a, b});
272 }
273 case affine::op::sub: {
274 auto [a, b] = rewrite(app->arg())->projs<2>();
275 return w.call(core::wrap::sub, core::Mode::none, Defs{a, b});
276 }
277 case affine::op::neg: {
278 auto a = rewrite(app->arg());
279 return w.call(core::wrap::sub, core::Mode::none, Defs{w.lit(w.type_i64(), 0), a});
280 }
281 case affine::op::mul: {
282 fe::throwf("`affine.op.mul` should have been rewritten to `affine.semiop.mul` and then to `core.mul`");
283 }
284 }
285 }
286
287 if (auto semiop = Axm::isa<affine::semiop>(app)) {
288 auto [x, c] = rewrite(app->arg())->projs<2>();
289 switch (semiop.id()) {
290 case affine::semiop::mul: {
292 app->blame("`affine.semiop.mul` called with non-constant second argument").bail();
293 // `c` is a `Nat` constant; reinterpret it on the `Idx 0` carrier.
294 return w.call(core::wrap::mul, core::Mode::none, Defs{x, w.call<core::bitcast>(w.type_i64(), c)});
295 }
297 return udiv(x, w.call<core::bitcast>(w.type_i64(), c));
298 }
299 case affine::semiop::rem: {
300 return urem(x, w.call<core::bitcast>(w.type_i64(), c));
301 }
303 auto c_idx = w.call<core::bitcast>(w.type_i64(), c);
304 // ceildiv(x, c) = (x + (c - 1)) / c (unsigned, on the Idx 0 carrier)
305 auto c_1 = w.call(core::wrap::sub, core::Mode::none, Defs{c_idx, w.lit(w.type_i64(), 1)});
306 return emit_div(w, mem_, core::div::udiv, w.call(core::wrap::add, core::Mode::none, Defs{x, c_1}),
307 c_idx);
308 }
309 }
310 }
311
312 // Row-major suffix-product strides of a shape `s` («n; Nat»): `strides#k = ∏_{j>k} s#j` (on `Idx 0`, via
313 // `core.nat`).
314 auto strides = [&](const Def* s, size_t n) {
315 DefVec str(n);
316 if (n == 0) return str;
317 str[n - 1] = w.lit_nat(1);
318 for (size_t k = n - 1; k-- != 0;)
319 str[k] = w.call(core::nat::mul, Defs{str[k + 1], s->proj(n, k + 1)});
320 for (size_t k = 0; k != n; ++k)
321 str[k] = w.call<core::bitcast>(w.type_i64(), str[k]);
322 return str;
323 };
324
325 // affine.linearize (idxs, s) ↦ Σ_k idxs#k · strides#k (on the `Idx 0` carrier).
327 auto [idxs, s] = rewrite(app->arg())->projs<2>();
328 auto xs = idxs->projs();
329 auto str = strides(s, xs.size());
330 const Def* lin = w.lit(w.type_i64(), 0);
331 for (size_t k = 0; k != xs.size(); ++k) {
332 auto term = w.call(core::wrap::mul, core::Mode::none, Defs{xs[k], str[k]});
333 lin = w.call(core::wrap::add, core::Mode::none, Defs{lin, term});
334 }
335 return lin;
336 }
337
338 // affine.delinearize (lin, s) ↦ (lin floordiv strides#d) mod s#d for each d (on the `Idx 0` carrier).
340 auto [lin, s] = rewrite(app->arg())->projs<2>();
341 auto m = s->num_projs();
342 auto str = strides(s, m);
343 return w.tuple(DefVec(
344 m, [&](size_t d) { return urem(udiv(lin, str[d]), w.call<core::bitcast>(w.type_i64(), s->proj(m, d))); }));
345 }
346
347 // affine.map f idxs mem ↦ widen idxs to `Idx 0`, inline f (advancing the threaded mem through any div), and narrow
348 // each result back to its target `Idx (sout#j)`; returns `(mem', narrowed)`.
349 if (Axm::isa<affine::map>(app)) {
350 // Extract f/idxs/sout from the *old* callee; we inline f's body at this call site rather than rewriting it into
351 // a standalone lam, since its body may reference the threaded mem (from the divs) and would otherwise be open.
352 auto [mn, sinout, f, idxs, _] = app->callee()->as<App>()->uncurry_args<5>();
353 auto [sin, sout] = sinout->projs<2>();
354
355 auto __ = fe::Restore(mem_);
356 auto mem = rewrite(app->arg()); // the `affine.map`'s mem operand
357 auto lifted
358 = w.tuple(rewrite(idxs)->projs([&](const Def* i) { return w.call(core::conv::u, w.lit_i64(), i); }));
359 auto f_lam = f->isa_mut<Lam>();
360 // An η-reduced map, e.g. `affine.op.add` itself, has no body to inline, so its η-expansion stands in for it.
361 if (!f_lam) {
362 auto& eta = etas_[f];
363 if (!eta) eta = Lam::eta_expand(f);
364 f_lam = eta;
365 }
366
367 Lam* idx_map_lam = nullptr;
368 Lam* rw_idx_lam = nullptr;
369 if (auto it = specialized_[f_lam].find(sin); it != specialized_[f_lam].end()) {
370 idx_map_lam = it->second;
371 } else if (auto idx_lam = lookup(f_lam)) {
372 // Anything already carrying a mem was rewritten for a *different* `sin` and gets its own specialization
373 // below; anything else was rewritten as part of an annex and needs the mem parameter added.
374 if (auto mut = idx_lam->isa_mut<Lam>();
375 !(mut && mut->num_vars() == 2 && mut->var(2, 0)->type() == mem->type()))
376 rw_idx_lam = idx_lam->as_mut<Lam>();
377 }
378 if (!idx_map_lam) {
379 auto lam_pi = rewrite(f_lam->type())->as<Pi>();
380
381 idx_map_lam = w.mut_lam(w.pi({mem->type(), lam_pi->dom()}, {mem->type(), lam_pi->codom()}));
382 specialized_[f_lam][sin] = idx_map_lam;
383 if (!lookup(f_lam)) map(f_lam, idx_map_lam);
384
385 push();
386 bind(f_lam->has_var(), idx_map_lam->has_var());
387 map(f_lam->var(), idx_map_lam->var(2, 1));
388 for (size_t i = 0; i != f_lam->num_vars(); ++i)
389 map(f_lam->var(f_lam->num_vars(), i), idx_map_lam->var(2, 1)->proj(f_lam->num_vars(), i));
390 mem_ = idx_map_lam->var(2, 0);
391
392 auto n = sin->num_projs();
393 auto extents = fe::Vector<std::optional<int64_t>>(n, [&](size_t i) -> std::optional<int64_t> {
394 if (auto e = Lit::isa(sin->proj(n, i)); e && *e <= u64(Limit)) return int64_t(*e);
395 return {};
396 });
397 auto restore_dom = fe::Restore(dom_, Domain{idx_map_lam->var(2, 1), std::move(extents)});
398
399 auto get_body = [&]() -> const Def* {
400 if (rw_idx_lam) return rw_idx_lam->reduce_body(idx_map_lam->var(2, 1));
401 return rewrite(f_lam->body());
402 };
403 idx_map_lam->set(true, w.tuple({mem_, get_body()}))->set(f_lam->dbg_key());
404 pop();
405 }
406
407 auto [out_mem, out_idxs] = w.app(idx_map_lam, {mem, lifted})->projs<2>();
408
409 auto sout_n = rewrite(sout);
410 auto n_out = sout_n->num_projs();
411 auto narrowed = w.tuple(DefVec(
412 n_out, [&](size_t j) { return w.call(core::conv::u, sout_n->proj(n_out, j), out_idxs->proj(n_out, j)); }));
413
414 return w.tuple({out_mem, narrowed});
415 }
416
417 return RWPhase::rewrite_imm_App(app);
418}
419
420} // namespace mim::plug::affine::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
const Def * proj(nat_t a, nat_t i) const
Similar to World::extract while assuming an arity of a, but also works on Sigmas and Arrays.
Definition def.cpp:647
T * as_mut() const
Asserts that this is a mutable, casts constness away and performs a static_cast to T.
Definition def.h:597
const Def * var(nat_t a, nat_t i) noexcept
Definition def.h:492
nat_t num_vars() noexcept
Definition def.h:492
auto projs(Projector auto f) const
Splits this Def via Def::projections into an Array (if A == std::dynamic_extent) or std::array (other...
Definition def.h:457
fe::Error & blame(fe::cite_string< Args... > s, Args &&... args) const
Reports an error that blames this; chain Error::n for Notes and Error::bail to throw.
Definition def.h:333
const Var * has_var()
Only returns not nullptr, if Var of this mutable has ever been created.
Definition def.h:496
A function.
Definition lam.h:91
static Lam * eta_expand(Filter, const Def *f)
Definition lam.cpp:84
const Def * reduce_body(const Def *arg) const
Definition lam.h:173
static std::optional< T > isa(const Def *def)
Definition def.h:976
A dependent function type.
Definition lam.h:14
Pi * set(const Def *dom, const Def *codom)
Definition lam.h:65
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 * rewrite_imm_App(const App *) override
Definition phase.cpp:223
void pop()
Definition rewrite.h:41
void bind(const Var *old_var, const Var *new_var)
Definition rewrite.h:46
const Def * map(const Def *old_def, const Def *new_def)
Definition rewrite.cpp:29
virtual const Def * rewrite(const Def *)
Definition rewrite.cpp:62
void push(Vars old_vars={}, Vars new_vars={})
Definition rewrite.h:40
const Def * type_i64()
Definition world.h:638
const Def * rewrite_imm_App(const App *) final
const Def * rewrite(const Def *) final
@ none
Wrap around.
Definition core.h:16
The mem Plugin
fe::View< const Def * > Defs
Definition def.h:96
fe::Vector< const Def * > DefVec
Definition def.h:98
uint64_t u64
Definition types.h:27