• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daisytuner / docc / 29818209498

21 Jul 2026 09:25AM UTC coverage: 63.21% (+0.03%) from 63.184%
29818209498

Pull #861

github

web-flow
Merge 4e275b4a4 into b57f184e7
Pull Request #861: Zero-fill to Memset Pass

159 of 235 new or added lines in 5 files covered. (67.66%)

4 existing lines in 2 files now uncovered.

40809 of 64561 relevant lines covered (63.21%)

984.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.19
/sdfg/src/analysis/memory_layout_analysis.cpp
1
#include "sdfg/analysis/memory_layout_analysis.h"
2

3
#include <algorithm>
4
#include <optional>
5
#include <set>
6
#include <unordered_set>
7

8
#include "sdfg/analysis/assumptions_analysis.h"
9
#include "sdfg/data_flow/access_node.h"
10
#include "sdfg/structured_control_flow/block.h"
11
#include "sdfg/structured_control_flow/if_else.h"
12
#include "sdfg/structured_control_flow/sequence.h"
13
#include "sdfg/structured_control_flow/structured_loop.h"
14
#include "sdfg/structured_control_flow/while.h"
15
#include "sdfg/symbolic/delinearization.h"
16
#include "sdfg/symbolic/extreme_values.h"
17
#include "sdfg/symbolic/polynomials.h"
18

