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

daisytuner / docc / 27293229917

10 Jun 2026 05:17PM UTC coverage: 61.101% (-0.007%) from 61.108%
27293229917

Pull #744

github

web-flow
Merge 5478255e1 into 00b1577a8
Pull Request #744: Segformer fixes

42 of 74 new or added lines in 2 files covered. (56.76%)

36088 of 59063 relevant lines covered (61.1%)

747.54 hits per line

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

79.0
/opt/src/transformations/map_fusion.cpp
1
#include "sdfg/transformations/map_fusion.h"
2

3
#include <isl/ctx.h>
4
#include <isl/map.h>
5
#include <isl/options.h>
6
#include <isl/set.h>
7
#include <isl/space.h>
8
#include <symengine/solve.h>
9
#include "sdfg/analysis/arguments_analysis.h"
10
#include "sdfg/analysis/loop_analysis.h"
11
#include "sdfg/analysis/scope_analysis.h"
12

13
#include "sdfg/analysis/assumptions_analysis.h"
14
#include "sdfg/control_flow/interstate_edge.h"
15
#include "sdfg/data_flow/data_flow_graph.h"
16
#include "sdfg/structured_control_flow/block.h"
17
#include "sdfg/structured_control_flow/for.h"
18
#include "sdfg/symbolic/delinearization.h"
19
#include "sdfg/symbolic/utils.h"
20

21
namespace sdfg {
22
namespace transformations {
23

24
MapFusion::MapFusion(
25
    structured_control_flow::Map& first_map,
26
    structured_control_flow::StructuredLoop& second_loop,
27
    bool require_consecutive
28
)
29
    : first_map_(first_map), second_loop_(second_loop), require_consecutive_(require_consecutive) {}
41✔
30

31
std::string MapFusion::name() const { return "MapFusion"; }
2✔
32

33
std::vector<std::pair<symbolic::Symbol, symbolic::Expression>> MapFusion::solve_subsets(
34
    const data_flow::Subset& producer_subset,
35
    const data_flow::Subset& consumer_subset,
36
    const std::vector<structured_control_flow::StructuredLoop*>& producer_loops,
37
    const std::vector<structured_control_flow::StructuredLoop*>& consumer_loops,
38
    const symbolic::Assumptions& producer_assumptions,
39
    const symbolic::Assumptions& consumer_assumptions,
40
    bool invert_range_check
41
) {
33✔
42
    // Delinearize subsets to recover multi-dimensional structure from linearized accesses
43
    // e.g. T[i*N + j] with assumptions on bounds -> T[i, j]
44
    auto producer_sub = producer_subset;
33✔
45
    if (producer_sub.size() == 1) {
33✔
46
        auto producer_result = symbolic::delinearize(producer_sub.at(0), producer_assumptions);
24✔
47
        if (producer_result.success) {
24✔
48
            producer_sub = producer_result.indices;
21✔
49
        }
21✔
50
    }
24✔
51
    auto consumer_sub = consumer_subset;
33✔
52
    if (consumer_sub.size() == 1) {
33✔
53
        auto consumer_result = symbolic::delinearize(consumer_sub.at(0), consumer_assumptions);
24✔
54
        if (consumer_result.success) {
24✔
55
            consumer_sub = consumer_result.indices;
21✔
56
        }
21✔
57
    }
24✔
58

59
    // Subset dimensions must match
60
    if (producer_sub.size() != consumer_sub.size()) {
33✔
61
        return {};
×
62
    }
×
63
    if (producer_sub.empty()) {
33✔
64
        return {};
×
65
    }
×
66

67
    // Extract producer indvars
68
    SymEngine::vec_sym producer_vars;
33✔
69
    for (auto* loop : producer_loops) {
42✔
70
        producer_vars.push_back(SymEngine::rcp_static_cast<const SymEngine::Symbol>(loop->indvar()));
42✔
71
    }
42✔
72

73
    // Step 1: Solve the linear equation system using SymEngine
74
    // System: producer_sub[d] - consumer_sub[d] = 0, for each dimension d
75
    // Solve for producer_vars in terms of consumer_vars and parameters
76
    SymEngine::vec_basic equations;
33✔
77
    for (size_t d = 0; d < producer_sub.size(); ++d) {
75✔
78
        equations.push_back(symbolic::sub(producer_sub.at(d), consumer_sub.at(d)));
42✔
79
    }
42✔
80

81
    // Need exactly as many equations as unknowns for a unique solution.
82
    // Underdetermined systems (e.g. linearized access with multiple loop vars)
83
    // cannot be uniquely solved and would crash linsolve.
84
    if (equations.size() != producer_vars.size()) {
33✔
85
        return {};
×
86
    }
×
87

88
    SymEngine::vec_basic solution;
33✔
89
    try {
33✔
90
        solution = SymEngine::linsolve(equations, producer_vars);
33✔
91
    } catch (...) {
33✔
92
        return {};
×
93
    }
×
94
    if (solution.size() != producer_vars.size()) {
33✔
95
        return {};
×
96
    }
×
97
    // Build consumer var set for atom validation
98
    symbolic::SymbolSet consumer_var_set;
33✔
99
    for (auto* loop : consumer_loops) {
42✔
100
        consumer_var_set.insert(loop->indvar());
42✔
101
    }
42✔
102

103
    std::vector<std::pair<symbolic::Symbol, symbolic::Expression>> mappings;
33✔
104
    for (size_t i = 0; i < producer_vars.size(); ++i) {
75✔
105
        auto& sol = solution[i];
42✔
106

107
        // Check for invalid solutions
108
        if (SymEngine::is_a<SymEngine::NaN>(*sol) || SymEngine::is_a<SymEngine::Infty>(*sol)) {
42✔
109
            return {};
×
110
        }
×
111

112
        // Validate that solution atoms are consumer vars or parameters
113
        for (const auto& atom : symbolic::atoms(sol)) {
44✔
114
            if (consumer_var_set.count(atom)) {
44✔
115
                continue;
44✔
116
            }
44✔
117
            bool is_param = false;
×
118
            auto it = consumer_assumptions.find(atom);
×
119
            if (it != consumer_assumptions.end() && it->second.constant()) {
×
120
                is_param = true;
×
121
            }
×
122
            if (!is_param) {
×
123
                it = producer_assumptions.find(atom);
×
124
                if (it != producer_assumptions.end() && it->second.constant()) {
×
125
                    is_param = true;
×
126
                }
×
127
            }
×
128
            if (!is_param) {
×
129
                return {};
×
130
            }
×
131
        }
×
132

133
        mappings.push_back({symbolic::symbol(producer_vars[i]->get_name()), symbolic::expand(sol)});
42✔
134
    }
42✔
135
    // Step 2: ISL integrality validation via map composition
136
    // Build an unconstrained producer access map (no domain bounds on producer vars).
137
    // In map fusion, the producer's computation is inlined into the consumer, so
138
    // the producer's original iteration domain is irrelevant. We only need to verify
139
    // that the equation system has an INTEGER solution for every consumer point.
140
    symbolic::Assumptions unconstrained_producer;
33✔
141
    for (auto* loop : producer_loops) {
42✔
142
        symbolic::Assumption a(loop->indvar());
42✔
143
        a.constant(false);
42✔
144
        unconstrained_producer[loop->indvar()] = a;
42✔
145
    }
42✔
146
    for (const auto& [sym, assump] : producer_assumptions) {
126✔
147
        if (assump.constant() && unconstrained_producer.find(sym) == unconstrained_producer.end()) {
126✔
148
            unconstrained_producer[sym] = assump;
42✔
149
        }
42✔
150
    }
126✔
151

152
    std::string producer_map_str = symbolic::expression_to_map_str(producer_sub, unconstrained_producer);
33✔
153
    // Build consumer access map with full domain constraints
154
    std::string consumer_map_str = symbolic::expression_to_map_str(consumer_sub, consumer_assumptions);
33✔
155

156
    isl_ctx* ctx = isl_ctx_alloc();
33✔
157
    isl_options_set_on_error(ctx, ISL_ON_ERROR_CONTINUE);
33✔
158

159
    isl_map* producer_map = isl_map_read_from_str(ctx, producer_map_str.c_str());
33✔
160
    isl_map* consumer_map = isl_map_read_from_str(ctx, consumer_map_str.c_str());
33✔
161

162
    if (!producer_map || !consumer_map) {
33✔
163
        if (producer_map) isl_map_free(producer_map);
×
164
        if (consumer_map) isl_map_free(consumer_map);
×
165
        isl_ctx_free(ctx);
×
166
        return {};
×
167
    }
×
168

169
    // Align parameters between the two maps
170
    isl_space* params_p = isl_space_params(isl_map_get_space(producer_map));
33✔
171
    isl_space* params_c = isl_space_params(isl_map_get_space(consumer_map));
33✔
172
    isl_space* unified = isl_space_align_params(isl_space_copy(params_p), isl_space_copy(params_c));
33✔
173
    isl_space_free(params_p);
33✔
174
    isl_space_free(params_c);
33✔
175

176
    producer_map = isl_map_align_params(producer_map, isl_space_copy(unified));
33✔
177
    consumer_map = isl_map_align_params(consumer_map, isl_space_copy(unified));
33✔
178

179
    // Save consumer domain before consuming consumer_map in composition
180
    isl_set* consumer_domain = isl_map_domain(isl_map_copy(consumer_map));
33✔
181

182
    // Compute composition: consumer_access ∘ inverse(producer_access)
183
    // This checks whether the equation system producer_subset = consumer_subset
184
    // has an integer solution for each consumer domain point.
185
    isl_map* producer_inverse = isl_map_reverse(producer_map);
33✔
186
    isl_map* composition = isl_map_apply_range(consumer_map, producer_inverse);
33✔
187

188
    // Check single-valuedness: each consumer point maps to at most one producer point
189
    bool single_valued = isl_map_is_single_valued(composition) == isl_bool_true;
33✔
190

191
    // Check domain coverage: every consumer point has a valid integer mapping
192
    isl_set* comp_domain = isl_map_domain(composition);
33✔
193

194
    bool domain_covered = isl_set_is_subset(consumer_domain, comp_domain) == isl_bool_true;
33✔
195

196
    isl_set_free(comp_domain);
33✔
197
    isl_set_free(consumer_domain);
33✔
198

199
    // Step 3: Verify producer write range covers consumer read range.
200
    // The producer only writes a subset of the array if its loops have restricted bounds.
201
    // Fusion is invalid if the consumer reads elements the producer never writes.
202
    bool range_covered = false;
33✔
203
    if (single_valued && domain_covered) {
33✔
204
        std::string constrained_producer_map_str = symbolic::expression_to_map_str(producer_sub, producer_assumptions);
31✔
205
        isl_map* constrained_producer = isl_map_read_from_str(ctx, constrained_producer_map_str.c_str());
31✔
206
        isl_map* consumer_map_copy = isl_map_read_from_str(ctx, consumer_map_str.c_str());
31✔
207

208
        if (constrained_producer && consumer_map_copy) {
31✔
209
            constrained_producer = isl_map_align_params(constrained_producer, isl_space_copy(unified));
31✔
210
            consumer_map_copy = isl_map_align_params(consumer_map_copy, isl_space_copy(unified));
31✔
211

212
            isl_set* producer_range = isl_map_range(constrained_producer);
31✔
213
            isl_set* consumer_range = isl_map_range(consumer_map_copy);
31✔
214

215
            // When arguments are swapped (ConsumerIntoProducer), the "producer"/"consumer"
216
            // labels are inverted. Flip the subset check to always verify:
217
            // actual_consumer_read_range ⊆ actual_producer_write_range
218
            if (invert_range_check) {
31✔
219
                range_covered = isl_set_is_subset(producer_range, consumer_range) == isl_bool_true;
4✔
220
            } else {
27✔
221
                range_covered = isl_set_is_subset(consumer_range, producer_range) == isl_bool_true;
27✔
222
            }
27✔
223

224
            isl_set_free(producer_range);
31✔
225
            isl_set_free(consumer_range);
31✔
226
        } else {
31✔
227
            if (constrained_producer) isl_map_free(constrained_producer);
×
228
            if (consumer_map_copy) isl_map_free(consumer_map_copy);
×
229
        }
×
230
    }
31✔
231

232
    isl_space_free(unified);
33✔
233
    isl_ctx_free(ctx);
33✔
234

235
    if (!single_valued || !domain_covered || !range_covered) {
33✔
236
        return {};
5✔
237
    }
5✔
238

239
    return mappings;
28✔
240
}
33✔
241

242
bool MapFusion::find_write_location(
243
    structured_control_flow::StructuredLoop& loop,
244
    const std::string& container,
245
    std::vector<structured_control_flow::StructuredLoop*>& loops,
246
    structured_control_flow::Sequence*& body,
247
    structured_control_flow::Block*& block
248
) {
4✔
249
    loops.push_back(&loop);
4✔
250
    auto& seq = loop.root();
4✔
251

252
    for (size_t i = 0; i < seq.size(); ++i) {
10✔
253
        auto& child = seq.at(i).first;
6✔
254

255
        if (auto* blk = dynamic_cast<structured_control_flow::Block*>(&child)) {
6✔
256
            // Check if this block writes to the container
257
            auto& dataflow = blk->dataflow();
4✔
258
            for (auto& node : dataflow.nodes()) {
14✔
259
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
14✔
260
                if (access == nullptr || access->data() != container) {
14✔
261
                    continue;
12✔
262
                }
12✔
263
                // Write access: has incoming edges (sink node)
264
                if (dataflow.in_degree(*access) > 0 && dataflow.out_degree(*access) == 0) {
2✔
265
                    if (block != nullptr) {
2✔
266
                        // Multiple write blocks found — ambiguous
267
                        return false;
×
268
                    }
×
269
                    body = &seq;
2✔
270
                    block = blk;
2✔
271
                }
2✔
272
            }
2✔
273
        } else if (auto* nested_loop = dynamic_cast<structured_control_flow::StructuredLoop*>(&child)) {
4✔
274
            if (!find_write_location(*nested_loop, container, loops, body, block)) {
2✔
275
                return false;
×
276
            }
×
277
            // If we didn't find the write in this subtree, pop the loop back off
278
            if (loops.back() != &loop && block == nullptr) {
2✔
279
                // The recursive call already popped — but we need to check
280
            }
×
281
        }
2✔
282
    }
6✔
283

284
    // If we didn't find the write in this subtree, remove this loop from the chain
285
    if (block == nullptr) {
4✔
286
        loops.pop_back();
×
287
    }
×
288

289
    return true;
4✔
290
}
4✔
291

292
bool MapFusion::find_read_location(
293
    structured_control_flow::StructuredLoop& loop,
294
    const std::string& container,
295
    std::vector<structured_control_flow::StructuredLoop*>& loops,
296
    structured_control_flow::Sequence*& body
297
) {
2✔
298
    loops.push_back(&loop);
2✔
299
    auto& seq = loop.root();
2✔
300

301
    for (size_t i = 0; i < seq.size(); ++i) {
5✔
302
        auto& child = seq.at(i).first;
3✔
303

304
        if (auto* blk = dynamic_cast<structured_control_flow::Block*>(&child)) {
3✔
305
            // Check if this block reads from the container
306
            auto& dataflow = blk->dataflow();
2✔
307
            for (auto& node : dataflow.nodes()) {
8✔
308
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
8✔
309
                if (access == nullptr || access->data() != container) {
8✔
310
                    continue;
7✔
311
                }
7✔
312
                // Read access: has outgoing edges (source node)
313
                if (dataflow.in_degree(*access) == 0 && dataflow.out_degree(*access) > 0) {
1✔
314
                    if (body != nullptr && body != &seq) {
1✔
315
                        // Reads at different sequence levels — ambiguous
316
                        return false;
×
317
                    }
×
318
                    body = &seq;
1✔
319
                }
1✔
320
            }
1✔
321
        } else if (auto* nested_loop = dynamic_cast<structured_control_flow::StructuredLoop*>(&child)) {
2✔
322
            if (!find_read_location(*nested_loop, container, loops, body)) {
1✔
323
                return false;
×
324
            }
×
325
        }
1✔
326
    }
3✔
327

328
    // If we didn't find any reads in this subtree, remove this loop from the chain
329
    if (body == nullptr) {
2✔
330
        loops.pop_back();
×
331
    }
×
332

333
    return true;
2✔
334
}
2✔
335

336
bool MapFusion::can_be_applied(builder::StructuredSDFGBuilder& builder, analysis::AnalysisManager& analysis_manager) {
39✔
337
    fusion_candidates_.clear();
39✔
338

339
    // no use in fusing empty loops. Also presumed to not be empty further down
340
    if (first_map_.root().size() == 0 || second_loop_.root().size() == 0) {
39✔
341
        return false;
×
342
    }
×
343

344
    // Criterion: Get parent scope and verify both loops are sequential children
345
    auto& scope_analysis = analysis_manager.get<analysis::ScopeAnalysis>();
39✔
346
    auto* first_parent = scope_analysis.parent_scope(&first_map_);
39✔
347
    auto* second_parent = scope_analysis.parent_scope(&second_loop_);
39✔
348
    if (first_parent == nullptr || second_parent == nullptr) {
39✔
349
        return false;
×
350
    }
×
351
    if (first_parent != second_parent) {
39✔
352
        return false;
×
353
    }
×
354

355
    auto* parent_sequence = dynamic_cast<structured_control_flow::Sequence*>(first_parent);
39✔
356
    if (parent_sequence == nullptr) {
39✔
357
        return false;
×
358
    }
×
359

360
    int first_index = parent_sequence->index(first_map_);
39✔
361
    int second_index = parent_sequence->index(second_loop_);
39✔
362
    if (first_index == -1 || second_index == -1) {
39✔
363
        return false;
×
364
    }
×
365
    if (require_consecutive_ && second_index != first_index + 1) {
39✔
366
        return false;
1✔
367
    }
1✔
368

369
    // Criterion: Transition between maps should have no assignments
370
    if (require_consecutive_) {
38✔
371
        auto& transition = parent_sequence->at(first_index).second;
38✔
372
        if (!transition.empty()) {
38✔
373
            return false;
×
374
        }
×
375
    }
38✔
376
    // Determine fusion pattern based on nesting properties
377
    auto& loop_analysis = analysis_manager.get<analysis::LoopAnalysis>();
38✔
378
    auto first_loop_info = loop_analysis.loop_info(&first_map_);
38✔
379
    auto second_loop_info = loop_analysis.loop_info(&second_loop_);
38✔
380

381
    bool first_nested = first_loop_info.is_perfectly_nested;
38✔
382
    bool second_nested = second_loop_info.is_perfectly_nested;
38✔
383

384
    // Both non-perfectly-nested: not supported
385
    if (!first_nested && !second_nested) {
38✔
386
        return false;
1✔
387
    }
1✔
388

389
    if (first_nested && second_nested) {
37✔
390
        // Pattern 1: Both perfectly nested — producer into consumer (original path)
391
        direction_ = FusionDirection::ProducerIntoConsumer;
34✔
392
    } else if (!first_nested && second_nested) {
34✔
393
        // Pattern 2: Producer non-perfectly-nested, consumer perfectly nested
394
        direction_ = FusionDirection::ConsumerIntoProducer;
2✔
395
    } else {
2✔
396
        // Reverse Pattern 2: Producer perfectly nested, consumer non-perfectly-nested
397
        direction_ = FusionDirection::ProducerIntoConsumer;
1✔
398
    }
1✔
399

400
    // Locate producer write point
401
    producer_loops_.clear();
37✔
402
    producer_body_ = nullptr;
37✔
403
    producer_block_ = nullptr;
37✔
404

405
    if (first_nested) {
37✔
406
        // Perfectly nested: walk the at(0).first chain
407
        producer_loops_.push_back(&first_map_);
35✔
408
        producer_body_ = &first_map_.root();
35✔
409
        structured_control_flow::ControlFlowNode* node = &first_map_.root().at(0).first;
35✔
410
        while (auto* nested = dynamic_cast<structured_control_flow::StructuredLoop*>(node)) {
44✔
411
            producer_loops_.push_back(nested);
10✔
412
            producer_body_ = &nested->root();
10✔
413
            if (nested->root().size() == 0) return false;
10✔
414
            node = &nested->root().at(0).first;
9✔
415
        }
9✔
416
        producer_block_ = dynamic_cast<structured_control_flow::Block*>(node);
34✔
417
        if (producer_block_ == nullptr) {
34✔
418
            return false;
×
419
        }
×
420
        // If the body has multiple children, the at(0) walk does not guarantee
421
        // we found the correct (or unique) write block. Fall back to deferred
422
        // find_write_location resolution.
423
        if (producer_body_->size() != 1) {
34✔
424
            producer_block_ = nullptr;
×
425
            // Keep producer_loops_ and producer_body_ from the walk — they are
426
            // still valid for the loop chain. find_write_location will re-resolve
427
            // the block within producer_body_.
428
        }
×
429
    } else {
34✔
430
        // Non-perfectly-nested: search recursively for the write block
431
        // We need to know which containers to look for, but we don't know them yet.
432
        // Defer write location search until after fusion_containers are identified.
433
    }
2✔
434

435
    // Locate consumer read point
436
    consumer_loops_.clear();
36✔
437
    consumer_body_ = nullptr;
36✔
438

439
    if (second_nested) {
36✔
440
        // Perfectly nested: walk the at(0).first chain through all loop types.
441
        // Reduction patterns (e.g. Map{Map{For{T[i,j]+=...}}}) are rejected by
442
        // the is_perfectly_parallel check — For loops make it non-parallel.
443
        consumer_loops_.push_back(&second_loop_);
35✔
444
        consumer_body_ = &second_loop_.root();
35✔
445
        structured_control_flow::ControlFlowNode* node = &second_loop_.root().at(0).first;
35✔
446
        while (auto* nested = dynamic_cast<structured_control_flow::StructuredLoop*>(node)) {
45✔
447
            consumer_loops_.push_back(nested);
11✔
448
            consumer_body_ = &nested->root();
11✔
449
            if (nested->root().size() == 0) return false;
11✔
450
            node = &nested->root().at(0).first;
10✔
451
        }
10✔
452
    } else {
35✔
453
        // Non-perfectly-nested: defer read location search until after fusion_containers are identified.
454
    }
1✔
455

456
    // Get arguments analysis to identify inputs/outputs of each loop
457
    auto& arguments_analysis = analysis_manager.get<analysis::ArgumentsAnalysis>();
35✔
458
    auto first_args = arguments_analysis.arguments(analysis_manager, first_map_);
35✔
459
    auto second_args = arguments_analysis.arguments(analysis_manager, second_loop_);
35✔
460

461
    std::unordered_set<std::string> first_inputs;
35✔
462
    std::unordered_set<std::string> first_outputs;
35✔
463
    for (const auto& [name, arg] : first_args) {
114✔
464
        if (arg.is_output) {
114✔
465
            first_outputs.insert(name);
36✔
466
        }
36✔
467
        if (arg.is_input) {
114✔
468
            first_inputs.insert(name);
114✔
469
        }
114✔
470
    }
114✔
471

472
    // First pass: identify fusion containers (producer writes, consumer reads)
473
    std::unordered_set<std::string> fusion_containers;
35✔
474
    for (const auto& [name, arg] : second_args) {
120✔
475
        if (first_outputs.contains(name) && arg.is_input) {
120✔
476
            fusion_containers.insert(name);
34✔
477
        }
34✔
478
    }
120✔
479
    if (fusion_containers.empty()) {
35✔
480
        return false;
1✔
481
    }
1✔
482

483
    // Second pass: check for conflicts on non-fusion containers
484
    for (const auto& [name, arg] : second_args) {
115✔
485
        bool is_fusion = fusion_containers.contains(name);
115✔
486
        if (first_outputs.contains(name) && arg.is_output && !is_fusion) {
115✔
487
            return false;
×
488
        }
×
489
        if (first_inputs.contains(name) && arg.is_output && !is_fusion) {
115✔
490
            return false;
1✔
491
        }
1✔
492
    }
115✔
493

494
    // The side being inlined must be all-parallel (all Maps) so iterations can be reordered.
495
    // ProducerIntoConsumer: both sides must be all-parallel. The producer is replicated at
496
    // each consumer site — it must be reorderable. The consumer must also be all-parallel
497
    // because a sequential (For) loop would re-execute the inlined producer on every
498
    // iteration (e.g. init T=0 fused into For(k){T+=A[k]} re-initializes each k).
499
    // ConsumerIntoProducer: only the consumer (inlined side) must be all-parallel.
500
    if (direction_ == FusionDirection::ProducerIntoConsumer) {
33✔
501
        if (!first_loop_info.is_perfectly_parallel || !second_loop_info.is_perfectly_parallel) {
31✔
502
            return false;
2✔
503
        }
2✔
504
    } else {
31✔
505
        if (!second_loop_info.is_perfectly_parallel) {
2✔
506
            return false;
×
507
        }
×
508
    }
2✔
509

510
    // Now that we know the fusion containers, resolve deferred locations
511
    if (producer_block_ == nullptr) {
31✔
512
        // Non-perfectly-nested producer (or perfectly-nested with multi-block body):
513
        // find write location for the first fusion container.
514
        // All fusion containers must be written at the same block for this to work.
515
        for (const auto& container : fusion_containers) {
2✔
516
            std::vector<structured_control_flow::StructuredLoop*> write_loops;
2✔
517
            structured_control_flow::Sequence* write_body = nullptr;
2✔
518
            structured_control_flow::Block* write_block = nullptr;
2✔
519

520
            if (!find_write_location(first_map_, container, write_loops, write_body, write_block)) {
2✔
521
                return false;
×
522
            }
×
523
            if (write_block == nullptr) {
2✔
524
                return false;
×
525
            }
×
526

527
            if (producer_block_ == nullptr) {
2✔
528
                // First container: set the locations
529
                producer_loops_ = write_loops;
2✔
530
                producer_body_ = write_body;
2✔
531
                producer_block_ = write_block;
2✔
532
            } else {
2✔
533
                // Subsequent containers must be in the same block
534
                if (write_block != producer_block_) {
×
535
                    return false;
×
536
                }
×
537
            }
×
538
        }
2✔
539
    }
2✔
540

541
    if (!second_nested) {
31✔
542
        // Non-perfectly-nested consumer: find read location for the first fusion container
543
        // All fusion containers must be read at the same sequence for this to work
544
        for (const auto& container : fusion_containers) {
1✔
545
            std::vector<structured_control_flow::StructuredLoop*> read_loops;
1✔
546
            structured_control_flow::Sequence* read_body = nullptr;
1✔
547

548
            if (!find_read_location(second_loop_, container, read_loops, read_body)) {
1✔
549
                return false;
×
550
            }
×
551
            if (read_body == nullptr) {
1✔
552
                return false;
×
553
            }
×
554

555
            if (consumer_body_ == nullptr) {
1✔
556
                // First container: set the locations
557
                consumer_loops_ = read_loops;
1✔
558
                consumer_body_ = read_body;
1✔
559
            } else {
1✔
560
                // Subsequent containers must be at the same sequence
561
                if (read_body != consumer_body_) {
×
562
                    return false;
×
563
                }
×
564
            }
×
565
        }
1✔
566
    }
1✔
567

568
    // Get assumptions for the resolved write/read locations
569
    // Include trivial bounds from types to help delinearization with symbolic strides
570
    auto& assumptions_analysis = analysis_manager.get<analysis::AssumptionsAnalysis>();
31✔
571
    auto& producer_assumptions = assumptions_analysis.get(*producer_block_, true);
31✔
572
    auto& consumer_assumptions = assumptions_analysis.get(consumer_body_->at(0).first, true);
31✔
573

574
    // Check if producer actually reads a fusion container in the dataflow.
575
    // If so, ProducerIntoConsumer is unsafe (original producer loop mutates the array
576
    // before the inlined copy reads it). Force ConsumerIntoProducer.
577
    // We check the dataflow directly rather than ArgumentsAnalysis, because the latter
578
    // conservatively marks written containers as also read.
579
    if (direction_ == FusionDirection::ProducerIntoConsumer) {
31✔
580
        auto& first_dataflow_check = producer_block_->dataflow();
29✔
581
        bool producer_reads_fusion = false;
29✔
582
        for (const auto& container : fusion_containers) {
29✔
583
            for (auto& node : first_dataflow_check.nodes()) {
92✔
584
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
92✔
585
                if (access != nullptr && access->data() == container && first_dataflow_check.out_degree(*access) > 0) {
92✔
586
                    producer_reads_fusion = true;
2✔
587
                    break;
2✔
588
                }
2✔
589
            }
92✔
590
            if (producer_reads_fusion) break;
29✔
591
        }
29✔
592
        if (producer_reads_fusion) {
29✔
593
            direction_ = FusionDirection::ConsumerIntoProducer;
2✔
594
            // Re-check: consumer must be all-parallel for ConsumerIntoProducer
595
            if (!second_loop_info.is_perfectly_parallel) {
2✔
596
                return false;
×
597
            }
×
598
        }
2✔
599
    }
29✔
600

601
    // ProducerIntoConsumer only deep-copies producer_block_ into the consumer body.
602
    // If the producer body has multiple blocks (e.g. from prior BlockFusion merging
603
    // a previous fusion's writeback + inlined blocks), the write block may depend on
604
    // intermediates produced by earlier blocks that would NOT be copied. Reject.
605
    if (direction_ == FusionDirection::ProducerIntoConsumer && producer_body_->size() > 1) {
31✔
606
        return false;
×
607
    }
×
608

609
    // For each fusion container, find the producer memlet and collect unique consumer subsets
610
    auto& first_dataflow = producer_block_->dataflow();
31✔
611
    for (const auto& container : fusion_containers) {
31✔
612
        // Find unique producer write in first map
613
        data_flow::Memlet* producer_memlet = nullptr;
31✔
614

615
        for (auto& node : first_dataflow.nodes()) {
99✔
616
            auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
99✔
617
            if (access == nullptr || access->data() != container) {
99✔
618
                continue;
66✔
619
            }
66✔
620
            // Skip read-only access nodes (producer reads the fusion container)
621
            if (first_dataflow.in_degree(*access) == 0) {
33✔
622
                continue;
2✔
623
            }
2✔
624
            // Write access: must have exactly one incoming edge and no outgoing
625
            if (first_dataflow.in_degree(*access) != 1 || first_dataflow.out_degree(*access) != 0) {
31✔
626
                return false;
×
627
            }
×
628
            auto& iedge = *first_dataflow.in_edges(*access).begin();
31✔
629
            if (iedge.type() != data_flow::MemletType::Computational) {
31✔
630
                return false;
×
631
            }
×
632
            if (producer_memlet != nullptr) {
31✔
633
                return false;
×
634
            }
×
635
            producer_memlet = &iedge;
31✔
636
        }
31✔
637
        if (producer_memlet == nullptr) {
31✔
638
            return false;
×
639
        }
×
640

641
        const auto& producer_subset = producer_memlet->subset();
31✔
642
        if (producer_subset.empty()) {
31✔
643
            return false;
×
644
        }
×
645

646
        // Collect all unique subsets from consumer blocks
647
        std::vector<data_flow::Subset> unique_subsets;
31✔
648
        for (size_t i = 0; i < consumer_body_->size(); ++i) {
62✔
649
            auto* block = dynamic_cast<structured_control_flow::Block*>(&consumer_body_->at(i).first);
32✔
650
            if (block == nullptr) {
32✔
651
                // Skip non-block children (e.g. nested loops that are not related)
652
                continue;
×
653
            }
×
654

655
            auto& dataflow = block->dataflow();
32✔
656
            for (auto& node : dataflow.nodes()) {
112✔
657
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
112✔
658
                if (access == nullptr || access->data() != container) {
112✔
659
                    continue;
75✔
660
                }
75✔
661
                // Skip write-only access nodes (consumer also writes the fusion container)
662
                if (dataflow.in_degree(*access) > 0 && dataflow.out_degree(*access) == 0) {
37✔
663
                    continue;
2✔
664
                }
2✔
665
                if (dataflow.in_degree(*access) != 0 || dataflow.out_degree(*access) == 0) {
35✔
666
                    return false;
×
667
                }
×
668

669
                // Check all read memlets from this access
670
                for (auto& memlet : dataflow.out_edges(*access)) {
36✔
671
                    if (memlet.type() != data_flow::MemletType::Computational) {
36✔
672
                        return false;
×
673
                    }
×
674

675
                    auto& consumer_subset = memlet.subset();
36✔
676
                    if (consumer_subset.size() != producer_subset.size()) {
36✔
677
                        return false;
1✔
678
                    }
1✔
679

680
                    // Check if this subset is already in unique_subsets
681
                    bool found = false;
35✔
682
                    for (const auto& existing : unique_subsets) {
35✔
683
                        if (existing.size() != consumer_subset.size()) continue;
6✔
684
                        bool match = true;
6✔
685
                        for (size_t d = 0; d < existing.size(); ++d) {
8✔
686
                            if (!symbolic::eq(existing[d], consumer_subset[d])) {
6✔
687
                                match = false;
4✔
688
                                break;
4✔
689
                            }
4✔
690
                        }
6✔
691
                        if (match) {
6✔
692
                            found = true;
2✔
693
                            break;
2✔
694
                        }
2✔
695
                    }
6✔
696
                    if (!found) {
35✔
697
                        unique_subsets.push_back(consumer_subset);
33✔
698
                    }
33✔
699
                }
35✔
700
            }
35✔
701
        }
32✔
702

703
        // For each unique consumer subset, solve index mappings and create a FusionCandidate
704
        // The direction determines which side's indvars are solved for
705
        for (const auto& consumer_subset : unique_subsets) {
33✔
706
            std::vector<std::pair<symbolic::Symbol, symbolic::Expression>> mappings;
33✔
707

708
            if (direction_ == FusionDirection::ProducerIntoConsumer) {
33✔
709
                // Solve producer indvars in terms of consumer indvars
710
                mappings = solve_subsets(
29✔
711
                    producer_subset,
29✔
712
                    consumer_subset,
29✔
713
                    producer_loops_,
29✔
714
                    consumer_loops_,
29✔
715
                    producer_assumptions,
29✔
716
                    consumer_assumptions
29✔
717
                );
29✔
718
            } else {
29✔
719
                // ConsumerIntoProducer: solve consumer indvars in terms of producer indvars
720
                // Arguments are swapped, so invert the range check direction
721
                mappings = solve_subsets(
4✔
722
                    consumer_subset,
4✔
723
                    producer_subset,
4✔
724
                    consumer_loops_,
4✔
725
                    producer_loops_,
4✔
726
                    consumer_assumptions,
4✔
727
                    producer_assumptions,
4✔
728
                    true
4✔
729
                );
4✔
730
            }
4✔
731

732
            if (mappings.empty()) {
33✔
733
                return false;
5✔
734
            }
5✔
735

736
            FusionCandidate candidate;
28✔
737
            candidate.container = container;
28✔
738
            candidate.consumer_subset = consumer_subset;
28✔
739
            candidate.index_mappings = std::move(mappings);
28✔
740

741
            fusion_candidates_.push_back(candidate);
28✔
742
        }
28✔
743
    }
30✔
744

745
    // Criterion: At least one valid fusion candidate
746
    return !fusion_candidates_.empty();
25✔
747
}
31✔
748

749
void MapFusion::apply(builder::StructuredSDFGBuilder& builder, analysis::AnalysisManager& analysis_manager) {
17✔
750
    auto& sdfg = builder.subject();
17✔
751

752
    if (direction_ == FusionDirection::ProducerIntoConsumer) {
17✔
753
        // Pattern 1 + Reverse Pattern 2: Inline producer blocks into consumer's read body
754
        auto& first_dataflow = producer_block_->dataflow();
14✔
755

756
        // For each fusion candidate, create a temp and insert a producer block
757
        std::vector<std::string> candidate_temps;
14✔
758

759
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
29✔
760
            auto& candidate = fusion_candidates_[cand_idx];
15✔
761

762
            auto& container_type = sdfg.type(candidate.container);
15✔
763
            std::string temp_name = builder.find_new_name("_fused_tmp");
15✔
764
            types::Scalar tmp_type(container_type.primitive_type());
15✔
765
            builder.add_container(temp_name, tmp_type);
15✔
766
            candidate_temps.push_back(temp_name);
15✔
767

768
            // Insert a producer block at the beginning of the consumer's body
769
            auto& first_child = consumer_body_->at(0).first;
15✔
770
            control_flow::Assignments empty_assignments;
15✔
771
            auto& new_block = builder.add_block_before(*consumer_body_, first_child, empty_assignments);
15✔
772
            structured_control_flow::Block* empty_block = nullptr;
15✔
773

774
            // Deep copy all nodes from producer block to new block
775
            std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
15✔
776
            std::unordered_map<std::string, std::string> intermediate_renames;
15✔
777
            for (auto& node : first_dataflow.nodes()) {
48✔
778
                node_mapping[&node] = &builder.copy_node(new_block, node);
48✔
779
                auto* copied = node_mapping[&node];
48✔
780
                if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
48✔
781
                    if (access_node->data() == candidate.container) {
33✔
782
                        access_node->data(temp_name);
15✔
783
                    } else if (access_node->data() == first_map_.indvar()->get_name()) {
18✔
784
                        // Determine the new expression for the index variable of the first map
785
                        symbolic::Expression new_expr = SymEngine::null;
2✔
786
                        for (auto& c : fusion_candidates_) {
2✔
787
                            for (auto& [sym, expr] : c.index_mappings) {
2✔
788
                                if (symbolic::eq(sym, first_map_.indvar())) {
2✔
789
                                    new_expr = expr;
2✔
790
                                    break;
2✔
791
                                }
2✔
792
                            }
2✔
793
                            if (!new_expr.is_null()) {
2✔
794
                                break;
2✔
795
                            }
2✔
796
                        }
2✔
797

798
                        if (new_expr.is_null() || symbolic::eq(new_expr, second_loop_.indvar())) {
2✔
799
                            // Simple case: The new expression is simply the index variable of the second loop
800
                            access_node->data(second_loop_.indvar()->get_name());
1✔
801
                        } else {
1✔
802
                            // Complex case: Add an empty block before the new block (if necessary) and store the
803
                            // shifted index into a new temporary variable with an assignment. Then, replace the index
804
                            // variable with the new temporary variable
805
                            if (!empty_block) {
1✔
806
                                empty_block = &builder.add_block_before(*consumer_body_, new_block, empty_assignments);
1✔
807
                            }
1✔
808
                            auto new_index_name = builder.find_new_name();
1✔
809
                            builder
1✔
810
                                .add_container(new_index_name, builder.subject().type(second_loop_.indvar()->get_name()));
1✔
811
                            consumer_body_->at(0)
1✔
812
                                .second.assignments()
1✔
813
                                .insert({symbolic::symbol(new_index_name), new_expr});
1✔
814
                            access_node->data(new_index_name);
1✔
815
                        }
1✔
816
                    } else if (first_dataflow.in_degree(node) > 0 && first_dataflow.out_degree(node) > 0 &&
16✔
817
                               dynamic_cast<const types::Scalar*>(&sdfg.type(access_node->data())) != nullptr) {
16✔
818
                        // SSA Dataflow required to check for non-local use of the access node's container.
819
                        // Intermediate access node (e.g. from a prior BlockFusion): clone
820
                        // its container so each inlined copy gets its own private scalar
821
                        auto it = intermediate_renames.find(access_node->data());
×
822
                        if (it == intermediate_renames.end()) {
×
823
                            std::string fresh = builder.find_new_name(access_node->data());
×
824
                            builder.add_container(fresh, sdfg.type(access_node->data()));
×
825
                            intermediate_renames[access_node->data()] = fresh;
×
826
                        }
×
827
                        access_node->data(intermediate_renames[access_node->data()]);
×
828
                    }
×
829
                }
33✔
830
            }
48✔
831

832
            // Add memlets with index substitution (producer indvars → consumer expressions)
833
            for (auto& edge : first_dataflow.edges()) {
33✔
834
                auto& src_node = edge.src();
33✔
835
                auto& dst_node = edge.dst();
33✔
836

837
                const types::IType* base_type = &edge.base_type();
33✔
838
                data_flow::Subset new_subset;
33✔
839
                for (const auto& dim : edge.subset()) {
34✔
840
                    auto new_dim = dim;
34✔
841
                    for (const auto& [pvar, mapping] : candidate.index_mappings) {
46✔
842
                        new_dim = symbolic::subs(new_dim, pvar, mapping);
46✔
843
                    }
46✔
844
                    new_dim = symbolic::expand(new_dim);
34✔
845
                    new_subset.push_back(new_dim);
34✔
846
                }
34✔
847

848
                // For output edges to temp scalar, use empty subset
849
                auto* dst_access = dynamic_cast<data_flow::AccessNode*>(&dst_node);
33✔
850
                if (dst_access != nullptr && dst_access->data() == candidate.container &&
33✔
851
                    first_dataflow.in_degree(*dst_access) > 0) {
33✔
852
                    new_subset.clear();
15✔
853
                    base_type = &tmp_type;
15✔
854
                }
15✔
855

856
                builder.add_memlet(
33✔
857
                    new_block,
33✔
858
                    *node_mapping[&src_node],
33✔
859
                    edge.src_conn(),
33✔
860
                    *node_mapping[&dst_node],
33✔
861
                    edge.dst_conn(),
33✔
862
                    new_subset,
33✔
863
                    *base_type,
33✔
864
                    edge.debug_info()
33✔
865
                );
33✔
866
            }
33✔
867
        }
15✔
868

869
        // Update all read accesses in consumer blocks to point to the appropriate temp
870
        size_t num_producer_blocks = fusion_candidates_.size();
14✔
871

872
        for (size_t block_idx = num_producer_blocks; block_idx < consumer_body_->size(); ++block_idx) {
30✔
873
            auto* block = dynamic_cast<structured_control_flow::Block*>(&consumer_body_->at(block_idx).first);
16✔
874
            if (block == nullptr) {
16✔
875
                continue;
×
876
            }
×
877

878
            auto& dataflow = block->dataflow();
16✔
879

880
            for (auto& node : dataflow.nodes()) {
57✔
881
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
57✔
882
                if (access == nullptr || dataflow.out_degree(*access) == 0) {
57✔
883
                    continue;
34✔
884
                }
34✔
885

886
                std::string original_container = access->data();
23✔
887

888
                for (auto& memlet : dataflow.out_edges(*access)) {
24✔
889
                    if (memlet.type() != data_flow::MemletType::Computational) {
24✔
890
                        continue;
×
891
                    }
×
892

893
                    const auto& memlet_subset = memlet.subset();
24✔
894

895
                    for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
32✔
896
                        auto& candidate = fusion_candidates_[cand_idx];
25✔
897

898
                        if (original_container != candidate.container) {
25✔
899
                            continue;
7✔
900
                        }
7✔
901

902
                        if (memlet_subset.size() != candidate.consumer_subset.size()) {
18✔
903
                            continue;
×
904
                        }
×
905

906
                        bool subset_matches = true;
18✔
907
                        for (size_t d = 0; d < memlet_subset.size(); ++d) {
38✔
908
                            if (!symbolic::eq(memlet_subset[d], candidate.consumer_subset[d])) {
21✔
909
                                subset_matches = false;
1✔
910
                                break;
1✔
911
                            }
1✔
912
                        }
21✔
913

914
                        if (!subset_matches) {
18✔
915
                            continue;
1✔
916
                        }
1✔
917

918
                        const auto& temp_name = candidate_temps[cand_idx];
17✔
919
                        auto& temp_type = sdfg.type(temp_name);
17✔
920

921
                        access->data(temp_name);
17✔
922

923
                        memlet.set_subset({});
17✔
924
                        memlet.set_base_type(temp_type);
17✔
925

926
                        for (auto& in_edge : dataflow.in_edges(*access)) {
17✔
927
                            in_edge.set_subset({});
×
928
                            in_edge.set_base_type(temp_type);
×
929
                        }
×
930

931
                        break;
17✔
932
                    }
18✔
933
                }
24✔
934
            }
23✔
935
        }
16✔
936

937
    } else {
14✔
938
        // ConsumerIntoProducer (Pattern 2): Inline consumer blocks into the producer's write body
939
        // Modify the producer block in-place to write to a temp scalar, add a writeback block
940
        // for the original array, then copy consumer blocks reading from the temp.
941

942
        std::vector<std::string> candidate_temps;
3✔
943
        auto& producer_dataflow = producer_block_->dataflow();
3✔
944

945
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
6✔
946
            auto& candidate = fusion_candidates_[cand_idx];
3✔
947

948
            auto& container_type = sdfg.type(candidate.container);
3✔
949
            std::string temp_name = builder.find_new_name("_fused_tmp");
3✔
950
            types::Scalar tmp_type(container_type.primitive_type());
3✔
951
            builder.add_container(temp_name, tmp_type);
3✔
952
            candidate_temps.push_back(temp_name);
3✔
953

954
            // Step 1: Modify the original producer block to write to _fused_tmp
955
            data_flow::Subset original_write_subset;
3✔
956
            for (auto& node : producer_dataflow.nodes()) {
5✔
957
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
5✔
958
                if (access == nullptr || access->data() != candidate.container) continue;
5✔
959
                if (producer_dataflow.in_degree(*access) == 0) continue;
3✔
960

961
                // This is the write access node — save the original subset, then redirect
962
                for (auto& in_edge : producer_dataflow.in_edges(*access)) {
3✔
963
                    original_write_subset = in_edge.subset();
3✔
964
                    in_edge.set_subset({});
3✔
965
                    in_edge.set_base_type(tmp_type);
3✔
966
                }
3✔
967
                access->data(temp_name);
3✔
968
                break;
3✔
969
            }
3✔
970

971
            // Step 2: Add a writeback block: container[original_subset] = _fused_tmp
972
            control_flow::Assignments empty_assignments;
3✔
973
            auto& wb_block = builder.add_block_after(*producer_body_, *producer_block_, empty_assignments);
3✔
974
            auto& wb_src = builder.add_access(wb_block, temp_name);
3✔
975
            auto& wb_dst = builder.add_access(wb_block, candidate.container);
3✔
976
            auto& wb_tasklet = builder.add_tasklet(wb_block, data_flow::TaskletCode::assign, "_out", {"_in"});
3✔
977
            builder.add_computational_memlet(wb_block, wb_src, wb_tasklet, "_in", {});
3✔
978
            builder.add_computational_memlet(wb_block, wb_tasklet, "_out", wb_dst, original_write_subset);
3✔
979

980
            // Step 3: Copy consumer blocks after the writeback block
981
            structured_control_flow::ControlFlowNode* last_inserted = &wb_block;
3✔
982

983
            for (size_t i = 0; i < consumer_body_->size(); ++i) {
6✔
984
                auto* consumer_block = dynamic_cast<structured_control_flow::Block*>(&consumer_body_->at(i).first);
3✔
985
                if (consumer_block == nullptr) {
3✔
986
                    continue;
×
987
                }
×
988

989
                auto& consumer_dataflow = consumer_block->dataflow();
3✔
990

991
                // Check if this block reads from the fusion container
992
                bool reads_container = false;
3✔
993
                for (auto& node : consumer_dataflow.nodes()) {
11✔
994
                    auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
11✔
995
                    if (access != nullptr && access->data() == candidate.container &&
11✔
996
                        consumer_dataflow.out_degree(*access) > 0) {
11✔
997
                        reads_container = true;
3✔
998
                        break;
3✔
999
                    }
3✔
1000
                }
11✔
1001
                if (!reads_container) {
3✔
1002
                    continue;
×
1003
                }
×
1004

1005
                // Insert a new block after the last inserted block in the producer's body
1006
                auto& new_block = builder.add_block_after(*producer_body_, *last_inserted, empty_assignments);
3✔
1007
                structured_control_flow::Block* empty_block = nullptr;
3✔
1008

1009
                // Deep copy all nodes from consumer block
1010
                std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
3✔
1011
                std::unordered_map<std::string, std::string> intermediate_renames;
3✔
1012
                for (auto& node : consumer_dataflow.nodes()) {
11✔
1013
                    node_mapping[&node] = &builder.copy_node(new_block, node);
11✔
1014
                    auto* copied = node_mapping[&node];
11✔
1015
                    if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
11✔
1016
                        if (access_node->data() == candidate.container) {
8✔
1017
                            // Only rename read access nodes to temp; keep write access nodes
1018
                            // pointing to the original container
1019
                            if (consumer_dataflow.in_degree(node) == 0) {
4✔
1020
                                access_node->data(temp_name);
3✔
1021
                            }
3✔
1022
                        } else if (consumer_dataflow.in_degree(node) > 0 && consumer_dataflow.out_degree(node) > 0 &&
4✔
1023
                                   dynamic_cast<const types::Scalar*>(&sdfg.type(access_node->data())) != nullptr) {
4✔
1024
                            // SSA Dataflow required to check for non-local use of the access node's container.
1025
                            // Intermediate access node (e.g. from a prior BlockFusion): clone
1026
                            // its container so each inlined copy gets its own private scalar
1027
                            auto it = intermediate_renames.find(access_node->data());
×
1028
                            if (it == intermediate_renames.end()) {
×
1029
                                std::string fresh = builder.find_new_name(access_node->data());
×
1030
                                builder.add_container(fresh, sdfg.type(access_node->data()));
×
1031
                                intermediate_renames[access_node->data()] = fresh;
×
1032
                            }
×
1033
                            access_node->data(intermediate_renames[access_node->data()]);
×
1034
                        }
×
1035
                        if (access_node->data() == second_loop_.indvar()->get_name() &&
8✔
1036
                            consumer_dataflow.in_degree(node) == 0) {
8✔
1037
                            // Determine the new expression for the index variable of the second loop
NEW
1038
                            symbolic::Expression new_expr = SymEngine::null;
×
NEW
1039
                            for (auto& c : fusion_candidates_) {
×
NEW
1040
                                for (auto& [sym, expr] : c.index_mappings) {
×
NEW
1041
                                    if (symbolic::eq(sym, second_loop_.indvar())) {
×
NEW
1042
                                        new_expr = expr;
×
NEW
1043
                                        break;
×
NEW
1044
                                    }
×
NEW
1045
                                }
×
NEW
1046
                                if (!new_expr.is_null()) {
×
NEW
1047
                                    break;
×
NEW
1048
                                }
×
NEW
1049
                            }
×
1050

NEW
1051
                            if (new_expr.is_null() || symbolic::eq(new_expr, first_map_.indvar())) {
×
1052
                                // Simple case: The new expression is simply the index variable of the first map
NEW
1053
                                access_node->data(first_map_.indvar()->get_name());
×
NEW
1054
                            } else {
×
1055
                                // Complex case: Add an empty block before the new block (if necessary) and store the
1056
                                // shifted index into a new temporary variable with an assignment. Then, replace the
1057
                                // index variable with the new temporary variable
NEW
1058
                                if (!empty_block) {
×
NEW
1059
                                    empty_block =
×
NEW
1060
                                        &builder.add_block_before(*producer_body_, new_block, empty_assignments);
×
NEW
1061
                                }
×
NEW
1062
                                auto new_index_name = builder.find_new_name();
×
NEW
1063
                                builder.add_container(
×
NEW
1064
                                    new_index_name, builder.subject().type(first_map_.indvar()->get_name())
×
NEW
1065
                                );
×
NEW
1066
                                producer_body_->at(0)
×
NEW
1067
                                    .second.assignments()
×
NEW
1068
                                    .insert({symbolic::symbol(new_index_name), new_expr});
×
NEW
1069
                                access_node->data(new_index_name);
×
NEW
1070
                            }
×
NEW
1071
                        }
×
1072
                    }
8✔
1073
                }
11✔
1074

1075
                // Add memlets with index substitution (consumer indvars → producer expressions)
1076
                for (auto& edge : consumer_dataflow.edges()) {
8✔
1077
                    auto& src_node = edge.src();
8✔
1078
                    auto& dst_node = edge.dst();
8✔
1079

1080
                    const types::IType* base_type = &edge.base_type();
8✔
1081
                    data_flow::Subset new_subset;
8✔
1082
                    for (const auto& dim : edge.subset()) {
9✔
1083
                        auto new_dim = dim;
9✔
1084
                        for (const auto& [cvar, mapping] : candidate.index_mappings) {
13✔
1085
                            new_dim = symbolic::subs(new_dim, cvar, mapping);
13✔
1086
                        }
13✔
1087
                        new_dim = symbolic::expand(new_dim);
9✔
1088
                        new_subset.push_back(new_dim);
9✔
1089
                    }
9✔
1090

1091
                    // For read edges from temp scalar, use empty subset
1092
                    auto* src_access = dynamic_cast<data_flow::AccessNode*>(&src_node);
8✔
1093
                    if (src_access != nullptr && src_access->data() == candidate.container &&
8✔
1094
                        consumer_dataflow.in_degree(*src_access) == 0) {
8✔
1095
                        new_subset.clear();
3✔
1096
                        base_type = &tmp_type;
3✔
1097
                    }
3✔
1098

1099
                    builder.add_memlet(
8✔
1100
                        new_block,
8✔
1101
                        *node_mapping[&src_node],
8✔
1102
                        edge.src_conn(),
8✔
1103
                        *node_mapping[&dst_node],
8✔
1104
                        edge.dst_conn(),
8✔
1105
                        new_subset,
8✔
1106
                        *base_type,
8✔
1107
                        edge.debug_info()
8✔
1108
                    );
8✔
1109
                }
8✔
1110

1111
                last_inserted = &new_block;
3✔
1112
            }
3✔
1113
        }
3✔
1114

1115
        // Remove the consumer loop
1116
        auto& scope_analysis = analysis_manager.get<analysis::ScopeAnalysis>();
3✔
1117
        auto* parent = scope_analysis.parent_scope(&second_loop_);
3✔
1118
        auto* parent_seq = dynamic_cast<structured_control_flow::Sequence*>(parent);
3✔
1119
        if (parent_seq != nullptr) {
3✔
1120
            int idx = parent_seq->index(second_loop_);
3✔
1121
            if (idx >= 0) {
3✔
1122
                builder.remove_child(*parent_seq, static_cast<size_t>(idx));
3✔
1123
            }
3✔
1124
        }
3✔
1125
    }
3✔
1126

1127
    analysis_manager.invalidate_all();
17✔
1128
    applied_ = true;
17✔
1129
}
17✔
1130

1131
void MapFusion::to_json(nlohmann::json& j) const {
1✔
1132
    std::string second_type = "for";
1✔
1133
    if (dynamic_cast<structured_control_flow::Map*>(&second_loop_) != nullptr) {
1✔
1134
        second_type = "map";
1✔
1135
    }
1✔
1136
    j["transformation_type"] = this->name();
1✔
1137
    j["subgraph"] = {
1✔
1138
        {"0", {{"element_id", first_map_.element_id()}, {"type", "map"}}},
1✔
1139
        {"1", {{"element_id", second_loop_.element_id()}, {"type", second_type}}}
1✔
1140
    };
1✔
1141
}
1✔
1142

1143
MapFusion MapFusion::from_json(builder::StructuredSDFGBuilder& builder, const nlohmann::json& desc) {
1✔
1144
    auto first_map_id = desc["subgraph"]["0"]["element_id"].get<size_t>();
1✔
1145
    auto second_loop_id = desc["subgraph"]["1"]["element_id"].get<size_t>();
1✔
1146

1147
    auto first_element = builder.find_element_by_id(first_map_id);
1✔
1148
    auto second_element = builder.find_element_by_id(second_loop_id);
1✔
1149

1150
    if (first_element == nullptr) {
1✔
1151
        throw InvalidTransformationDescriptionException("Element with ID " + std::to_string(first_map_id) + " not found.");
×
1152
    }
×
1153
    if (second_element == nullptr) {
1✔
1154
        throw InvalidTransformationDescriptionException(
×
1155
            "Element with ID " + std::to_string(second_loop_id) + " not found."
×
1156
        );
×
1157
    }
×
1158

1159
    auto* first_map = dynamic_cast<structured_control_flow::Map*>(first_element);
1✔
1160
    auto* second_loop = dynamic_cast<structured_control_flow::StructuredLoop*>(second_element);
1✔
1161

1162
    if (first_map == nullptr) {
1✔
1163
        throw InvalidTransformationDescriptionException(
×
1164
            "Element with ID " + std::to_string(first_map_id) + " is not a Map."
×
1165
        );
×
1166
    }
×
1167
    if (second_loop == nullptr) {
1✔
1168
        throw InvalidTransformationDescriptionException(
×
1169
            "Element with ID " + std::to_string(second_loop_id) + " is not a StructuredLoop."
×
1170
        );
×
1171
    }
×
1172

1173
    return MapFusion(*first_map, *second_loop);
1✔
1174
}
1✔
1175

1176
} // namespace transformations
1177
} // 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