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

daisytuner / docc / 28101952530

24 Jun 2026 01:27PM UTC coverage: 61.988% (+0.1%) from 61.844%
28101952530

Pull #802

github

web-flow
Merge 54d7d8794 into 57cc1db99
Pull Request #802: adds reduce as new structured loop type

604 of 780 new or added lines in 24 files covered. (77.44%)

96 existing lines in 11 files now uncovered.

38005 of 61310 relevant lines covered (61.99%)

1006.39 hits per line

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

82.8
/sdfg/src/analysis/loop_carried_dependency_analysis.cpp
1
#include "sdfg/analysis/loop_carried_dependency_analysis.h"
2

3
#include <cassert>
4
#include <map>
5
#include <optional>
6
#include <set>
7
#include <string>
8
#include <vector>
9

10
#include <isl/ctx.h>
11
#include <isl/options.h>
12
#include <isl/set.h>
13

14
#include "sdfg/analysis/analysis.h"
15
#include "sdfg/analysis/assumptions_analysis.h"
16
#include "sdfg/analysis/data_dependency_analysis.h"
17
#include "sdfg/analysis/loop_analysis.h"
18
#include "sdfg/analysis/memory_layout_analysis.h"
19
#include "sdfg/analysis/users.h"
20
#include "sdfg/data_flow/access_node.h"
21
#include "sdfg/data_flow/library_nodes/math/cmath/cmath_node.h"
22
#include "sdfg/data_flow/memlet.h"
23
#include "sdfg/data_flow/tasklet.h"
24
#include "sdfg/structured_control_flow/block.h"
25
#include "sdfg/structured_control_flow/if_else.h"
26
#include "sdfg/structured_control_flow/sequence.h"
27
#include "sdfg/structured_sdfg.h"
28
#include "sdfg/symbolic/maps.h"
29
#include "sdfg/symbolic/polyhedral.h"
30
#include "sdfg/symbolic/symbolic.h"
31
#include "sdfg/types/scalar.h"
32

33
namespace sdfg {
34
namespace analysis {
35

36
LoopCarriedDependencyAnalysis::LoopCarriedDependencyAnalysis(StructuredSDFG& sdfg)
37
    : Analysis(sdfg), node_(sdfg.root()) {}
181✔
38

39
LoopCarriedDependencyAnalysis::LoopCarriedDependencyAnalysis(StructuredSDFG& sdfg, structured_control_flow::Sequence& node)
40
    : Analysis(sdfg), node_(node) {}
×
41

42
DataDependencyAnalysis& LoopCarriedDependencyAnalysis::detailed_dda() {
181✔
43
    if (!detailed_dda_) {
181✔
44
        detailed_dda_ = std::make_unique<DataDependencyAnalysis>(this->sdfg_, this->node_);
181✔
45
        detailed_dda_->set_detailed(true);
181✔
46
    }
181✔
47
    return *detailed_dda_;
181✔
48
}
181✔
49

50
void LoopCarriedDependencyAnalysis::analyze_loop(
51
    analysis::AnalysisManager& /*analysis_manager*/, structured_control_flow::StructuredLoop& /*loop*/
52
) {
×
53
    // Per-loop work is done inline in `run()`.
54
}
×
55

56
namespace {
57

58
bool is_undefined_user(User& user) {
4,261✔
59
    // Mirror DDA::is_undefined_user — undefined users have a null vertex.
60
    return user.element() == nullptr;
4,261✔
61
}
4,261✔
62

63
// Collect a user's subsets, preferring delinearized (multi-dim) subsets from
64
// MemoryLayoutAnalysis when available. ISL cannot reason precisely about
65
// linearized expressions like `i*N + j`; delinearization recovers `[i, j]` so
66
// `dependence_deltas` can compute exact distance vectors.
67
//
68
// The returned vector is index-aligned with the user's underlying memlets:
69
// for each memlet, either MLA's delinearized subset (when MLA succeeded) or
70
// the memlet's original subset (linearized fallback).
71
std::vector<data_flow::Subset> collect_subsets(User& user, MemoryLayoutAnalysis& mla) {
4,256✔
72
    std::vector<data_flow::Subset> result;
4,256✔
73
    auto* access_node = dynamic_cast<data_flow::AccessNode*>(user.element());
4,256✔
74
    if (access_node == nullptr) {
4,256✔
75
        for (auto& s : user.subsets()) result.push_back(s);
×
76
        return result;
×
77
    }
×
78
    auto& graph = access_node->get_parent();
4,256✔
79
    if (user.use() == Use::READ || user.use() == Use::VIEW) {
4,256✔
80
        for (auto& edge : graph.out_edges(*access_node)) {
1,010✔
81
            if (auto* acc = mla.access(edge)) {
1,010✔
82
                result.push_back(acc->subset);
1,008✔
83
            } else {
1,008✔
84
                result.push_back(edge.subset());
2✔
85
            }
2✔
86
        }
1,010✔
87
    } else if (user.use() == Use::WRITE || user.use() == Use::MOVE) {
3,315✔
88
        for (auto& edge : graph.in_edges(*access_node)) {
3,315✔
89
            if (auto* acc = mla.access(edge)) {
3,315✔
90
                result.push_back(acc->subset);
3,315✔
91
            } else {
3,315✔
92
                result.push_back(edge.subset());
×
93
            }
×
94
        }
3,315✔
95
    }
3,315✔
96
    return result;
4,256✔
97
}
4,256✔
98

99
// Compute the union of iteration-distance delta sets between two users wrt
100
// `loop`'s indvar. Inner-loop indvars are marked non-constant so that the ISL
101
// formulation existentially quantifies them per-side (giving "different inner
102
// iteration" the freedom it needs).
103
//
104
// Returns {empty=true} when no loop-carried dependence exists between the two
105
// users. Returns {empty=false, deltas_str=""} for the scalar shortcut and for
106
// undefined users (dependence exists, distance unrepresentable).
107
symbolic::maps::DependenceDeltas pair_deltas(
108
    StructuredSDFG& sdfg,
109
    User& previous,
110
    User& current,
111
    analysis::AnalysisManager& analysis_manager,
112
    AssumptionsAnalysis& assumptions_analysis,
113
    structured_control_flow::StructuredLoop& loop
114
) {
5,728✔
115
    symbolic::maps::DependenceDeltas empty_result{true, "", {}};
5,728✔
116

117
    if (previous.container() != current.container()) {
5,728✔
118
        return empty_result;
×
119
    }
×
120

121
    // Scalar shortcut: any pair of accesses to a scalar is loop-carried but the
122
    // distance vector is unrepresentable.
123
    auto& type = sdfg.type(previous.container());
5,728✔
124
    if (dynamic_cast<const types::Scalar*>(&type)) {
5,728✔
125
        return symbolic::maps::DependenceDeltas{false, "", {}};
3,596✔
126
    }
3,596✔
127

128
    if (is_undefined_user(previous) || is_undefined_user(current)) {
2,132✔
129
        return empty_result;
4✔
130
    }
4✔
131

132
    // Use MLA-delinearized subsets when available so ISL can reason precisely
133
    // about multi-dimensional pointer accesses.
134
    auto& mla = analysis_manager.get<analysis::MemoryLayoutAnalysis>();
2,128✔
135
    auto previous_subsets = collect_subsets(previous, mla);
2,128✔
136
    auto current_subsets = collect_subsets(current, mla);
2,128✔
137

138
    auto previous_scope = Users::scope(&previous);
2,128✔
139
    auto previous_assumptions = assumptions_analysis.get(*previous_scope, true);
2,128✔
140
    auto current_scope = Users::scope(&current);
2,128✔
141
    auto current_assumptions = assumptions_analysis.get(*current_scope, true);
2,128✔
142

143
    // Mark loop's indvar and all nested loop indvars as non-constant from this
144
    // loop's perspective.
145
    auto& loop_analysis = analysis_manager.get<analysis::LoopAnalysis>();
2,128✔
146
    if (previous_assumptions.find(loop.indvar()) != previous_assumptions.end()) {
2,128✔
147
        previous_assumptions.at(loop.indvar()).constant(false);
2,128✔
148
    }
2,128✔
149
    if (current_assumptions.find(loop.indvar()) != current_assumptions.end()) {
2,128✔
150
        current_assumptions.at(loop.indvar()).constant(false);
2,128✔
151
    }
2,128✔
152
    for (auto& inner_loop : loop_analysis.descendants(&loop)) {
5,174✔
153
        if (auto structured_loop = dynamic_cast<const structured_control_flow::StructuredLoop*>(inner_loop)) {
5,174✔
154
            auto indvar = structured_loop->indvar();
5,174✔
155
            if (previous_assumptions.find(indvar) != previous_assumptions.end()) {
5,174✔
156
                previous_assumptions.at(indvar).constant(false);
5,174✔
157
            }
5,174✔
158
            if (current_assumptions.find(indvar) != current_assumptions.end()) {
5,174✔
159
                current_assumptions.at(indvar).constant(false);
5,174✔
160
            }
5,174✔
161
        }
5,174✔
162
    }
5,174✔
163

164
    // Collect deltas across all subset pairs and union them.
165
    symbolic::AssumptionsBounds previous_bounds(previous_assumptions);
2,128✔
166
    symbolic::AssumptionsBounds current_bounds(current_assumptions);
2,128✔
167

168
    isl_ctx* union_ctx = nullptr;
2,128✔
169
    isl_set* accumulated = nullptr;
2,128✔
170
    std::vector<std::string> result_dimensions;
2,128✔
171

172
    for (auto& previous_subset : previous_subsets) {
2,128✔
173
        for (auto& current_subset : current_subsets) {
2,179✔
174
            auto deltas = symbolic::maps::
2,179✔
175
                dependence_deltas(previous_subset, current_subset, loop.indvar(), previous_bounds, current_bounds);
2,179✔
176
            if (deltas.empty) {
2,179✔
177
                continue;
1,486✔
178
            }
1,486✔
179
            if (deltas.deltas_str.empty()) {
693✔
180
                if (accumulated) {
77✔
181
                    isl_set_free(accumulated);
×
182
                    isl_ctx_free(union_ctx);
×
183
                }
×
184
                return symbolic::maps::DependenceDeltas{false, "", {}};
77✔
185
            }
77✔
186
            if (!union_ctx) {
616✔
187
                union_ctx = isl_ctx_alloc();
596✔
188
                isl_options_set_on_error(union_ctx, ISL_ON_ERROR_CONTINUE);
596✔
189
                accumulated = isl_set_read_from_str(union_ctx, deltas.deltas_str.c_str());
596✔
190
                result_dimensions = deltas.dimensions;
596✔
191
            } else {
596✔
192
                isl_set* new_set = isl_set_read_from_str(union_ctx, deltas.deltas_str.c_str());
20✔
193
                if (new_set && accumulated) {
20✔
194
                    accumulated = isl_set_union(accumulated, new_set);
20✔
195
                } else if (new_set) {
20✔
196
                    isl_set_free(new_set);
×
197
                }
×
198
            }
20✔
199
        }
616✔
200
    }
2,128✔
201

202
    if (!accumulated) {
2,051✔
203
        if (union_ctx) {
1,455✔
204
            isl_ctx_free(union_ctx);
×
205
        }
×
206
        return empty_result;
1,455✔
207
    }
1,455✔
208

209
    char* str = isl_set_to_str(accumulated);
596✔
210
    if (!str) {
596✔
211
        isl_set_free(accumulated);
×
212
        isl_ctx_free(union_ctx);
×
213
        return symbolic::maps::DependenceDeltas{false, "", {}};
×
214
    }
×
215
    std::string union_str(str);
596✔
216
    free(str);
596✔
217

218
    isl_set_free(accumulated);
596✔
219
    isl_ctx_free(union_ctx);
596✔
220

221
    return symbolic::maps::DependenceDeltas{false, union_str, result_dimensions};
596✔
222
}
596✔
223

224
void merge_deltas(LoopCarriedDependencyInfo& info, const symbolic::maps::DependenceDeltas& add) {
115✔
225
    if (add.empty) return;
115✔
226
    if (info.deltas.empty) {
115✔
227
        info.deltas = add;
×
228
        return;
×
229
    }
×
230
    if (add.deltas_str.empty() || info.deltas.deltas_str.empty()) {
115✔
231
        info.deltas.empty = false;
59✔
232
        return;
59✔
233
    }
59✔
234
    isl_ctx* ctx = isl_ctx_alloc();
56✔
235
    isl_options_set_on_error(ctx, ISL_ON_ERROR_CONTINUE);
56✔
236
    isl_set* s1 = isl_set_read_from_str(ctx, info.deltas.deltas_str.c_str());
56✔
237
    isl_set* s2 = isl_set_read_from_str(ctx, add.deltas_str.c_str());
56✔
238
    if (s1 && s2) {
56✔
239
        isl_set* u = isl_set_union(s1, s2);
56✔
240
        char* str = u ? isl_set_to_str(u) : nullptr;
56✔
241
        if (str) {
56✔
242
            info.deltas.deltas_str = std::string(str);
23✔
243
            free(str);
23✔
244
        } else {
33✔
245
            info.deltas.deltas_str = "";
33✔
246
            info.deltas.dimensions.clear();
33✔
247
        }
33✔
248
        if (u) isl_set_free(u);
56✔
249
    } else {
56✔
250
        if (s1) isl_set_free(s1);
×
251
        if (s2) isl_set_free(s2);
×
252
        info.deltas.deltas_str = "";
×
253
        info.deltas.dimensions.clear();
×
254
    }
×
255
    isl_ctx_free(ctx);
56✔
256
}
56✔
257

258
bool user_in_subtree(User& user, const structured_control_flow::ControlFlowNode& subtree) {
×
259
    if (user.element() == nullptr) {
×
260
        return false;
×
261
    }
×
262
    auto* scope = Users::scope(&user);
×
263
    while (scope != nullptr) {
×
264
        if (scope == &subtree) {
×
265
            return true;
×
266
        }
×
267
        scope = scope->get_parent();
×
268
    }
×
269
    return false;
×
270
}
×
271

272
// Map an associative/commutative combine node (scalar tasklet or CMath library
273
// node) to its reduction operator. Returns nullopt for any node that is not a
274
// recognized reorderable reduction operator.
275
std::optional<structured_control_flow::ReductionOperation> combine_operator(const data_flow::DataFlowNode& node) {
137✔
276
    using structured_control_flow::ReductionOperation;
137✔
277
    if (auto* tasklet = dynamic_cast<const data_flow::Tasklet*>(&node)) {
137✔
278
        switch (tasklet->code()) {
133✔
279
            case data_flow::TaskletCode::fp_add:
47✔
280
            case data_flow::TaskletCode::int_add:
48✔
281
                return ReductionOperation::Add;
48✔
282
            case data_flow::TaskletCode::fp_mul:
4✔
283
            case data_flow::TaskletCode::int_mul:
4✔
284
                return ReductionOperation::Mul;
4✔
285
            case data_flow::TaskletCode::int_smin:
1✔
286
            case data_flow::TaskletCode::int_umin:
1✔
287
                return ReductionOperation::Min;
1✔
288
            case data_flow::TaskletCode::int_smax:
1✔
289
            case data_flow::TaskletCode::int_umax:
1✔
290
                return ReductionOperation::Max;
1✔
291
            default:
79✔
292
                return std::nullopt;
79✔
293
        }
133✔
294
    }
133✔
295
    if (auto* cmath = dynamic_cast<const math::cmath::CMathNode*>(&node)) {
4✔
296
        switch (cmath->function()) {
4✔
297
            case math::cmath::CMathFunction::fmax:
2✔
298
                return ReductionOperation::Max;
2✔
NEW
299
            case math::cmath::CMathFunction::fmin:
×
NEW
300
                return ReductionOperation::Min;
×
301
            default:
2✔
302
                return std::nullopt;
2✔
303
        }
4✔
304
    }
4✔
NEW
305
    return std::nullopt;
×
306
}
4✔
307

308
// Collect the dataflow Blocks directly belonging to `node`'s subtree, descending
309
// through Sequences and IfElse branches but NOT into nested loops (those have
310
// their own loop-carried analysis). Used to scan a loop body for the combine.
311
void collect_body_blocks(
312
    const structured_control_flow::ControlFlowNode& node, std::vector<const structured_control_flow::Block*>& out
313
) {
1,609✔
314
    if (auto* block = dynamic_cast<const structured_control_flow::Block*>(&node)) {
1,609✔
315
        out.push_back(block);
529✔
316
    } else if (auto* seq = dynamic_cast<const structured_control_flow::Sequence*>(&node)) {
1,080✔
317
        for (size_t i = 0; i < seq->size(); i++) {
1,609✔
318
            collect_body_blocks(seq->at(i).first, out);
951✔
319
        }
951✔
320
    } else if (auto* ifelse = dynamic_cast<const structured_control_flow::IfElse*>(&node)) {
658✔
321
        for (size_t i = 0; i < ifelse->size(); i++) {
13✔
322
            collect_body_blocks(ifelse->at(i).first, out);
8✔
323
        }
8✔
324
    }
5✔
325
    // Stop at nested StructuredLoop / While: their bodies belong to other loops.
326
}
1,609✔
327

328
// True if the accumulator subset is invariant across the reduction loop: the
329
// induction variable does not appear in any index expression. This is a sound
330
// (conservative) guard — an accumulator whose address moves with the induction
331
// variable is not a single-location reduction. The actual *equality* of the
332
// written and read-back addresses is proven separately and precisely by
333
// `symbolic::polyhedral::equal_on_domain`.
334
bool address_invariant_in_indvar(const data_flow::Subset& subset, const symbolic::Symbol& indvar) {
56✔
335
    for (auto& dim : subset) {
63✔
336
        for (auto& atom : symbolic::atoms(dim)) {
69✔
337
            if (atom->get_name() == indvar->get_name()) {
69✔
338
                return false;
3✔
339
            }
3✔
340
        }
69✔
341
    }
63✔
342
    return true;
53✔
343
}
56✔
344

345
} // namespace
346

347
void LoopCarriedDependencyAnalysis::run(analysis::AnalysisManager& analysis_manager) {
181✔
348
    dependencies_.clear();
181✔
349
    pairs_.clear();
181✔
350

351
    // Build a fresh branch-condition-aware assumptions analysis. The
352
    // manager-cached `AssumptionsAnalysis` deliberately skips IfElse-branch
353
    // refinement to stay cheap; LCDA needs the refined coupled constraints
354
    // so that `dependence_deltas`'s ISL formulation can prove halo-style
355
    // patterns are non-loop-carried.
356
    detailed_assumptions_ = std::make_unique<AssumptionsAnalysis>(this->sdfg_, /*with_branch_conditions=*/true);
181✔
357
    detailed_assumptions_->run(analysis_manager);
181✔
358

359
    // Drive entirely from DDA's reaching-definitions scaffold:
360
    //   - DDA computes per-loop boundary snapshots (upward-exposed reads,
361
    //     escaping definitions) — its primary job.
362
    //   - LCDA enumerates the cross-iteration pair space and computes delta
363
    //     sets via `pair_deltas` (using `symbolic::maps::dependence_deltas`).
364
    //
365
    // For a structured loop L with indvar i_L:
366
    //   pairs(L) = { (W,R, RAW, Δ_L(W,R)) : W ∈ esc(L), R ∈ ue(L),
367
    //                                       cont(W) = cont(R), Δ ≠ ∅ }
368
    //            ∪ { (W₁,W₂, WAW, Δ_L(W₁,W₂)) : W₁,W₂ ∈ esc(L),
369
    //                                           cont(W₁) = cont(W₂), Δ ≠ ∅ }
370
    auto& dda = detailed_dda();
181✔
371
    dda.run(analysis_manager);
181✔
372
    auto& loop_analysis = analysis_manager.get<analysis::LoopAnalysis>();
181✔
373

374
    for (auto* loop_node : loop_analysis.loops()) {
650✔
375
        auto* loop = dynamic_cast<structured_control_flow::StructuredLoop*>(loop_node);
650✔
376
        if (loop == nullptr) {
650✔
UNCOV
377
            continue;
×
UNCOV
378
        }
×
379

380
        // Restrict to loops within the analysis scope (`node_`).
381
        bool in_scope = false;
650✔
382
        structured_control_flow::ControlFlowNode* cur = loop;
650✔
383
        while (cur != nullptr) {
2,822✔
384
            if (cur == &node_) {
2,822✔
385
                in_scope = true;
650✔
386
                break;
650✔
387
            }
650✔
388
            cur = cur->get_parent();
2,172✔
389
        }
2,172✔
390
        if (!in_scope) {
650✔
UNCOV
391
            continue;
×
UNCOV
392
        }
×
393

394
        // Non-monotonic / unanalyzable loops: don't register; consumers must
395
        // treat absence as "no info" and fall back to safe defaults.
396
        if (!loop->is_monotonic()) {
650✔
UNCOV
397
            continue;
×
UNCOV
398
        }
×
399
        if (!dda.has_loop_boundary(*loop)) {
650✔
400
            continue;
×
UNCOV
401
        }
×
402

403
        auto& ue_reads = dda.upward_exposed_reads(*loop);
650✔
404
        auto& esc_defs = dda.escaping_definitions(*loop);
650✔
405

406
        auto& deps = dependencies_[loop];
650✔
407
        auto& pair_list = pairs_[loop];
650✔
408

409
        // RAW: escaping_writes × upward_exposed_reads
410
        for (auto& write_entry : esc_defs) {
2,863✔
411
            auto* write = write_entry.first;
2,863✔
412
            for (auto* read : ue_reads) {
44,927✔
413
                if (write->container() != read->container()) {
44,927✔
414
                    continue;
43,960✔
415
                }
43,960✔
416
                auto deltas = pair_deltas(this->sdfg_, *write, *read, analysis_manager, *detailed_assumptions_, *loop);
967✔
417
                if (deltas.empty) continue;
967✔
418
                pair_list.push_back(LoopCarriedDependencyPair{write, read, LOOP_CARRIED_DEPENDENCY_READ_WRITE, deltas});
389✔
419
                auto it = deps.find(read->container());
389✔
420
                if (it == deps.end()) {
389✔
421
                    deps[read->container()] = LoopCarriedDependencyInfo{LOOP_CARRIED_DEPENDENCY_READ_WRITE, deltas};
274✔
422
                } else {
274✔
423
                    it->second.type = LOOP_CARRIED_DEPENDENCY_READ_WRITE;
115✔
424
                    merge_deltas(it->second, deltas);
115✔
425
                }
115✔
426
            }
389✔
427
        }
2,863✔
428

429
        // WAW: escaping_writes × escaping_writes (ordered pairs incl. self)
430
        for (auto& w1_entry : esc_defs) {
2,863✔
431
            auto* w1 = w1_entry.first;
2,863✔
432
            for (auto& w2_entry : esc_defs) {
26,575✔
433
                auto* w2 = w2_entry.first;
26,575✔
434
                if (w1->container() != w2->container()) {
26,575✔
435
                    continue;
21,814✔
436
                }
21,814✔
437
                auto deltas = pair_deltas(this->sdfg_, *w1, *w2, analysis_manager, *detailed_assumptions_, *loop);
4,761✔
438
                if (deltas.empty) continue;
4,761✔
439
                pair_list.push_back(LoopCarriedDependencyPair{w1, w2, LOOP_CARRIED_DEPENDENCY_WRITE_WRITE, deltas});
3,880✔
440
                if (deps.find(w1->container()) == deps.end()) {
3,880✔
441
                    deps[w1->container()] = LoopCarriedDependencyInfo{LOOP_CARRIED_DEPENDENCY_WRITE_WRITE, deltas};
1,212✔
442
                }
1,212✔
443
            }
3,880✔
444
        }
2,863✔
445

446
        detect_reductions(*loop);
650✔
447
    }
650✔
448
}
181✔
449

450
void LoopCarriedDependencyAnalysis::detect_reductions(structured_control_flow::StructuredLoop& loop) {
650✔
451
    // Ensure an entry exists for every analyzed loop so queries on reduction-free
452
    // loops return an empty list rather than asserting.
453
    auto& result = reductions_[&loop];
650✔
454

455
    auto dep_it = dependencies_.find(&loop);
650✔
456
    if (dep_it == dependencies_.end()) {
650✔
NEW
UNCOV
457
        return;
×
NEW
UNCOV
458
    }
×
459
    auto& deps = dep_it->second;
650✔
460

461
    std::vector<const structured_control_flow::Block*> blocks;
650✔
462
    collect_body_blocks(loop.root(), blocks);
650✔
463

464
    auto indvar = loop.indvar();
650✔
465

466
    // Assumptions for the loop body, used by the polyhedral equality test. They
467
    // must be taken from the loop body (`loop.root()`), not the loop node, so
468
    // that the induction variable's own bounds (e.g. 0 <= i < N) are present in
469
    // the domain. The induction variable must additionally be treated as
470
    // evolving (a domain dimension) rather than a constant parameter so that
471
    // shifted accesses such as A[i] vs A[i-1] are correctly rejected.
472
    symbolic::Assumptions assums = detailed_assumptions_->get(loop.root(), true);
650✔
473
    auto indvar_it = assums.find(indvar);
650✔
474
    if (indvar_it != assums.end()) {
650✔
475
        indvar_it->second.constant(false);
650✔
476
    }
650✔
477

478
    // Candidate accumulator -> operator. A container is rejected (and stays out
479
    // of the result) if any of its writes in the body is not a clean combine.
480
    std::map<std::string, structured_control_flow::ReductionOperation> found;
650✔
481
    std::set<std::string> rejected;
650✔
482

483
    for (auto* block : blocks) {
650✔
484
        auto& graph = block->dataflow();
529✔
485
        for (auto& node : graph.nodes()) {
2,069✔
486
            auto* write_node = dynamic_cast<const data_flow::AccessNode*>(&node);
2,069✔
487
            if (write_node == nullptr) {
2,069✔
488
                continue;
567✔
489
            }
567✔
490

491
            std::vector<const data_flow::Memlet*> in_edges;
1,502✔
492
            for (auto& edge : graph.in_edges(*write_node)) {
1,502✔
493
                in_edges.push_back(&edge);
568✔
494
            }
568✔
495
            if (in_edges.empty()) {
1,502✔
496
                continue; // not written in this block
934✔
497
            }
934✔
498

499
            const std::string& container = write_node->data();
568✔
500

501
            // Only loop-carried read-write containers can be reductions.
502
            auto ci = deps.find(container);
568✔
503
            if (ci == deps.end() || ci->second.type != LOOP_CARRIED_DEPENDENCY_READ_WRITE) {
568✔
504
                continue;
431✔
505
            }
431✔
506

507
            // A reduction accumulator is produced by exactly one combine node.
508
            if (in_edges.size() != 1) {
137✔
NEW
509
                rejected.insert(container);
×
NEW
510
                continue;
×
NEW
511
            }
×
512
            auto& write_edge = *in_edges.front();
137✔
513
            auto& combine = write_edge.src();
137✔
514

515
            auto op = combine_operator(combine);
137✔
516
            if (!op.has_value()) {
137✔
517
                rejected.insert(container);
81✔
518
                continue;
81✔
519
            }
81✔
520

521
            // The combine must read the same accumulator back (acc = acc OP x).
522
            const data_flow::Memlet* read_edge = nullptr;
56✔
523
            for (auto& edge : graph.in_edges(combine)) {
71✔
524
                if (auto* read_node = dynamic_cast<const data_flow::AccessNode*>(&edge.src())) {
71✔
525
                    if (read_node->data() == container) {
71✔
526
                        read_edge = &edge;
56✔
527
                        break;
56✔
528
                    }
56✔
529
                }
71✔
530
            }
71✔
531
            if (read_edge == nullptr) {
56✔
NEW
532
                rejected.insert(container);
×
NEW
533
                continue;
×
NEW
534
            }
×
535

536
            // The accumulator must address one fixed, loop-invariant location
537
            // and the written cell must equal the read-back cell. `equal_on_domain`
538
            // proves the latter precisely (parametric/linearized affine forms),
539
            // while the invariance guard rejects accumulators that move with the
540
            // induction variable.
541
            if (!address_invariant_in_indvar(write_edge.subset(), indvar) ||
56✔
542
                !symbolic::polyhedral::equal_on_domain(write_edge.subset(), read_edge->subset(), indvar, assums)) {
56✔
543
                rejected.insert(container);
4✔
544
                continue;
4✔
545
            }
4✔
546

547
            auto existing = found.find(container);
52✔
548
            if (existing != found.end() && existing->second != op.value()) {
52✔
549
                rejected.insert(container);
1✔
550
                continue;
1✔
551
            }
1✔
552
            found[container] = op.value();
51✔
553
        }
51✔
554
    }
529✔
555

556
    for (auto& entry : found) {
650✔
557
        if (rejected.count(entry.first) != 0) {
51✔
558
            continue;
1✔
559
        }
1✔
560
        result.push_back(structured_control_flow::ReductionInfo{entry.second, entry.first});
50✔
561
    }
50✔
562
}
650✔
563

564
bool LoopCarriedDependencyAnalysis::available(structured_control_flow::StructuredLoop& loop) const {
192✔
565
    return pairs_.find(&loop) != pairs_.end();
192✔
566
}
192✔
567

568
const std::unordered_map<std::string, LoopCarriedDependencyInfo>& LoopCarriedDependencyAnalysis::
569
    dependencies(structured_control_flow::StructuredLoop& loop) const {
372✔
570
    auto it = dependencies_.find(&loop);
372✔
571
    assert(it != dependencies_.end() && "LoopCarriedDependencyAnalysis: loop not analyzed");
372✔
572
    return it->second;
372✔
573
}
372✔
574

575
const std::vector<LoopCarriedDependencyPair>& LoopCarriedDependencyAnalysis::pairs(structured_control_flow::StructuredLoop&
576
                                                                                       loop) const {
126✔
577
    auto it = pairs_.find(&loop);
126✔
578
    assert(it != pairs_.end() && "LoopCarriedDependencyAnalysis: loop not analyzed");
126✔
579
    return it->second;
126✔
580
}
126✔
581

582
std::vector<const LoopCarriedDependencyPair*> LoopCarriedDependencyAnalysis::pairs_between(
583
    structured_control_flow::StructuredLoop& loop,
584
    const structured_control_flow::ControlFlowNode& subtree_a,
585
    const structured_control_flow::ControlFlowNode& subtree_b,
586
    analysis::AnalysisManager& analysis_manager
587
) const {
×
588
    std::vector<const LoopCarriedDependencyPair*> result;
×
589
    auto it = pairs_.find(&loop);
×
590
    if (it == pairs_.end()) {
×
591
        return result;
×
592
    }
×
593

594
    for (auto& pair : it->second) {
×
UNCOV
595
        bool wa = user_in_subtree(*pair.writer, subtree_a);
×
UNCOV
596
        bool wb = user_in_subtree(*pair.writer, subtree_b);
×
UNCOV
597
        bool ra = user_in_subtree(*pair.reader, subtree_a);
×
UNCOV
598
        bool rb = user_in_subtree(*pair.reader, subtree_b);
×
599

UNCOV
600
        if ((wa && rb) || (wb && ra)) {
×
UNCOV
601
            result.push_back(&pair);
×
UNCOV
602
        }
×
UNCOV
603
    }
×
UNCOV
604
    return result;
×
UNCOV
605
}
×
606

607
bool LoopCarriedDependencyAnalysis::has_loop_carried(structured_control_flow::StructuredLoop& loop) const {
2✔
608
    auto it = pairs_.find(&loop);
2✔
609
    if (it == pairs_.end()) return false;
2✔
610
    return !it->second.empty();
2✔
611
}
2✔
612

613
bool LoopCarriedDependencyAnalysis::has_loop_carried_raw(structured_control_flow::StructuredLoop& loop) const {
5✔
614
    auto it = pairs_.find(&loop);
5✔
615
    if (it == pairs_.end()) return false;
5✔
616
    for (auto& p : it->second) {
5✔
617
        if (p.type == LOOP_CARRIED_DEPENDENCY_READ_WRITE) return true;
5✔
618
    }
5✔
619
    return false;
1✔
620
}
5✔
621

622
bool LoopCarriedDependencyAnalysis::has_loop_carried_hazard(structured_control_flow::StructuredLoop& loop) const {
198✔
623
    auto it = pairs_.find(&loop);
198✔
624
    if (it == pairs_.end()) return false;
198✔
625
    for (auto& p : it->second) {
397✔
626
        if (p.type != LOOP_CARRIED_DEPENDENCY_WRITE_WRITE) return true;
397✔
627
    }
397✔
628
    return false;
77✔
629
}
198✔
630

631
const std::vector<structured_control_flow::ReductionInfo>& LoopCarriedDependencyAnalysis::
632
    reductions(structured_control_flow::StructuredLoop& loop) const {
235✔
633
    auto it = reductions_.find(&loop);
235✔
634
    assert(it != reductions_.end() && "LoopCarriedDependencyAnalysis: loop not analyzed");
235✔
635
    return it->second;
235✔
636
}
235✔
637

638
bool LoopCarriedDependencyAnalysis::has_reductions(structured_control_flow::StructuredLoop& loop) const {
18✔
639
    auto it = reductions_.find(&loop);
18✔
640
    return it != reductions_.end() && !it->second.empty();
18✔
641
}
18✔
642

643
bool LoopCarriedDependencyAnalysis::is_reduction_only(structured_control_flow::StructuredLoop& loop) const {
215✔
644
    auto rit = reductions_.find(&loop);
215✔
645
    if (rit == reductions_.end() || rit->second.empty()) {
215✔
646
        return false;
194✔
647
    }
194✔
648
    std::set<std::string> reduction_containers;
21✔
649
    for (auto& reduction : rit->second) {
23✔
650
        reduction_containers.insert(reduction.container);
23✔
651
    }
23✔
652

653
    auto pit = pairs_.find(&loop);
21✔
654
    if (pit == pairs_.end()) {
21✔
NEW
UNCOV
655
        return false;
×
NEW
UNCOV
656
    }
×
657
    for (auto& pair : pit->second) {
53✔
658
        if (pair.type == LOOP_CARRIED_DEPENDENCY_WRITE_WRITE) {
53✔
659
            continue;
30✔
660
        }
30✔
661
        if (reduction_containers.count(pair.writer->container()) == 0) {
23✔
662
            return false;
1✔
663
        }
1✔
664
    }
23✔
665
    return true;
20✔
666
}
21✔
667

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

© 2026 Coveralls, Inc