19
namespace sdfg {
20
namespace analysis {
21

22
namespace {
23

24
// Sentinel symbol stored in shape[0] of a MemoryLayout when the leading dimension's
25
// extent is unknown (raw pointer accesses). The symbol never escapes the analysis:
26
// any expression that mentions it must be reported to the caller as `SymEngine::null`
27
// from the public size accessors (see `MemoryTile::extents()` etc.).
28
constexpr const char* kUnboundedName = "__unbounded__";
29

30
bool is_unbounded_dim(const symbolic::Expression& e) {
12,128✔
31
    if (e.is_null()) return false;
12,128✔
32
    if (!SymEngine::is_a<SymEngine::Symbol>(*e)) return false;
12,128✔
33
    return SymEngine::down_cast<const SymEngine::Symbol&>(*e).get_name() == kUnboundedName;
11,236✔
34
}
12,128✔
35

36
bool depends_on_unbounded(const symbolic::Expression& e) {
429✔
37
    if (e.is_null()) return false;
429✔
38
    for (const auto& a : symbolic::atoms(e)) {
429✔
39
        if (is_unbounded_dim(a)) return true;
232✔
40
    }
232✔
41
    return false;
429✔
42
}
429✔
43

44
bool layout_has_unbounded_first_dim(const MemoryLayout& layout) {
11,896✔
45
    const auto& shape = layout.shape();
11,896✔
46
    return !shape.empty() && is_unbounded_dim(shape[0]);
11,896✔
47
}
11,896✔
48

49
// Collect immediate child scopes (Sequence/IfElse/While/StructuredLoop) of a given
50
// scope that carry their own MemoryTile entries. Blocks are excluded because their
51
// per-memlet info is held in `accesses_`, not in `tiles_`/`tile_groups_`.
52
void collect_direct_child_scopes(
53
    structured_control_flow::ControlFlowNode& scope, std::set<const structured_control_flow::ControlFlowNode*>& result
54
) {
3,099✔
55
    if (auto* loop = dyn_cast<structured_control_flow::StructuredLoop*>(&scope)) {
3,099✔
56
        result.insert(&loop->root());
992✔
57
    } else if (auto* w = dyn_cast<structured_control_flow::While*>(&scope)) {
2,107✔
58
        result.insert(&w->root());
3✔
59
    } else if (auto* seq = dyn_cast<structured_control_flow::Sequence*>(&scope)) {
2,104✔
60
        for (size_t i = 0; i < seq->size(); i++) {
3,100✔
61
            auto& child = seq->at(i).first;
1,773✔
62
            if (!dyn_cast<structured_control_flow::Block*>(&child)) {
1,773✔
63
                result.insert(&child);
1,019✔
64
            }
1,019✔
65
        }
1,773✔
66
    } else if (auto* ife = dyn_cast<structured_control_flow::IfElse*>(&scope)) {
1,327✔
67
        for (size_t i = 0; i < ife->size(); i++) {
53✔
68
            result.insert(&ife->at(i).first);
30✔
69
        }
30✔
70
    }
23✔
71
}
3,099✔
72
} // namespace
73

74
MemoryLayoutAnalysis::MemoryLayoutAnalysis(StructuredSDFG& sdfg) : Analysis(sdfg) {}
301✔
75

76
void MemoryLayoutAnalysis::run(analysis::AnalysisManager& analysis_manager) {
301✔
77
    accesses_.clear();
301✔
78
    tiles_.clear();
301✔
79
    tile_groups_.clear();
301✔
80

81
    // Build a fresh branch-condition-aware assumptions analysis on every
82
    // run. The manager-cached `AssumptionsAnalysis` deliberately skips
83
    // IfElse-branch refinement; MLA's per-scope delinearization / bound
84
    // queries below need the refined coupled constraints (e.g. halo
85
    // guards).
86
    detailed_assumptions_ = std::make_unique<AssumptionsAnalysis>(sdfg_, /*with_branch_conditions=*/true);
301✔
87
    detailed_assumptions_->run(analysis_manager);
301✔
88

89
    traverse(sdfg_.root(), analysis_manager);
301✔
90
}
301✔
91

92
void MemoryLayoutAnalysis::
93
    traverse(structured_control_flow::ControlFlowNode& node, analysis::AnalysisManager& analysis_manager) {
3,099✔
94
    // Snapshot current memlets and tile keys before recursing into the scope's children
95
    std::vector<const data_flow::Memlet*> memlets_before;
3,099✔
96
    memlets_before.reserve(accesses_.size());
3,099✔
97
    for (const auto& entry : accesses_) {
4,068✔
98
        memlets_before.push_back(entry.first);
4,068✔
99
    }
4,068✔
100
    std::set<std::pair<const structured_control_flow::ControlFlowNode*, std::string>> tiles_before;
3,099✔
101
    for (const auto& entry : tiles_) {
9,621✔
102
        tiles_before.insert(entry.first);
9,621✔
103
    }
9,621✔
104

105
    if (auto block = dyn_cast<structured_control_flow::Block*>(&node)) {
3,099✔
106
        process_block(*block, analysis_manager);
754✔
107
    } else if (auto sequence = dyn_cast<structured_control_flow::Sequence*>(&node)) {
2,345✔
108
        for (size_t i = 0; i < sequence->size(); i++) {
3,100✔
109
            traverse(sequence->at(i).first, analysis_manager);
1,773✔
110
        }
1,773✔
111
    } else if (auto if_else = dyn_cast<structured_control_flow::IfElse*>(&node)) {
1,327✔
112
        for (size_t i = 0; i < if_else->size(); i++) {
53✔
113
            traverse(if_else->at(i).first, analysis_manager);
30✔
114
        }
30✔
115
    } else if (auto while_stmt = dyn_cast<structured_control_flow::While*>(&node)) {
995✔
116
        traverse(while_stmt->root(), analysis_manager);
3✔
117
    } else if (auto loop = dyn_cast<structured_control_flow::StructuredLoop*>(&node)) {
992✔
118
        traverse(loop->root(), analysis_manager);
992✔
119
    } else {
992✔
120
        // Break, Continue, Return nodes don't contain blocks
121
        return;
×
122
    }
×
123

124
    // Merge tiles for containers accessed within this scope
125
    merge_scope_layouts(node, memlets_before, tiles_before, analysis_manager);
3,099✔
126
}
3,099✔
127

128
void MemoryLayoutAnalysis::
129
    process_block(structured_control_flow::Block& block, analysis::AnalysisManager& analysis_manager) {
754✔
130
    auto& assumptions_analysis = *this->detailed_assumptions_;
754✔
131
    // Use trivial bounds (type-derived, e.g. unsigned >= 0) so delinearization
132
    // can soundly discharge non-negativity proof obligations on parameters.
133
    auto& assumptions = assumptions_analysis.get(block, /*include_trivial_bounds=*/true);
754✔
134

135
    auto& dfg = block.dataflow();
754✔
136
    for (auto& memlet : dfg.edges()) {
2,066✔
137
        const auto& subset = memlet.subset();
2,066✔
138
        if (subset.empty()) {
2,066✔
139
            continue;
604✔
140
        }
604✔
141

142
        // Get container name from the AccessNode (either src or dst)
143
        std::string container_name;
1,462✔
144
        if (auto* access = dynamic_cast<const data_flow::AccessNode*>(&memlet.src())) {
1,462✔
145
            container_name = access->data();
906✔
146
        } else if (auto* access = dynamic_cast<const data_flow::AccessNode*>(&memlet.dst())) {
906✔
147
            container_name = access->data();
556✔
148
        } else {
556✔
149
            continue; // Skip memlets without AccessNode
×
150
        }
×
151

152
        auto& base_type = memlet.base_type();
1,462✔
153
        switch (base_type.type_id()) {
1,462✔
154
            case types::TypeID::Scalar:
×
155
            case types::TypeID::Structure:
×
156
                continue; // Skip scalars and structures
×
157
            case types::TypeID::Tensor: {
×
158
                // Tensor types already contain layout information, so we can directly store it without delinearization
159
                auto& tensor_type = dynamic_cast<const types::Tensor&>(memlet.base_type());
×
160

161
                MemoryLayout layout(tensor_type.shape(), tensor_type.strides(), tensor_type.offset());
×
162
                MemoryAccess layout_info{container_name, subset, layout, true};
×
163
                this->accesses_.emplace(&memlet, layout_info);
×
164
                continue;
×
165
            }
×
166
            case types::TypeID::Array: {
395✔
167
                // Arrays are c-like stack arrays, so we can infer a simple row-major layout without needing
168
                // delinearization. Collect the extent of every (nested) array dimension so that
169
                // multi-dimensional arrays get a full shape instead of just their leading dimension.
170
                auto* array_type = dynamic_cast<const types::Array*>(&memlet.base_type());
395✔
171
                symbolic::MultiExpression shape;
395✔
172
                shape.push_back(array_type->num_elements());
395✔
173
                while (array_type->element_type().type_id() == types::TypeID::Array) {
411✔
174
                    array_type = dynamic_cast<const types::Array*>(&array_type->element_type());
16✔
175
                    shape.push_back(array_type->num_elements());
16✔
176
                }
16✔
177
                if (array_type->element_type().type_id() != types::TypeID::Scalar) {
395✔
178
                    continue; // Skip non-scalar arrays
×
179
                }
×
180
                if (subset.size() != shape.size()) {
395✔
NEW
181
                    continue; // Require one index per (native) array dimension
×
NEW
182
                }
×
183

184
                MemoryLayout layout(shape);
395✔
185
                MemoryAccess layout_info{container_name, subset, layout, true};
395✔
186
                this->accesses_.emplace(&memlet, layout_info);
395✔
187
                continue;
395✔
188
            }
395✔
189
            case types::TypeID::Pointer: {
1,067✔
190
                // For pointers, we attempt to delinearize the access pattern to infer the layout based
191
                // on assumptions from loop bounds
192
                auto* pointer_type = dynamic_cast<const types::Pointer*>(&memlet.base_type());
1,067✔
193

194
                // Typed pointer to a (possibly multi-dim) fixed array of scalar,
195
                // e.g. `float (*A)[M]`. The pointer adds one unbounded leading
196
                // dimension; remaining dimensions come from the array shape. The
197
                // subset is expected to be one index per dimension — no
198
                // delinearization needed.
199
                if (pointer_type->pointee_type().type_id() == types::TypeID::Array) {
1,067✔
200
                    auto* array_type = dynamic_cast<const types::Array*>(&pointer_type->pointee_type());
544✔
201
                    symbolic::MultiExpression array_shape = {array_type->num_elements()};
544✔
202
                    while (array_type->element_type().type_id() == types::TypeID::Array) {
570✔
203
                        array_type = dynamic_cast<const types::Array*>(&array_type->element_type());
26✔
204
                        array_shape.push_back(array_type->num_elements());
26✔
205
                    }
26✔
206
                    if (array_type->element_type().type_id() != types::TypeID::Scalar) {
544✔
207
                        continue; // Skip non-scalar leaf
×
208
                    }
×
209
                    if (subset.size() != array_shape.size() + 1) {
544✔
210
                        continue; // Require one index per dimension (leading pointer + array dims)
×
211
                    }
×
212

213
                    symbolic::MultiExpression shape;
544✔
214
                    shape.push_back(symbolic::symbol("__unbounded__"));
544✔
215
                    for (const auto& dim : array_shape) {
570✔
216
                        shape.push_back(dim);
570✔
217
                    }
570✔
218

219
                    MemoryLayout layout(shape);
544✔
220
                    MemoryAccess layout_info{container_name, subset, layout, false};
544✔
221
                    this->accesses_.emplace(&memlet, layout_info);
544✔
222
                    continue;
544✔
223
                }
544✔
224

225
                if (pointer_type->pointee_type().type_id() != types::TypeID::Scalar) {
523✔
226
                    continue; // Skip non-scalar pointers
1✔
227
                }
1✔
228

229
                if (subset.size() != 1) {
522✔
230
                    continue; // Require full linearization
×
231
                }
×
232
                auto& linearized_expr = subset.at(0);
522✔
233

234
                auto result = symbolic::delinearize(linearized_expr, assumptions);
522✔
235
                if (!result.success) {
522✔
236
                    continue; // Delinearization failed, skip
15✔
237
                }
15✔
238

239
                // Delinearization returns N indices but only N-1 dimensions (from stride division)
240
                // The first dimension is unbounded - insert a placeholder that will be filled in by merge
241
                // Using a special symbol as placeholder for the first dimension
242
                symbolic::MultiExpression shape;
507✔
243
                shape.push_back(symbolic::symbol("__unbounded__"));
507✔
244
                for (const auto& dim : result.dimensions) {
507✔
245
                    shape.push_back(dim);
346✔
246
                }
346✔
247

248
                // Store symbolic indices and dimensions with unbounded first dimension
249
                // The merge phase will attempt to bound the first dimension using loop assumptions
250
                MemoryLayout layout(shape);
507✔
251
                MemoryAccess layout_info{container_name, result.indices, layout, false};
507✔
252
                this->accesses_.emplace(&memlet, layout_info);
507✔
253
                continue;
507✔
254
            }
522✔
255
            default:
×
256
                continue; // Skip unsupported types
×
257
        }
1,462✔
258
    }
1,462✔
259
}
754✔
260

261
const MemoryAccess* MemoryLayoutAnalysis::access(const data_flow::Memlet& memlet) const {
4,318✔
262
    auto layout_it = accesses_.find(&memlet);
4,318✔
263
    if (layout_it == accesses_.end()) {
4,318✔
264
        return nullptr;
2✔
265
    }
2✔
266
    return &layout_it->second;
4,316✔
267
}
4,318✔
268

269
void MemoryLayoutAnalysis::merge_scope_layouts(
270
    structured_control_flow::ControlFlowNode& scope,
271
    const std::vector<const data_flow::Memlet*>& memlets_before,
272
    const std::set<std::pair<const structured_control_flow::ControlFlowNode*, std::string>>& tiles_before,
273
    analysis::AnalysisManager& analysis_manager
274
) {
3,099✔
275
    // Convert memlets_before to a set for O(1) lookup
276
    std::unordered_set<const data_flow::Memlet*> before_set(memlets_before.begin(), memlets_before.end());
3,099✔
277

278
    // Group all new accesses by container
279
    std::unordered_map<std::string, std::vector<const data_flow::Memlet*>> all_container_groups;
3,099✔
280
    for (auto& [memlet_ptr, acc] : accesses_) {
14,764✔
281
        if (before_set.find(memlet_ptr) != before_set.end()) {
14,764✔
282
            continue;
4,068✔
283
        }
4,068✔
284
        all_container_groups[acc.container].push_back(memlet_ptr);
10,696✔
285
    }
10,696✔
286

287
    // Sort memlets within each container group by element_id for deterministic processing order
288
    for (auto& [container, memlets] : all_container_groups) {
5,842✔
289
        std::sort(memlets.begin(), memlets.end(), [](const data_flow::Memlet* a, const data_flow::Memlet* b) {
10,065✔
290
            return a->element_id() < b->element_id();
10,065✔
291
        });
10,065✔
292
    }
5,842✔
293

294
    auto* loop = dyn_cast<structured_control_flow::StructuredLoop*>(&scope);
3,099✔
295

296
    auto& assumptions_analysis = *this->detailed_assumptions_;
3,099✔
297
    // For loops, query at the loop body so the induction variable's bounds are visible.
298
    auto& assumption_node = loop ? static_cast<structured_control_flow::ControlFlowNode&>(loop->root()) : scope;
3,099✔
299
    // Trivial-bounds view: includes type-derived defaults (e.g. Int32 ∈ [INT_MIN, INT_MAX]).
300
    // Used as the assumption set passed to symbolic::minimum/maximum so that the
301
    // resolver has sign information for parameters.
302
    auto& assumptions = assumptions_analysis.get(assumption_node, /*include_trivial_bounds=*/true);
3,099✔
303
    // Narrowing-only view: excludes type-derived defaults. A symbol that only
304
    // appears here (or in neither) has at most its type's intrinsic range — any
305
    // min/max resolution would collapse to INT_MIN/INT_MAX-style numerics, which
306
    // is not a sound tile bound. We use this to decide whether to emit a tile.
307
    auto& narrowing_assumptions = assumptions_analysis.get(assumption_node, /*include_trivial_bounds=*/false);
3,099✔
308
    // Parameters of a scope are constant symbols that the min/max resolver
309
    // should treat as opaque (BoundAnalysis short-circuits them to
310
    // `Interval::exact(sym)`). SDFG-level read-only arguments are constant by
311
    // construction; for each scope-local entry, the `constant()` flag tells us
312
    // whether the symbol is invariant within the scope.
313
    //
314
    // Loop induction variables are marked `constant()` by AssumptionsAnalysis
315
    // (they're frozen within one iteration). The decision of *which* indvars
316
    // to keep bounded vs. opaque at the current scope governs when each
317
    // ancestor loop's range is unfolded into the tile, and crucially when
318
    // branch-refined bounds (set by AssumptionsAnalysis on IfElse case
319
    // sequences) become visible:
320
    //
321
    //   * Loop body (`scope == loop->root()`): exclude only the directly
322
    //     surrounding loop's indvar. Outer ancestor indvars stay opaque so
323
    //     they are unfolded one level at a time by their own loop's
324
    //     `merge_scope_layouts` — this is what lets Linearized_2D-style
325
    //     tiles propagate as `[i, 0]..[i, M-1]` through the inner loop and
326
    //     get the proper outer-loop range only at the outer-loop merge.
327
    //
328
    //   * Other scopes (IfElse case body, plain Sequence): exclude *all*
329
    //     enclosing loops' indvars. At these scopes, branch refinements may
330
    //     have tightened ancestor indvars' AA bounds (e.g. `i ∈ [1, 8]`
331
    //     inside a guarded case), and those refinements only live on this
332
    //     scope's AssumptionsAnalysis entry — once a tile bubbles up past
333
    //     the IfElse, the refined bounds are no longer reachable. By
334
    //     unfolding ancestor indvars here, the tile gets baked with the
335
    //     tightest visible facts before merging upward.
336
    symbolic::SymbolSet excluded_indvars;
3,099✔
337
    const bool scope_is_loop_body = !loop && [&]() {
3,099✔
338
        auto* parent = scope.get_parent();
2,107✔
339
        auto* parent_loop = dyn_cast<structured_control_flow::StructuredLoop*>(parent);
2,107✔
340
        return parent_loop && &parent_loop->root() == &scope;
2,107✔
341
    }();
2,107✔
342
    if (loop) {
3,099✔
343
        excluded_indvars.insert(loop->indvar());
992✔
344
    } else if (scope_is_loop_body) {
2,107✔
345
        auto* parent_loop = dyn_cast<structured_control_flow::StructuredLoop*>(scope.get_parent());
992✔
346
        excluded_indvars.insert(parent_loop->indvar());
992✔
347
    } else {
1,115✔
348
        for (auto* cur = scope.get_parent(); cur; cur = cur->get_parent()) {
6,035✔
349
            if (auto* l = dyn_cast<structured_control_flow::StructuredLoop*>(cur)) {
4,920✔
350
                excluded_indvars.insert(l->indvar());
1,998✔
351
            }
1,998✔
352
        }
4,920✔
353
    }
1,115✔
354
    symbolic::SymbolSet parameters = assumptions_analysis.parameters();
3,099✔
355
    for (auto& entry : assumptions) {
25,880✔
356
        if (excluded_indvars.contains(entry.first)) {
25,880✔
357
            continue; // unfolded via AA bounds, not treated as opaque
3,980✔
358
        }
3,980✔
359
        if (entry.second.constant()) {
21,900✔
360
            parameters.insert(entry.first);
6,485✔
361
        }
6,485✔
362
    }
21,900✔
363

364
    // Soundness check: every free (non-parameter) symbol in an index expression
365
    // must have a narrowing assumption at this scope. Otherwise symbolic::minimum/
366
    // maximum would fall back to the symbol's type-default range and produce
367
    // bogus tile bounds (e.g. INT_MAX) that the rest of the pipeline would
368
    // silently consume as truth.
369
    auto has_narrowing = [&](const symbolic::Symbol& sym) -> bool {
3,099✔
370
        auto it = narrowing_assumptions.find(sym);
156✔
371
        if (it == narrowing_assumptions.end()) return false;
156✔
372
        return !it->second.lower_bounds().empty() || !it->second.upper_bounds().empty();
4✔
373
    };
156✔
374
    auto bounds_are_sound = [&](const symbolic::Expression& expr) -> bool {
18,725✔
375
        for (const auto& sym : symbolic::atoms(expr)) {
18,725✔
376
            if (parameters.contains(sym)) continue;
15,379✔
377
            if (excluded_indvars.contains(sym)) continue;
5,605✔
378
            if (!has_narrowing(sym)) return false;
156✔
379
        }
156✔
380
        return true;
18,569✔
381
    };
18,725✔
382

383
    // Per-scope BoundAnalysis pair (tight + loose-fallback). Both instances
384
    // memoize their results, so repeated expressions and shared subterms
385
    // across all per-element lower/upper queries below hit the cache.
386
    symbolic::BoundAnalysis ba_tight(parameters, assumptions, true);
3,099✔
387
    symbolic::BoundAnalysis ba_loose(parameters, assumptions, false);
3,099✔
388
    auto bound_lb = [&](const symbolic::Expression& e) -> symbolic::Expression {
27,062✔
389
        auto r = ba_tight.lower_bound(e);
27,062✔
390
        if (r.is_null()) r = ba_loose.lower_bound(e);
27,062✔
391
        return r;
27,062✔
392
    };
27,062✔
393
    auto bound_ub = [&](const symbolic::Expression& e) -> symbolic::Expression {
19,295✔
394
        auto r = ba_tight.upper_bound(e);
19,295✔
395
        if (r.is_null()) r = ba_loose.upper_bound(e);
19,295✔
396
        return r;
19,295✔
397
    };
19,295✔
398

399
    // Find direct child scopes that may carry tiles for this scope
400
    std::set<const structured_control_flow::ControlFlowNode*> direct_child_scopes;
3,099✔
401
    collect_direct_child_scopes(scope, direct_child_scopes);
3,099✔
402

403
    for (auto& [container, memlets] : all_container_groups) {
5,842✔
404
        if (memlets.empty()) continue;
5,842✔
405

406
        std::vector<const MemoryTile*> inner_tiles;
5,842✔
407
        for (const auto* child : direct_child_scopes) {
5,842✔
408
            auto it = tiles_.find({child, container});
4,504✔
409
            if (it != tiles_.end()) {
4,504✔
410
                inner_tiles.push_back(&it->second);
4,057✔
411
            }
4,057✔
412
        }
4,504✔
413

414
        size_t ndims = 0;
5,842✔
415
        MemoryLayout reference_layout({symbolic::one()});
5,842✔
416
        // Separate min/max index lists to avoid unnecessary symbolic min/max
417
        std::vector<std::vector<symbolic::Expression>> min_indices;
5,842✔
418
        std::vector<std::vector<symbolic::Expression>> max_indices;
5,842✔
419

420
        if (!inner_tiles.empty()) {
5,842✔
421
            // Use inner tile min/max as representative values
422
            // Inner tiles have already resolved inner loop variables to their bounds
423
            ndims = inner_tiles[0]->min_subset.size();
3,859✔
424
            reference_layout = inner_tiles[0]->layout;
3,859✔
425
            min_indices.resize(ndims);
3,859✔
426
            max_indices.resize(ndims);
3,859✔
427

428
            for (const auto* tile : inner_tiles) {
4,057✔
429
                if (tile->min_subset.size() != ndims) continue;
4,057✔
430
                for (size_t d = 0; d < ndims; ++d) {
11,096✔
431
                    min_indices[d].push_back(tile->min_subset[d]);
7,039✔
432
                    max_indices[d].push_back(tile->max_subset[d]);
7,039✔
433
                }
7,039✔
434
            }
4,057✔
435

436
            // Propagate tile groups from child scopes upward using the same
437
            // base-partitioning logic: group inner groups by their min_subset
438
            // base at this scope level, then merge each partition.
439
            // Same direct-lookup optimization as above for inner_tiles.
440
            std::vector<const MemoryTileGroup*> inner_groups;
3,859✔
441
            for (const auto* child : direct_child_scopes) {
4,367✔
442
                auto it = tile_groups_.find({child, container});
4,367✔
443
                if (it == tile_groups_.end()) continue;
4,367✔
444
                for (const auto& g : it->second) {
4,449✔
445
                    inner_groups.push_back(&g);
4,449✔
446
                }
4,449✔
447
            }
4,057✔
448

449
            if (!inner_groups.empty()) {
3,859✔
450
                // Group inner groups by their base at this level
451
                struct OuterGroupEntry {
3,859✔
452
                    data_flow::Subset base;
3,859✔
453
                    std::vector<const MemoryTileGroup*> constituents;
3,859✔
454
                };
3,859✔
455
                std::vector<OuterGroupEntry> outer_partitions;
3,859✔
456

457
                for (const auto* ig : inner_groups) {
4,449✔
458
                    if (ig->tile.min_subset.size() != ndims) continue;
4,449✔
459

460
                    // Compute base: minimum of the inner group's min_subset per dim
461
                    data_flow::Subset base;
4,449✔
462
                    bool base_ok = true;
4,449✔
463
                    for (size_t d = 0; d < ndims; ++d) {
12,210✔
464
                        auto lb = bound_lb(ig->tile.min_subset[d]);
7,761✔
465
                        if (lb.is_null()) {
7,761✔
466
                            base_ok = false;
×
467
                            break;
×
468
                        }
×
469
                        base.push_back(symbolic::simplify(lb));
7,761✔
470
                    }
7,761✔
471
                    if (!base_ok) continue;
4,449✔
472

473
                    // Find matching partition (same base OR constant-offset base)
474
                    bool found = false;
4,449✔
475
                    for (auto& op : outer_partitions) {
4,449✔
476
                        bool const_diff = true;
732✔
477
                        for (size_t d = 0; d < ndims; ++d) {
1,297✔
478
                            auto diff = symbolic::simplify(symbolic::sub(base[d], op.base[d]));
1,015✔
479
                            if (!SymEngine::is_a<SymEngine::Integer>(*diff)) {
1,015✔
480
                                const_diff = false;
450✔
481
                                break;
450✔
482
                            }
450✔
483
                        }
1,015✔
484
                        if (const_diff) {
732✔
485
                            op.constituents.push_back(ig);
282✔
486
                            found = true;
282✔
487
                            break;
282✔
488
                        }
282✔
489
                    }
732✔
490
                    if (!found) {
4,449✔
491
                        outer_partitions.push_back({base, {ig}});
4,167✔
492
                    }
4,167✔
493
                }
4,449✔
494

495
                // For each partition, merge constituent tile bounds and collect memlets
496
                std::vector<MemoryTileGroup> result_groups;
3,859✔
497
                for (auto& op : outer_partitions) {
4,167✔
498
                    data_flow::Subset grp_min, grp_max;
4,167✔
499
                    bool grp_bounded = true;
4,167✔
500

501
                    for (size_t d = 0; d < ndims; ++d) {
11,427✔
502
                        symbolic::Expression d_min = SymEngine::null;
7,267✔
503
                        symbolic::Expression d_max = SymEngine::null;
7,267✔
504
                        for (const auto* c : op.constituents) {
7,761✔
505
                            auto lb = bound_lb(c->tile.min_subset[d]);
7,761✔
506
                            if (lb.is_null()) {
7,761✔
507
                                grp_bounded = false;
×
508
                                break;
×
509
                            }
×
510
                            d_min = d_min.is_null() ? lb : symbolic::min(d_min, lb);
7,761✔
511

512
                            auto ub = bound_ub(c->tile.max_subset[d]);
7,761✔
513
                            if (ub.is_null()) {
7,761✔
514
                                grp_bounded = false;
7✔
515
                                break;
7✔
516
                            }
7✔
517
                            d_max = d_max.is_null() ? ub : symbolic::max(d_max, ub);
7,754✔
518
                        }
7,754✔
519
                        if (!grp_bounded) break;
7,267✔
520
                        grp_min.push_back(symbolic::simplify(d_min));
7,260✔
521
                        grp_max.push_back(symbolic::simplify(d_max));
7,260✔
522
                    }
7,260✔
523
                    if (!grp_bounded) continue;
4,167✔
524

525
                    // Collect all memlets from constituent groups
526
                    std::vector<const data_flow::Memlet*> grp_memlets;
4,160✔
527
                    for (const auto* c : op.constituents) {
4,442✔
528
                        grp_memlets.insert(grp_memlets.end(), c->memlets.begin(), c->memlets.end());
4,442✔
529
                    }
4,442✔
530

531
                    MemoryTile grp_tile{
4,160✔
532
                        container, grp_min, grp_max, reference_layout, !layout_has_unbounded_first_dim(reference_layout)
4,160✔
533
                    };
4,160✔
534
                    result_groups.push_back({grp_tile, std::move(grp_memlets)});
4,160✔
535
                }
4,160✔
536

537
                if (!result_groups.empty()) {
3,859✔
538
                    tile_groups_.insert({{&scope, container}, std::move(result_groups)});
3,854✔
539
                }
3,854✔
540
            }
3,859✔
541
        } else {
3,859✔
542
            // Use raw access indices (no inner tiles available)
543
            auto& first_access = accesses_.at(memlets[0]);
1,983✔
544
            auto& reference_shape = first_access.layout.shape();
1,983✔
545
            ndims = reference_shape.size();
1,983✔
546
            reference_layout = first_access.layout;
1,983✔
547
            min_indices.resize(ndims);
1,983✔
548
            max_indices.resize(ndims);
1,983✔
549

550
            bool consistent = true;
1,983✔
551
            for (const auto* memlet_ptr : memlets) {
2,947✔
552
                auto& acc = accesses_.at(memlet_ptr);
2,947✔
553
                auto& shape = acc.layout.shape();
2,947✔
554

555
                if (shape.size() != ndims) {
2,947✔
556
                    consistent = false;
×
557
                    break;
×
558
                }
×
559
                // Check inner dimensions match (all except first which may be unbounded)
560
                for (size_t d = 1; d < ndims; ++d) {
4,853✔
561
                    if (!symbolic::eq(shape[d], reference_shape[d])) {
1,906✔
562
                        consistent = false;
×
563
                        break;
×
564
                    }
×
565
                }
1,906✔
566
                if (!consistent) break;
2,947✔
567

568
                // Collect indices for each dimension. In the raw-access path,
569
                // min and max indices are identical (both are `acc.subset[d]`),
570
                // so we only populate `min_indices[d]` and leave `max_indices[d]`
571
                // empty. The shared bound-resolution loop below detects this
572
                // alias case and fuses the two passes into one walk, halving
573
                // the soundness-check and outer-loop overhead.
574
                if (acc.subset.size() != ndims) {
2,947✔
UNCOV
575
                    consistent = false;
×
UNCOV
576
                    break;
×
UNCOV
577
                }
×
578
                for (size_t d = 0; d < ndims; ++d) {
7,800✔
579
                    min_indices[d].push_back(acc.subset[d]);
4,853✔
580
                }
4,853✔
581
            }
2,947✔
582

583
            if (!consistent) continue;
1,983✔
584

585
            // Compute tile groups for raw memlets
586
            compute_tile_groups(scope, container, memlets, reference_layout, ndims, ba_tight, ba_loose);
1,983✔
587
        }
1,983✔
588

589
        if (ndims == 0) continue;
5,842✔
590

591
        // Bound each candidate index individually via the reused BoundAnalysis
592
        // (memoized across all per-dim queries below) and combine the per-index
593
        // bounds into the dim's min/max via symbolic::min / symbolic::max.
594
        //
595
        // In the raw-access path, `max_indices[d]` is left empty as a signal
596
        // that it would otherwise alias `min_indices[d]`. When detected, we
597
        // fuse the two passes into a single walk so the soundness check and
598
        // outer-loop bookkeeping run once per index instead of twice.
599
        data_flow::Subset min_subset;
5,842✔
600
        data_flow::Subset max_subset;
5,842✔
601
        bool all_bounded = true;
5,842✔
602

603
        for (size_t d = 0; d < ndims; ++d) {
15,544✔
604
            symbolic::Expression dim_min = SymEngine::null;
9,865✔
605
            symbolic::Expression dim_max = SymEngine::null;
9,865✔
606
            const bool fused = max_indices[d].empty();
9,865✔
607

608
            for (const auto& idx : min_indices[d]) {
11,690✔
609
                if (!bounds_are_sound(idx)) {
11,690✔
610
                    all_bounded = false;
150✔
611
                    break;
150✔
612
                }
150✔
613
                auto lb = bound_lb(idx);
11,540✔
614
                if (lb.is_null()) {
11,540✔
615
                    all_bounded = false;
×
616
                    break;
×
617
                }
×
618
                dim_min = dim_min.is_null() ? lb : symbolic::min(dim_min, lb);
11,540✔
619

620
                if (fused) {
11,540✔
621
                    // Soundness already checked above for the same idx.
622
                    auto ub = bound_ub(idx);
4,505✔
623
                    if (ub.is_null()) {
4,505✔
624
                        all_bounded = false;
6✔
625
                        break;
6✔
626
                    }
6✔
627
                    dim_max = dim_max.is_null() ? ub : symbolic::max(dim_max, ub);
4,499✔
628
                }
4,499✔
629
            }
11,540✔
630
            if (!all_bounded) break;
9,865✔
631

632
            if (!fused) {
9,709✔
633
                for (const auto& idx : max_indices[d]) {
7,035✔
634
                    if (!bounds_are_sound(idx)) {
7,035✔
635
                        all_bounded = false;
6✔
636
                        break;
6✔
637
                    }
6✔
638
                    auto ub = bound_ub(idx);
7,029✔
639
                    if (ub.is_null()) {
7,029✔
640
                        all_bounded = false;
1✔
641
                        break;
1✔
642
                    }
1✔
643
                    dim_max = dim_max.is_null() ? ub : symbolic::max(dim_max, ub);
7,028✔
644
                }
7,028✔
645
                if (!all_bounded) break;
6,709✔
646
            }
6,709✔
647

648
            min_subset.push_back(symbolic::simplify(dim_min));
9,702✔
649
            max_subset.push_back(symbolic::simplify(dim_max));
9,702✔
650
        }
9,702✔
651

652
        if (!all_bounded) continue;
5,842✔
653

654
        // Store this scope's tile with the original memory layout. `first_dim_bounded`
655
        // mirrors the underlying layout: false whenever shape[0] is the unbounded sentinel.
656
        MemoryTile merged_tile{
5,679✔
657
            container, min_subset, max_subset, reference_layout, !layout_has_unbounded_first_dim(reference_layout)
5,679✔
658
        };
5,679✔
659
        tiles_.insert({{&scope, container}, merged_tile});
5,679✔
660
    }
5,679✔
661
}
3,099✔
662

663
const MemoryTile* MemoryLayoutAnalysis::
664
    tile(const structured_control_flow::ControlFlowNode& scope, const std::string& container) const {
98✔
665
    auto key = std::make_pair(&scope, container);
98✔
666
    auto it = tiles_.find(key);
98✔
667
    if (it == tiles_.end()) {
98✔
668
        return nullptr;
6✔
669
    }
6✔
670
    return &it->second;
92✔
671
}
98✔
672

673
void MemoryLayoutAnalysis::compute_tile_groups(
674
    structured_control_flow::ControlFlowNode& scope,
675
    const std::string& container,
676
    const std::vector<const data_flow::Memlet*>& memlets,
677
    const MemoryLayout& reference_layout,
678
    size_t ndims,
679
    symbolic::BoundAnalysis& ba_tight,
680
    symbolic::BoundAnalysis& ba_loose
681
) {
1,983✔
682
    // Bound helpers reusing the caller's BoundAnalysis instances. These share
683
    // their memoization cache with merge_scope_layouts for the same scope, so
684
    // expressions bounded there hit instantly when re-encountered here.
685
    auto bound_lb = [&](const symbolic::Expression& e) -> symbolic::Expression {
9,628✔
686
        auto r = ba_tight.lower_bound(e);
9,628✔
687
        if (r.is_null()) r = ba_loose.lower_bound(e);
9,628✔
688
        return r;
9,628✔
689
    };
9,628✔
690
    auto bound_ub = [&](const symbolic::Expression& e) -> symbolic::Expression {
4,731✔
691
        auto r = ba_tight.upper_bound(e);
4,731✔
692
        if (r.is_null()) r = ba_loose.upper_bound(e);
4,731✔
693
        return r;
4,731✔
694
    };
4,731✔
695

696
    // For each memlet, compute per-dimension base (minimum of index expression)
697
    // Group memlets whose bases are symbolically equal in all dimensions
698
    struct GroupEntry {
1,983✔
699
        data_flow::Subset base; // per-dim minimum
1,983✔
700
        std::vector<const data_flow::Memlet*> group_memlets;
1,983✔
701
    };
1,983✔
702

703
    std::vector<GroupEntry> groups;
1,983✔
704

705
    for (const auto* memlet_ptr : memlets) {
2,947✔
706
        auto& acc = accesses_.at(memlet_ptr);
2,947✔
707
        if (acc.subset.size() != ndims) continue;
2,947✔
708

709
        // Compute per-dimension base (minimum)
710
        data_flow::Subset base;
2,947✔
711
        bool base_ok = true;
2,947✔
712
        for (size_t d = 0; d < ndims; ++d) {
7,778✔
713
            auto lb = bound_lb(acc.subset[d]);
4,853✔
714
            if (lb.is_null()) {
4,853✔
715
                base_ok = false;
22✔
716
                break;
22✔
717
            }
22✔
718
            base.push_back(symbolic::simplify(lb));
4,831✔
719
        }
4,831✔
720
        if (!base_ok) continue;
2,947✔
721

722
        // Find existing group with same base
723
        bool found = false;
2,925✔
724
        for (auto& group : groups) {
2,925✔
725
            if (group.base.size() != ndims) continue;
1,064✔
726
            bool match = true;
1,064✔
727
            for (size_t d = 0; d < ndims; ++d) {
2,308✔
728
                if (!symbolic::eq(group.base[d], base[d])) {
1,664✔
729
                    match = false;
420✔
730
                    break;
420✔
731
                }
420✔
732
            }
1,664✔
733
            if (match) {
1,064✔
734
                group.group_memlets.push_back(memlet_ptr);
644✔
735
                found = true;
644✔
736
                break;
644✔
737
            }
644✔
738
        }
1,064✔
739
        if (!found) {
2,925✔
740
            groups.push_back({base, {memlet_ptr}});
2,281✔
741
        }
2,281✔
742
    }
2,925✔
743

744
    if (groups.empty()) return;
1,983✔
745

746
    // Merge groups whose bases differ only by integer constants.
747
    // E.g. stencil bases [i-1, j], [i, j], [i+1, j] should merge (constant offsets in dim0).
748
    // But SYR2K bases [i, 0] vs [j, 0] should NOT merge (symbolic difference).
749
    std::vector<GroupEntry> merged_groups;
1,982✔
750
    for (auto& group : groups) {
2,281✔
751
        bool merged = false;
2,281✔
752
        for (auto& existing : merged_groups) {
2,281✔
753
            bool const_diff = true;
319✔
754
            for (size_t d = 0; d < ndims; ++d) {
681✔
755
                auto diff = symbolic::simplify(symbolic::sub(group.base[d], existing.base[d]));
493✔
756
                if (!SymEngine::is_a<SymEngine::Integer>(*diff)) {
493✔
757
                    const_diff = false;
131✔
758
                    break;
131✔
759
                }
131✔
760
            }
493✔
761
            if (const_diff) {
319✔
762
                existing.group_memlets
188✔
763
                    .insert(existing.group_memlets.end(), group.group_memlets.begin(), group.group_memlets.end());
188✔
764
                merged = true;
188✔
765
                break;
188✔
766
            }
188✔
767
        }
319✔
768
        if (!merged) {
2,281✔
769
            merged_groups.push_back(std::move(group));
2,093✔
770
        }
2,093✔
771
    }
2,281✔
772

773
    // Compute tile for each merged group
774
    std::vector<MemoryTileGroup> result_groups;
1,982✔
775
    for (auto& group : merged_groups) {
2,093✔
776
        std::vector<std::vector<symbolic::Expression>> min_indices(ndims);
2,093✔
777
        std::vector<std::vector<symbolic::Expression>> max_indices(ndims);
2,093✔
778

779
        for (const auto* memlet_ptr : group.group_memlets) {
2,925✔
780
            auto& acc = accesses_.at(memlet_ptr);
2,925✔
781
            for (size_t d = 0; d < ndims; ++d) {
7,756✔
782
                min_indices[d].push_back(acc.subset[d]);
4,831✔
783
                max_indices[d].push_back(acc.subset[d]);
4,831✔
784
            }
4,831✔
785
        }
2,925✔
786

787
        data_flow::Subset min_subset;
2,093✔
788
        data_flow::Subset max_subset;
2,093✔
789
        bool all_bounded = true;
2,093✔
790

791
        for (size_t d = 0; d < ndims; ++d) {
5,470✔
792
            symbolic::Expression dim_min = SymEngine::null;
3,413✔
793
            symbolic::Expression dim_max = SymEngine::null;
3,413✔
794

795
            for (const auto& idx : min_indices[d]) {
4,775✔
796
                auto lb = bound_lb(idx);
4,775✔
797
                if (lb.is_null()) {
4,775✔
798
                    all_bounded = false;
×
799
                    break;
×
800
                }
×
801
                if (dim_min.is_null()) {
4,775✔
802
                    dim_min = lb;
3,413✔
803
                } else {
3,413✔
804
                    dim_min = symbolic::min(dim_min, lb);
1,362✔
805
                }
1,362✔
806
            }
4,775✔
807
            if (!all_bounded) break;
3,413✔
808

809
            for (const auto& idx : max_indices[d]) {
4,731✔
810
                auto ub = bound_ub(idx);
4,731✔
811
                if (ub.is_null()) {
4,731✔
812
                    all_bounded = false;
36✔
813
                    break;
36✔
814
                }
36✔
815
                if (dim_max.is_null()) {
4,695✔
816
                    dim_max = ub;
3,377✔
817
                } else {
3,377✔
818
                    dim_max = symbolic::max(dim_max, ub);
1,318✔
819
                }
1,318✔
820
            }
4,695✔
821
            if (!all_bounded) break;
3,413✔
822

823
            min_subset.push_back(symbolic::simplify(dim_min));
3,377✔
824
            max_subset.push_back(symbolic::simplify(dim_max));
3,377✔
825
        }
3,377✔
826

827
        if (!all_bounded) continue;
2,093✔
828

829
        MemoryTile tile{
2,057✔
830
            container, min_subset, max_subset, reference_layout, !layout_has_unbounded_first_dim(reference_layout)
2,057✔
831
        };
2,057✔
832
        result_groups.push_back({tile, group.group_memlets});
2,057✔
833
    }
2,057✔
834

835
    if (!result_groups.empty()) {
1,982✔
836
        tile_groups_.insert({{&scope, container}, std::move(result_groups)});
1,948✔
837
    }
1,948✔
838
}
1,982✔
839

840
const std::vector<MemoryTileGroup>* MemoryLayoutAnalysis::
841
    tile_groups(const structured_control_flow::ControlFlowNode& scope, const std::string& container) const {
6✔
842
    auto key = std::make_pair(&scope, container);
6✔
843
    auto it = tile_groups_.find(key);
6✔
844
    if (it == tile_groups_.end()) {
6✔
845
        return nullptr;
×
846
    }
×
847
    return &it->second;
6✔
848
}
6✔
849

850
const MemoryTileGroup* MemoryLayoutAnalysis::
851
    tile_group_for(const structured_control_flow::ControlFlowNode& scope, const data_flow::Memlet& memlet) const {
83✔
852
    // Find which container this memlet accesses
853
    auto acc_it = accesses_.find(&memlet);
83✔
854
    if (acc_it == accesses_.end()) {
83✔
855
        return nullptr;
×
856
    }
×
857
    auto& container = acc_it->second.container;
83✔
858

859
    auto key = std::make_pair(&scope, container);
83✔
860
    auto groups_it = tile_groups_.find(key);
83✔
861
    if (groups_it == tile_groups_.end()) {
83✔
862
        return nullptr;
×
863
    }
×
864

865
    for (const auto& group : groups_it->second) {
85✔
866
        for (const auto* m : group.memlets) {
103✔
867
            if (m == &memlet) {
103✔
868
                return &group;
83✔
869
            }
83✔
870
        }
103✔
871
    }
85✔
872
    return nullptr;
×
873
}
83✔
874

875
symbolic::MultiExpression MemoryTile::extents() const {
55✔
876
    symbolic::MultiExpression result;
55✔
877
    for (size_t d = 0; d < min_subset.size(); ++d) {
166✔
878
        auto ext =
111✔
879
            symbolic::simplify(symbolic::expand(symbolic::add(symbolic::sub(max_subset[d], min_subset[d]), symbolic::one())
111✔
880
            ));
111✔
881
        // Defensive: subset values are always proven-bounded, so this should never trigger
882
        // for row-major layouts. Guards future custom layouts whose subsets could pick up
883
        // the unbounded sentinel.
884
        if (depends_on_unbounded(ext)) {
111✔
885
            result.push_back(SymEngine::null);
×
886
        } else {
111✔
887
            result.push_back(ext);
111✔
888
        }
111✔
889
    }
111✔
890
    return result;
55✔
891
}
55✔
892

893
symbolic::MultiExpression MemoryTile::extents_approx() const {
113✔
894
    symbolic::MultiExpression result;
113✔
895
    for (size_t d = 0; d < min_subset.size(); ++d) {
311✔
896
        auto ext = symbolic::simplify(symbolic::expand(
198✔
897
            symbolic::overapproximate(symbolic::add(symbolic::sub(max_subset[d], min_subset[d]), symbolic::one()))
198✔
898
        ));
198✔
899
        if (depends_on_unbounded(ext)) {
198✔
900
            result.push_back(SymEngine::null);
×
901
        } else {
198✔
902
            result.push_back(ext);
198✔
903
        }
198✔
904
    }
198✔
905
    return result;
113✔
906
}
113✔
907

908
std::pair<symbolic::Expression, symbolic::Expression> MemoryTile::contiguous_range() const {
60✔
909
    auto& strides = layout.strides();
60✔
910
    auto first = layout.offset();
60✔
911
    auto last = layout.offset();
60✔
912
    for (size_t d = 0; d < min_subset.size(); ++d) {
176✔
913
        first = symbolic::add(first, symbolic::mul(strides[d], min_subset[d]));
116✔
914
        last = symbolic::add(last, symbolic::mul(strides[d], max_subset[d]));
116✔
915
    }
116✔
916
    first = symbolic::simplify(symbolic::expand(first));
60✔
917
    last = symbolic::simplify(symbolic::expand(last));
60✔
918
    // If either endpoint references the unbounded sentinel, the linear range is undefined
919
    // (e.g. a non-row-major layout whose stride references shape[0]). Report as unknown
920
    // rather than leaking the sentinel symbol to callers.
921
    if (depends_on_unbounded(first) || depends_on_unbounded(last)) {
60✔
922
        return {SymEngine::null, SymEngine::null};
×
923
    }
×
924
    return {first, last};
60✔
925
}
60✔
926

927
} // namespace analysis
928
} // namespace sdfg
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc