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

daisytuner / docc / 27148613514

08 Jun 2026 03:32PM UTC coverage: 61.246% (-0.03%) from 61.275%
27148613514

Pull #741

github

web-flow
Merge 870ac7e2e into aacd50c09
Pull Request #741: replaces MemAccessRangeAnalysis with MemoryLayoutAnalysis

145 of 164 new or added lines in 4 files covered. (88.41%)

34 existing lines in 9 files now uncovered.

35524 of 58002 relevant lines covered (61.25%)

11129.02 hits per line

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

91.93
/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) {
11,079✔
31
    if (e.is_null()) return false;
11,079✔
32
    if (!SymEngine::is_a<SymEngine::Symbol>(*e)) return false;
11,079✔
33
    return SymEngine::down_cast<const SymEngine::Symbol&>(*e).get_name() == kUnboundedName;
10,370✔
34
}
11,079✔
35

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

44
bool layout_has_unbounded_first_dim(const MemoryLayout& layout) {
10,921✔
45
    const auto& shape = layout.shape();
10,921✔
46
    return !shape.empty() && is_unbounded_dim(shape[0]);
10,921✔
47
}
10,921✔
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
) {
2,687✔
55
    if (auto* loop = dynamic_cast<structured_control_flow::StructuredLoop*>(&scope)) {
2,687✔
56
        result.insert(&loop->root());
870✔
57
    } else if (auto* w = dynamic_cast<structured_control_flow::While*>(&scope)) {
1,817✔
58
        result.insert(&w->root());
3✔
59
    } else if (auto* seq = dynamic_cast<structured_control_flow::Sequence*>(&scope)) {
1,814✔
60
        for (size_t i = 0; i < seq->size(); i++) {
2,688✔
61
            auto& child = seq->at(i).first;
1,553✔
62
            if (!dynamic_cast<structured_control_flow::Block*>(&child)) {
1,553✔
63
                result.insert(&child);
886✔
64
            }
886✔
65
        }
1,553✔
66
    } else if (auto* ife = dynamic_cast<structured_control_flow::IfElse*>(&scope)) {
1,135✔
67
        for (size_t i = 0; i < ife->size(); i++) {
28✔
68
            result.insert(&ife->at(i).first);
16✔
69
        }
16✔
70
    }
12✔
71
}
2,687✔
72
} // namespace
73

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

76
void MemoryLayoutAnalysis::run(analysis::AnalysisManager& analysis_manager) {
245✔
77
    accesses_.clear();
245✔
78
    tiles_.clear();
245✔
79
    tile_groups_.clear();
245✔
80
    traverse(sdfg_.root(), analysis_manager);
245✔
81
}
245✔
82

83
void MemoryLayoutAnalysis::
84
    traverse(structured_control_flow::ControlFlowNode& node, analysis::AnalysisManager& analysis_manager) {
2,687✔
85
    // Snapshot current memlets and tile keys before recursing into the scope's children
86
    std::vector<const data_flow::Memlet*> memlets_before;
2,687✔
87
    memlets_before.reserve(accesses_.size());
2,687✔
88
    for (const auto& entry : accesses_) {
4,257✔
89
        memlets_before.push_back(entry.first);
4,257✔
90
    }
4,257✔
91
    std::set<std::pair<const structured_control_flow::ControlFlowNode*, std::string>> tiles_before;
2,687✔
92
    for (const auto& entry : tiles_) {
10,526✔
93
        tiles_before.insert(entry.first);
10,526✔
94
    }
10,526✔
95

96
    if (auto block = dynamic_cast<structured_control_flow::Block*>(&node)) {
2,687✔
97
        process_block(*block, analysis_manager);
667✔
98
    } else if (auto sequence = dynamic_cast<structured_control_flow::Sequence*>(&node)) {
2,020✔
99
        for (size_t i = 0; i < sequence->size(); i++) {
2,688✔
100
            traverse(sequence->at(i).first, analysis_manager);
1,553✔
101
        }
1,553✔
102
    } else if (auto if_else = dynamic_cast<structured_control_flow::IfElse*>(&node)) {
1,135✔
103
        for (size_t i = 0; i < if_else->size(); i++) {
28✔
104
            traverse(if_else->at(i).first, analysis_manager);
16✔
105
        }
16✔
106
    } else if (auto while_stmt = dynamic_cast<structured_control_flow::While*>(&node)) {
873✔
107
        traverse(while_stmt->root(), analysis_manager);
3✔
108
    } else if (auto loop = dynamic_cast<structured_control_flow::StructuredLoop*>(&node)) {
870✔
109
        traverse(loop->root(), analysis_manager);
870✔
110
    } else {
870✔
111
        // Break, Continue, Return nodes don't contain blocks
NEW
112
        return;
×
UNCOV
113
    }
×
114

115
    // Merge tiles for containers accessed within this scope
116
    merge_scope_layouts(node, memlets_before, tiles_before, analysis_manager);
2,687✔
117
}
2,687✔
118

119
void MemoryLayoutAnalysis::
120
    process_block(structured_control_flow::Block& block, analysis::AnalysisManager& analysis_manager) {
667✔
121
    auto& assumptions_analysis = analysis_manager.get<AssumptionsAnalysis>();
667✔
122
    // Use trivial bounds (type-derived, e.g. unsigned >= 0) so delinearization
123
    // can soundly discharge non-negativity proof obligations on parameters.
124
    auto& assumptions = assumptions_analysis.get(block, /*include_trivial_bounds=*/true);
667✔
125

126
    auto& dfg = block.dataflow();
667✔
127
    for (auto& memlet : dfg.edges()) {
1,938✔
128
        const auto& subset = memlet.subset();
1,938✔
129
        if (subset.empty()) {
1,938✔
130
            continue;
561✔
131
        }
561✔
132

133
        // Get container name from the AccessNode (either src or dst)
134
        std::string container_name;
1,377✔
135
        if (auto* access = dynamic_cast<const data_flow::AccessNode*>(&memlet.src())) {
1,377✔
136
            container_name = access->data();
889✔
137
        } else if (auto* access = dynamic_cast<const data_flow::AccessNode*>(&memlet.dst())) {
889✔
138
            container_name = access->data();
488✔
139
        } else {
488✔
140
            continue; // Skip memlets without AccessNode
×
141
        }
×
142

143
        auto& base_type = memlet.base_type();
1,377✔
144
        switch (base_type.type_id()) {
1,377✔
145
            case types::TypeID::Scalar:
×
146
            case types::TypeID::Structure:
×
147
                continue; // Skip scalars and structures
×
148
            case types::TypeID::Tensor: {
×
149
                // Tensor types already contain layout information, so we can directly store it without delinearization
150
                auto& tensor_type = dynamic_cast<const types::Tensor&>(memlet.base_type());
×
151

152
                MemoryLayout layout(tensor_type.shape(), tensor_type.strides(), tensor_type.offset());
×
153
                MemoryAccess layout_info{container_name, subset, layout, true};
×
154
                this->accesses_.emplace(&memlet, layout_info);
×
155
                continue;
×
156
            }
×
157
            case types::TypeID::Array: {
440✔
158
                // Arrays are c-like stack array, so we can infer a simple row-major layout without needing
159
                // delinearization
160
                auto* array_type = dynamic_cast<const types::Array*>(&memlet.base_type());
440✔
161
                symbolic::MultiExpression shape = {array_type->num_elements()};
440✔
162
                while (array_type->element_type().type_id() == types::TypeID::Array) {
454✔
163
                    array_type = dynamic_cast<const types::Array*>(&array_type->element_type());
14✔
164
                }
14✔
165
                if (array_type->element_type().type_id() != types::TypeID::Scalar) {
440✔
166
                    continue; // Skip non-scalar arrays
×
167
                }
×
168

169
                MemoryLayout layout(shape);
440✔
170
                MemoryAccess layout_info{container_name, subset, layout, true};
440✔
171
                this->accesses_.emplace(&memlet, layout_info);
440✔
172
                continue;
440✔
173
            }
440✔
174
            case types::TypeID::Pointer: {
937✔
175
                // For pointers, we attempt to delinearize the access pattern to infer the layout based
176
                // on assumptions from loop bounds
177
                auto* pointer_type = dynamic_cast<const types::Pointer*>(&memlet.base_type());
937✔
178

179
                // Typed pointer to a (possibly multi-dim) fixed array of scalar,
180
                // e.g. `float (*A)[M]`. The pointer adds one unbounded leading
181
                // dimension; remaining dimensions come from the array shape. The
182
                // subset is expected to be one index per dimension — no
183
                // delinearization needed.
184
                if (pointer_type->pointee_type().type_id() == types::TypeID::Array) {
937✔
185
                    auto* array_type = dynamic_cast<const types::Array*>(&pointer_type->pointee_type());
550✔
186
                    symbolic::MultiExpression array_shape = {array_type->num_elements()};
550✔
187
                    while (array_type->element_type().type_id() == types::TypeID::Array) {
568✔
188
                        array_type = dynamic_cast<const types::Array*>(&array_type->element_type());
18✔
189
                        array_shape.push_back(array_type->num_elements());
18✔
190
                    }
18✔
191
                    if (array_type->element_type().type_id() != types::TypeID::Scalar) {
550✔
NEW
192
                        continue; // Skip non-scalar leaf
×
NEW
193
                    }
×
194
                    if (subset.size() != array_shape.size() + 1) {
550✔
NEW
195
                        continue; // Require one index per dimension (leading pointer + array dims)
×
NEW
196
                    }
×
197

198
                    symbolic::MultiExpression shape;
550✔
199
                    shape.push_back(symbolic::symbol("__unbounded__"));
550✔
200
                    for (const auto& dim : array_shape) {
568✔
201
                        shape.push_back(dim);
568✔
202
                    }
568✔
203

204
                    MemoryLayout layout(shape);
550✔
205
                    MemoryAccess layout_info{container_name, subset, layout, false};
550✔
206
                    this->accesses_.emplace(&memlet, layout_info);
550✔
207
                    continue;
550✔
208
                }
550✔
209

210
                if (pointer_type->pointee_type().type_id() != types::TypeID::Scalar) {
387✔
211
                    continue; // Skip non-scalar pointers
1✔
212
                }
1✔
213

214
                if (subset.size() != 1) {
386✔
215
                    continue; // Require full linearization
×
216
                }
×
217
                auto& linearized_expr = subset.at(0);
386✔
218

219
                auto result = symbolic::delinearize(linearized_expr, assumptions);
386✔
220
                if (!result.success) {
386✔
221
                    continue; // Delinearization failed, skip
15✔
222
                }
15✔
223

224
                // Delinearization returns N indices but only N-1 dimensions (from stride division)
225
                // The first dimension is unbounded - insert a placeholder that will be filled in by merge
226
                // Using a special symbol as placeholder for the first dimension
227
                symbolic::MultiExpression shape;
371✔
228
                shape.push_back(symbolic::symbol("__unbounded__"));
371✔
229
                for (const auto& dim : result.dimensions) {
371✔
230
                    shape.push_back(dim);
257✔
231
                }
257✔
232

233
                // Store symbolic indices and dimensions with unbounded first dimension
234
                // The merge phase will attempt to bound the first dimension using loop assumptions
235
                MemoryLayout layout(shape);
371✔
236
                MemoryAccess layout_info{container_name, result.indices, layout, false};
371✔
237
                this->accesses_.emplace(&memlet, layout_info);
371✔
238
                continue;
371✔
239
            }
386✔
240
            default:
×
241
                continue; // Skip unsupported types
×
242
        }
1,377✔
243
    }
1,377✔
244
}
667✔
245

246
const MemoryAccess* MemoryLayoutAnalysis::access(const data_flow::Memlet& memlet) const {
4,422✔
247
    auto layout_it = accesses_.find(&memlet);
4,422✔
248
    if (layout_it == accesses_.end()) {
4,422✔
249
        return nullptr;
6✔
250
    }
6✔
251
    return &layout_it->second;
4,416✔
252
}
4,422✔
253

254
void MemoryLayoutAnalysis::merge_scope_layouts(
255
    structured_control_flow::ControlFlowNode& scope,
256
    const std::vector<const data_flow::Memlet*>& memlets_before,
257
    const std::set<std::pair<const structured_control_flow::ControlFlowNode*, std::string>>& tiles_before,
258
    analysis::AnalysisManager& analysis_manager
259
) {
2,687✔
260
    // Convert memlets_before to a set for O(1) lookup
261
    std::unordered_set<const data_flow::Memlet*> before_set(memlets_before.begin(), memlets_before.end());
2,687✔
262

263
    // Group all new accesses by container
264
    std::unordered_map<std::string, std::vector<const data_flow::Memlet*>> all_container_groups;
2,687✔
265
    for (auto& [memlet_ptr, acc] : accesses_) {
14,207✔
266
        if (before_set.find(memlet_ptr) != before_set.end()) {
14,207✔
267
            continue;
4,257✔
268
        }
4,257✔
269
        all_container_groups[acc.container].push_back(memlet_ptr);
9,950✔
270
    }
9,950✔
271

272
    // Sort memlets within each container group by element_id for deterministic processing order
273
    for (auto& [container, memlets] : all_container_groups) {
5,358✔
274
        std::sort(memlets.begin(), memlets.end(), [](const data_flow::Memlet* a, const data_flow::Memlet* b) {
9,273✔
275
            return a->element_id() < b->element_id();
9,273✔
276
        });
9,273✔
277
    }
5,358✔
278

279
    auto* loop = dynamic_cast<structured_control_flow::StructuredLoop*>(&scope);
2,687✔
280

281
    auto& assumptions_analysis = analysis_manager.get<AssumptionsAnalysis>();
2,687✔
282
    // For loops, query at the loop body so the induction variable's bounds are visible.
283
    auto& assumption_node = loop ? static_cast<structured_control_flow::ControlFlowNode&>(loop->root()) : scope;
2,687✔
284
    // Trivial-bounds view: includes type-derived defaults (e.g. Int32 ∈ [INT_MIN, INT_MAX]).
285
    // Used as the assumption set passed to symbolic::minimum/maximum so that the
286
    // resolver has sign information for parameters.
287
    auto& assumptions = assumptions_analysis.get(assumption_node, /*include_trivial_bounds=*/true);
2,687✔
288
    // Narrowing-only view: excludes type-derived defaults. A symbol that only
289
    // appears here (or in neither) has at most its type's intrinsic range — any
290
    // min/max resolution would collapse to INT_MIN/INT_MAX-style numerics, which
291
    // is not a sound tile bound. We use this to decide whether to emit a tile.
292
    auto& narrowing_assumptions = assumptions_analysis.get(assumption_node, /*include_trivial_bounds=*/false);
2,687✔
293
    // Parameters of a scope can only be constant symbols (invariant within the
294
    // scope). SDFG-level read-only arguments are constant by construction; for
295
    // each scope-local entry, the constant() flag tells us whether the symbol
296
    // can be treated opaquely by the min/max resolver.
297
    symbolic::SymbolSet parameters = assumptions_analysis.parameters();
2,687✔
298
    for (auto& entry : assumptions) {
23,389✔
299
        if (loop && symbolic::eq(entry.first, loop->indvar())) {
23,389✔
300
            continue; // The induction variable is not a parameter of its own loop scope
870✔
301
        }
870✔
302
        if (entry.second.constant()) {
22,519✔
303
            parameters.insert(entry.first);
8,653✔
304
        }
8,653✔
305
    }
22,519✔
306

307
    // Soundness check: every free (non-parameter) symbol in an index expression
308
    // must have a narrowing assumption at this scope. Otherwise symbolic::minimum/
309
    // maximum would fall back to the symbol's type-default range and produce
310
    // bogus tile bounds (e.g. INT_MAX) that the rest of the pipeline would
311
    // silently consume as truth.
312
    auto has_narrowing = [&](const symbolic::Symbol& sym) -> bool {
2,687✔
313
        auto it = narrowing_assumptions.find(sym);
68✔
314
        if (it == narrowing_assumptions.end()) return false;
68✔
NEW
315
        return !it->second.lower_bounds().empty() || !it->second.upper_bounds().empty();
×
316
    };
68✔
317
    auto bounds_are_sound = [&](const symbolic::Expression& expr) -> bool {
20,476✔
318
        for (const auto& sym : symbolic::atoms(expr)) {
20,476✔
319
            if (parameters.contains(sym)) continue;
19,944✔
320
            if (loop && symbolic::eq(sym, loop->indvar())) continue;
2,967✔
321
            if (!has_narrowing(sym)) return false;
68✔
322
        }
68✔
323
        return true;
20,408✔
324
    };
20,476✔
325

326
    // Find direct child scopes that may carry tiles for this scope
327
    std::set<const structured_control_flow::ControlFlowNode*> direct_child_scopes;
2,687✔
328
    collect_direct_child_scopes(scope, direct_child_scopes);
2,687✔
329

330
    for (auto& [container, memlets] : all_container_groups) {
5,358✔
331
        if (memlets.empty()) continue;
5,358✔
332

333
        // Find inner tiles from direct child scopes only
334
        std::vector<const MemoryTile*> inner_tiles;
5,358✔
335
        for (auto& [key, tile] : tiles_) {
85,989✔
336
            if (tiles_before.count(key) > 0) continue;
85,989✔
337
            if (key.second != container) continue;
63,347✔
338
            if (direct_child_scopes.count(key.first) == 0) continue;
20,675✔
339
            inner_tiles.push_back(&tile);
3,668✔
340
        }
3,668✔
341

342
        size_t ndims = 0;
5,358✔
343
        MemoryLayout reference_layout({symbolic::one()});
5,358✔
344
        // Separate min/max index lists to avoid unnecessary symbolic min/max
345
        std::vector<std::vector<symbolic::Expression>> min_indices;
5,358✔
346
        std::vector<std::vector<symbolic::Expression>> max_indices;
5,358✔
347

348
        if (!inner_tiles.empty()) {
5,358✔
349
            // Use inner tile min/max as representative values
350
            // Inner tiles have already resolved inner loop variables to their bounds
351
            ndims = inner_tiles[0]->min_subset.size();
3,474✔
352
            reference_layout = inner_tiles[0]->layout;
3,474✔
353
            min_indices.resize(ndims);
3,474✔
354
            max_indices.resize(ndims);
3,474✔
355

356
            for (const auto* tile : inner_tiles) {
3,668✔
357
                if (tile->min_subset.size() != ndims) continue;
3,668✔
358
                for (size_t d = 0; d < ndims; ++d) {
9,761✔
359
                    min_indices[d].push_back(tile->min_subset[d]);
6,093✔
360
                    max_indices[d].push_back(tile->max_subset[d]);
6,093✔
361
                }
6,093✔
362
            }
3,668✔
363

364
            // Propagate tile groups from child scopes upward using the same
365
            // base-partitioning logic: group inner groups by their min_subset
366
            // base at this scope level, then merge each partition.
367
            std::vector<const MemoryTileGroup*> inner_groups;
3,474✔
368
            for (auto& [key, groups] : tile_groups_) {
70,641✔
369
                if (tiles_before.count({key.first, key.second}) > 0) continue;
70,641✔
370
                if (key.second != container) continue;
58,672✔
371
                if (direct_child_scopes.count(key.first) == 0) continue;
19,732✔
372
                for (const auto& g : groups) {
4,148✔
373
                    inner_groups.push_back(&g);
4,148✔
374
                }
4,148✔
375
            }
3,668✔
376

377
            if (!inner_groups.empty()) {
3,474✔
378
                // Group inner groups by their base at this level
379
                struct OuterGroupEntry {
3,474✔
380
                    data_flow::Subset base;
3,474✔
381
                    std::vector<const MemoryTileGroup*> constituents;
3,474✔
382
                };
3,474✔
383
                std::vector<OuterGroupEntry> outer_partitions;
3,474✔
384

385
                for (const auto* ig : inner_groups) {
4,148✔
386
                    if (ig->tile.min_subset.size() != ndims) continue;
4,148✔
387

388
                    // Compute base: minimum of the inner group's min_subset per dim
389
                    data_flow::Subset base;
4,148✔
390
                    bool base_ok = true;
4,148✔
391
                    for (size_t d = 0; d < ndims; ++d) {
11,134✔
392
                        auto lb = symbolic::minimum(ig->tile.min_subset[d], parameters, assumptions, true);
6,986✔
393
                        if (lb.is_null()) {
6,986✔
394
                            lb = symbolic::minimum(ig->tile.min_subset[d], parameters, assumptions, false);
10✔
395
                        }
10✔
396
                        if (lb.is_null()) {
6,986✔
397
                            base_ok = false;
×
398
                            break;
×
399
                        }
×
400
                        base.push_back(symbolic::simplify(lb));
6,986✔
401
                    }
6,986✔
402
                    if (!base_ok) continue;
4,148✔
403

404
                    // Find matching partition (same base OR constant-offset base)
405
                    bool found = false;
4,148✔
406
                    for (auto& op : outer_partitions) {
4,148✔
407
                        bool const_diff = true;
865✔
408
                        for (size_t d = 0; d < ndims; ++d) {
1,518✔
409
                            auto diff = symbolic::simplify(symbolic::sub(base[d], op.base[d]));
1,220✔
410
                            if (!SymEngine::is_a<SymEngine::Integer>(*diff)) {
1,220✔
411
                                const_diff = false;
567✔
412
                                break;
567✔
413
                            }
567✔
414
                        }
1,220✔
415
                        if (const_diff) {
865✔
416
                            op.constituents.push_back(ig);
298✔
417
                            found = true;
298✔
418
                            break;
298✔
419
                        }
298✔
420
                    }
865✔
421
                    if (!found) {
4,148✔
422
                        outer_partitions.push_back({base, {ig}});
3,850✔
423
                    }
3,850✔
424
                }
4,148✔
425

426
                // For each partition, merge constituent tile bounds and collect memlets
427
                std::vector<MemoryTileGroup> result_groups;
3,474✔
428
                for (auto& op : outer_partitions) {
3,850✔
429
                    data_flow::Subset grp_min, grp_max;
3,850✔
430
                    bool grp_bounded = true;
3,850✔
431

432
                    for (size_t d = 0; d < ndims; ++d) {
10,309✔
433
                        symbolic::Expression d_min = SymEngine::null;
6,466✔
434
                        symbolic::Expression d_max = SymEngine::null;
6,466✔
435

436
                        for (const auto* c : op.constituents) {
6,984✔
437
                            // min from min_subset
438
                            auto lb = symbolic::minimum(c->tile.min_subset[d], parameters, assumptions, true);
6,984✔
439
                            if (lb.is_null())
6,984✔
440
                                lb = symbolic::minimum(c->tile.min_subset[d], parameters, assumptions, false);
8✔
441
                            if (lb.is_null()) {
6,984✔
442
                                grp_bounded = false;
×
443
                                break;
×
444
                            }
×
445
                            d_min = d_min.is_null() ? lb : symbolic::min(d_min, lb);
6,984✔
446

447
                            // max from max_subset
448
                            auto ub = symbolic::maximum(c->tile.max_subset[d], parameters, assumptions, true);
6,984✔
449
                            if (ub.is_null())
6,984✔
450
                                ub = symbolic::maximum(c->tile.max_subset[d], parameters, assumptions, false);
15✔
451
                            if (ub.is_null()) {
6,984✔
452
                                grp_bounded = false;
7✔
453
                                break;
7✔
454
                            }
7✔
455
                            d_max = d_max.is_null() ? ub : symbolic::max(d_max, ub);
6,977✔
456
                        }
6,977✔
457
                        if (!grp_bounded) break;
6,466✔
458
                        grp_min.push_back(symbolic::simplify(d_min));
6,459✔
459
                        grp_max.push_back(symbolic::simplify(d_max));
6,459✔
460
                    }
6,459✔
461
                    if (!grp_bounded) continue;
3,850✔
462

463
                    // Collect all memlets from constituent groups
464
                    std::vector<const data_flow::Memlet*> grp_memlets;
3,843✔
465
                    for (const auto* c : op.constituents) {
4,139✔
466
                        grp_memlets.insert(grp_memlets.end(), c->memlets.begin(), c->memlets.end());
4,139✔
467
                    }
4,139✔
468

469
                    MemoryTile grp_tile{
3,843✔
470
                        container, grp_min, grp_max, reference_layout, !layout_has_unbounded_first_dim(reference_layout)
3,843✔
471
                    };
3,843✔
472
                    result_groups.push_back({grp_tile, std::move(grp_memlets)});
3,843✔
473
                }
3,843✔
474

475
                if (!result_groups.empty()) {
3,474✔
476
                    tile_groups_.insert({{&scope, container}, std::move(result_groups)});
3,467✔
477
                }
3,467✔
478
            }
3,474✔
479
        } else {
3,474✔
480
            // Use raw access indices (no inner tiles available)
481
            auto& first_access = accesses_.at(memlets[0]);
1,884✔
482
            auto& reference_shape = first_access.layout.shape();
1,884✔
483
            ndims = reference_shape.size();
1,884✔
484
            reference_layout = first_access.layout;
1,884✔
485
            min_indices.resize(ndims);
1,884✔
486
            max_indices.resize(ndims);
1,884✔
487

488
            bool consistent = true;
1,884✔
489
            for (const auto* memlet_ptr : memlets) {
2,777✔
490
                auto& acc = accesses_.at(memlet_ptr);
2,777✔
491
                auto& shape = acc.layout.shape();
2,777✔
492

493
                if (shape.size() != ndims) {
2,777✔
494
                    consistent = false;
×
495
                    break;
×
496
                }
×
497
                // Check inner dimensions match (all except first which may be unbounded)
498
                for (size_t d = 1; d < ndims; ++d) {
4,425✔
499
                    if (!symbolic::eq(shape[d], reference_shape[d])) {
1,648✔
500
                        consistent = false;
×
501
                        break;
×
502
                    }
×
503
                }
1,648✔
504
                if (!consistent) break;
2,777✔
505

506
                // Collect indices for each dimension
507
                if (acc.subset.size() != ndims) {
2,777✔
508
                    consistent = false;
134✔
509
                    break;
134✔
510
                }
134✔
511
                for (size_t d = 0; d < ndims; ++d) {
6,934✔
512
                    min_indices[d].push_back(acc.subset[d]);
4,291✔
513
                    max_indices[d].push_back(acc.subset[d]);
4,291✔
514
                }
4,291✔
515
            }
2,643✔
516

517
            if (!consistent) continue;
1,884✔
518

519
            // Compute tile groups for raw memlets
520
            compute_tile_groups(scope, container, memlets, reference_layout, ndims, parameters, assumptions);
1,750✔
521
        }
1,750✔
522

523
        if (ndims == 0) continue;
5,224✔
524

525
        // Compute min/max bounds for each dimension
526
        data_flow::Subset min_subset;
5,224✔
527
        data_flow::Subset max_subset;
5,224✔
528
        bool all_bounded = true;
5,224✔
529

530
        for (size_t d = 0; d < ndims; ++d) {
13,672✔
531
            symbolic::Expression dim_min = SymEngine::null;
8,521✔
532
            symbolic::Expression dim_max = SymEngine::null;
8,521✔
533

534
            // Compute dim_min from min_indices
535
            for (const auto& idx : min_indices[d]) {
10,269✔
536
                if (!bounds_are_sound(idx)) {
10,269✔
537
                    all_bounded = false;
60✔
538
                    break;
60✔
539
                }
60✔
540
                auto lb = symbolic::minimum(idx, parameters, assumptions, true);
10,209✔
541
                if (lb.is_null()) {
10,209✔
542
                    lb = symbolic::minimum(idx, parameters, assumptions, false);
6✔
543
                }
6✔
544
                if (lb.is_null()) {
10,209✔
545
                    all_bounded = false;
2✔
546
                    break;
2✔
547
                }
2✔
548
                if (dim_min.is_null()) {
10,207✔
549
                    dim_min = lb;
8,459✔
550
                } else {
8,459✔
551
                    dim_min = symbolic::min(dim_min, lb);
1,748✔
552
                }
1,748✔
553
            }
10,207✔
554
            if (!all_bounded) break;
8,521✔
555

556
            // Compute dim_max from max_indices
557
            for (const auto& idx : max_indices[d]) {
10,207✔
558
                if (!bounds_are_sound(idx)) {
10,207✔
559
                    all_bounded = false;
8✔
560
                    break;
8✔
561
                }
8✔
562
                auto ub = symbolic::maximum(idx, parameters, assumptions, true);
10,199✔
563
                if (ub.is_null()) {
10,199✔
564
                    ub = symbolic::maximum(idx, parameters, assumptions, false);
3✔
565
                }
3✔
566
                if (ub.is_null()) {
10,199✔
567
                    all_bounded = false;
3✔
568
                    break;
3✔
569
                }
3✔
570
                if (dim_max.is_null()) {
10,196✔
571
                    dim_max = ub;
8,448✔
572
                } else {
8,448✔
573
                    dim_max = symbolic::max(dim_max, ub);
1,748✔
574
                }
1,748✔
575
            }
10,196✔
576
            if (!all_bounded) break;
8,459✔
577

578
            min_subset.push_back(symbolic::simplify(dim_min));
8,448✔
579
            max_subset.push_back(symbolic::simplify(dim_max));
8,448✔
580
        }
8,448✔
581

582
        if (!all_bounded) continue;
5,224✔
583

584
        // Store this scope's tile with the original memory layout. `first_dim_bounded`
585
        // mirrors the underlying layout: false whenever shape[0] is the unbounded sentinel.
586
        MemoryTile merged_tile{
5,151✔
587
            container, min_subset, max_subset, reference_layout, !layout_has_unbounded_first_dim(reference_layout)
5,151✔
588
        };
5,151✔
589
        tiles_.insert({{&scope, container}, merged_tile});
5,151✔
590
    }
5,151✔
591
}
2,687✔
592

593
const MemoryTile* MemoryLayoutAnalysis::
594
    tile(const structured_control_flow::ControlFlowNode& scope, const std::string& container) const {
45✔
595
    auto key = std::make_pair(&scope, container);
45✔
596
    auto it = tiles_.find(key);
45✔
597
    if (it == tiles_.end()) {
45✔
598
        return nullptr;
2✔
599
    }
2✔
600
    return &it->second;
43✔
601
}
45✔
602

603
void MemoryLayoutAnalysis::compute_tile_groups(
604
    structured_control_flow::ControlFlowNode& scope,
605
    const std::string& container,
606
    const std::vector<const data_flow::Memlet*>& memlets,
607
    const MemoryLayout& reference_layout,
608
    size_t ndims,
609
    const symbolic::SymbolSet& parameters,
610
    const symbolic::Assumptions& assumptions
611
) {
1,750✔
612
    // For each memlet, compute per-dimension base (minimum of index expression)
613
    // Group memlets whose bases are symbolically equal in all dimensions
614
    struct GroupEntry {
1,750✔
615
        data_flow::Subset base; // per-dim minimum
1,750✔
616
        std::vector<const data_flow::Memlet*> group_memlets;
1,750✔
617
    };
1,750✔
618

619
    std::vector<GroupEntry> groups;
1,750✔
620

621
    for (const auto* memlet_ptr : memlets) {
2,643✔
622
        auto& acc = accesses_.at(memlet_ptr);
2,643✔
623
        if (acc.subset.size() != ndims) continue;
2,643✔
624

625
        // Compute per-dimension base (minimum)
626
        data_flow::Subset base;
2,643✔
627
        bool base_ok = true;
2,643✔
628
        for (size_t d = 0; d < ndims; ++d) {
6,932✔
629
            auto lb = symbolic::minimum(acc.subset[d], parameters, assumptions, true);
4,291✔
630
            if (lb.is_null()) {
4,291✔
631
                lb = symbolic::minimum(acc.subset[d], parameters, assumptions, false);
164✔
632
            }
164✔
633
            if (lb.is_null()) {
4,291✔
634
                base_ok = false;
2✔
635
                break;
2✔
636
            }
2✔
637
            base.push_back(symbolic::simplify(lb));
4,289✔
638
        }
4,289✔
639
        if (!base_ok) continue;
2,643✔
640

641
        // Find existing group with same base
642
        bool found = false;
2,641✔
643
        for (auto& group : groups) {
2,641✔
644
            if (group.base.size() != ndims) continue;
1,011✔
645
            bool match = true;
1,011✔
646
            for (size_t d = 0; d < ndims; ++d) {
2,105✔
647
                if (!symbolic::eq(group.base[d], base[d])) {
1,533✔
648
                    match = false;
439✔
649
                    break;
439✔
650
                }
439✔
651
            }
1,533✔
652
            if (match) {
1,011✔
653
                group.group_memlets.push_back(memlet_ptr);
572✔
654
                found = true;
572✔
655
                break;
572✔
656
            }
572✔
657
        }
1,011✔
658
        if (!found) {
2,641✔
659
            groups.push_back({base, {memlet_ptr}});
2,069✔
660
        }
2,069✔
661
    }
2,641✔
662

663
    if (groups.empty()) return;
1,750✔
664

665
    // Merge groups whose bases differ only by integer constants.
666
    // E.g. stencil bases [i-1, j], [i, j], [i+1, j] should merge (constant offsets in dim0).
667
    // But SYR2K bases [i, 0] vs [j, 0] should NOT merge (symbolic difference).
668
    std::vector<GroupEntry> merged_groups;
1,748✔
669
    for (auto& group : groups) {
2,069✔
670
        bool merged = false;
2,069✔
671
        for (auto& existing : merged_groups) {
2,069✔
672
            bool const_diff = true;
342✔
673
            for (size_t d = 0; d < ndims; ++d) {
597✔
674
                auto diff = symbolic::simplify(symbolic::sub(group.base[d], existing.base[d]));
485✔
675
                if (!SymEngine::is_a<SymEngine::Integer>(*diff)) {
485✔
676
                    const_diff = false;
230✔
677
                    break;
230✔
678
                }
230✔
679
            }
485✔
680
            if (const_diff) {
342✔
681
                existing.group_memlets
112✔
682
                    .insert(existing.group_memlets.end(), group.group_memlets.begin(), group.group_memlets.end());
112✔
683
                merged = true;
112✔
684
                break;
112✔
685
            }
112✔
686
        }
342✔
687
        if (!merged) {
2,069✔
688
            merged_groups.push_back(std::move(group));
1,957✔
689
        }
1,957✔
690
    }
2,069✔
691

692
    // Compute tile for each merged group
693
    std::vector<MemoryTileGroup> result_groups;
1,748✔
694
    for (auto& group : merged_groups) {
1,957✔
695
        std::vector<std::vector<symbolic::Expression>> min_indices(ndims);
1,957✔
696
        std::vector<std::vector<symbolic::Expression>> max_indices(ndims);
1,957✔
697

698
        for (const auto* memlet_ptr : group.group_memlets) {
2,641✔
699
            auto& acc = accesses_.at(memlet_ptr);
2,641✔
700
            for (size_t d = 0; d < ndims; ++d) {
6,930✔
701
                min_indices[d].push_back(acc.subset[d]);
4,289✔
702
                max_indices[d].push_back(acc.subset[d]);
4,289✔
703
            }
4,289✔
704
        }
2,641✔
705

706
        data_flow::Subset min_subset;
1,957✔
707
        data_flow::Subset max_subset;
1,957✔
708
        bool all_bounded = true;
1,957✔
709

710
        for (size_t d = 0; d < ndims; ++d) {
5,079✔
711
            symbolic::Expression dim_min = SymEngine::null;
3,152✔
712
            symbolic::Expression dim_max = SymEngine::null;
3,152✔
713

714
            for (const auto& idx : min_indices[d]) {
4,233✔
715
                auto lb = symbolic::minimum(idx, parameters, assumptions, true);
4,233✔
716
                if (lb.is_null()) {
4,233✔
717
                    lb = symbolic::minimum(idx, parameters, assumptions, false);
106✔
718
                }
106✔
719
                if (lb.is_null()) {
4,233✔
720
                    all_bounded = false;
×
721
                    break;
×
722
                }
×
723
                if (dim_min.is_null()) {
4,233✔
724
                    dim_min = lb;
3,152✔
725
                } else {
3,152✔
726
                    dim_min = symbolic::min(dim_min, lb);
1,081✔
727
                }
1,081✔
728
            }
4,233✔
729
            if (!all_bounded) break;
3,152✔
730

731
            for (const auto& idx : max_indices[d]) {
4,189✔
732
                auto ub = symbolic::maximum(idx, parameters, assumptions, true);
4,189✔
733
                if (ub.is_null()) {
4,189✔
734
                    ub = symbolic::maximum(idx, parameters, assumptions, false);
62✔
735
                }
62✔
736
                if (ub.is_null()) {
4,189✔
737
                    all_bounded = false;
30✔
738
                    break;
30✔
739
                }
30✔
740
                if (dim_max.is_null()) {
4,159✔
741
                    dim_max = ub;
3,122✔
742
                } else {
3,122✔
743
                    dim_max = symbolic::max(dim_max, ub);
1,037✔
744
                }
1,037✔
745
            }
4,159✔
746
            if (!all_bounded) break;
3,152✔
747

748
            min_subset.push_back(symbolic::simplify(dim_min));
3,122✔
749
            max_subset.push_back(symbolic::simplify(dim_max));
3,122✔
750
        }
3,122✔
751

752
        if (!all_bounded) continue;
1,957✔
753

754
        MemoryTile tile{
1,927✔
755
            container, min_subset, max_subset, reference_layout, !layout_has_unbounded_first_dim(reference_layout)
1,927✔
756
        };
1,927✔
757
        result_groups.push_back({tile, group.group_memlets});
1,927✔
758
    }
1,927✔
759

760
    if (!result_groups.empty()) {
1,748✔
761
        tile_groups_.insert({{&scope, container}, std::move(result_groups)});
1,720✔
762
    }
1,720✔
763
}
1,748✔
764

765
const std::vector<MemoryTileGroup>* MemoryLayoutAnalysis::
766
    tile_groups(const structured_control_flow::ControlFlowNode& scope, const std::string& container) const {
6✔
767
    auto key = std::make_pair(&scope, container);
6✔
768
    auto it = tile_groups_.find(key);
6✔
769
    if (it == tile_groups_.end()) {
6✔
770
        return nullptr;
×
771
    }
×
772
    return &it->second;
6✔
773
}
6✔
774

775
const MemoryTileGroup* MemoryLayoutAnalysis::
776
    tile_group_for(const structured_control_flow::ControlFlowNode& scope, const data_flow::Memlet& memlet) const {
59✔
777
    // Find which container this memlet accesses
778
    auto acc_it = accesses_.find(&memlet);
59✔
779
    if (acc_it == accesses_.end()) {
59✔
UNCOV
780
        return nullptr;
×
UNCOV
781
    }
×
782
    auto& container = acc_it->second.container;
59✔
783

784
    auto key = std::make_pair(&scope, container);
59✔
785
    auto groups_it = tile_groups_.find(key);
59✔
786
    if (groups_it == tile_groups_.end()) {
59✔
787
        return nullptr;
×
788
    }
×
789

790
    for (const auto& group : groups_it->second) {
61✔
791
        for (const auto* m : group.memlets) {
72✔
792
            if (m == &memlet) {
72✔
793
                return &group;
59✔
794
            }
59✔
795
        }
72✔
796
    }
61✔
797
    return nullptr;
×
798
}
59✔
799

800
symbolic::MultiExpression MemoryTile::extents() const {
25✔
801
    symbolic::MultiExpression result;
25✔
802
    for (size_t d = 0; d < min_subset.size(); ++d) {
81✔
803
        auto ext =
56✔
804
            symbolic::simplify(symbolic::expand(symbolic::add(symbolic::sub(max_subset[d], min_subset[d]), symbolic::one())
56✔
805
            ));
56✔
806
        // Defensive: subset values are always proven-bounded, so this should never trigger
807
        // for row-major layouts. Guards future custom layouts whose subsets could pick up
808
        // the unbounded sentinel.
809
        if (depends_on_unbounded(ext)) {
56✔
NEW
810
            result.push_back(SymEngine::null);
×
811
        } else {
56✔
812
            result.push_back(ext);
56✔
813
        }
56✔
814
    }
56✔
815
    return result;
25✔
816
}
25✔
817

818
symbolic::MultiExpression MemoryTile::extents_approx() const {
82✔
819
    symbolic::MultiExpression result;
82✔
820
    for (size_t d = 0; d < min_subset.size(); ++d) {
219✔
821
        auto ext = symbolic::simplify(symbolic::expand(
137✔
822
            symbolic::overapproximate(symbolic::add(symbolic::sub(max_subset[d], min_subset[d]), symbolic::one()))
137✔
823
        ));
137✔
824
        if (depends_on_unbounded(ext)) {
137✔
NEW
825
            result.push_back(SymEngine::null);
×
826
        } else {
137✔
827
            result.push_back(ext);
137✔
828
        }
137✔
829
    }
137✔
830
    return result;
82✔
831
}
82✔
832

833
std::pair<symbolic::Expression, symbolic::Expression> MemoryTile::contiguous_range() const {
28✔
834
    auto& strides = layout.strides();
28✔
835
    auto first = layout.offset();
28✔
836
    auto last = layout.offset();
28✔
837
    for (size_t d = 0; d < min_subset.size(); ++d) {
84✔
838
        first = symbolic::add(first, symbolic::mul(strides[d], min_subset[d]));
56✔
839
        last = symbolic::add(last, symbolic::mul(strides[d], max_subset[d]));
56✔
840
    }
56✔
841
    first = symbolic::simplify(symbolic::expand(first));
28✔
842
    last = symbolic::simplify(symbolic::expand(last));
28✔
843
    // If either endpoint references the unbounded sentinel, the linear range is undefined
844
    // (e.g. a non-row-major layout whose stride references shape[0]). Report as unknown
845
    // rather than leaking the sentinel symbol to callers.
846
    if (depends_on_unbounded(first) || depends_on_unbounded(last)) {
28✔
NEW
847
        return {SymEngine::null, SymEngine::null};
×
NEW
848
    }
×
849
    return {first, last};
28✔
850
}
28✔
851

852
} // namespace analysis
853
} // 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