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

daisytuner / docc / 27378133903

11 Jun 2026 09:19PM UTC coverage: 61.387% (+0.04%) from 61.346%
27378133903

Pull #753

github

web-flow
Merge dc9a99725 into 7d4e149b0
Pull Request #753: fixes map fusion bug for stencil patterns with shared access nodes

67 of 70 new or added lines in 1 file covered. (95.71%)

1 existing line in 1 file now uncovered.

36356 of 59224 relevant lines covered (61.39%)

1118.59 hits per line

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

80.27
/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) {}
42✔
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
) {
35✔
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;
35✔
45
    if (producer_sub.size() == 1) {
35✔
46
        auto producer_result = symbolic::delinearize(producer_sub.at(0), producer_assumptions);
26✔
47
        if (producer_result.success) {
26✔
48
            producer_sub = producer_result.indices;
23✔
49
        }
23✔
50
    }
26✔
51
    auto consumer_sub = consumer_subset;
35✔
52
    if (consumer_sub.size() == 1) {
35✔
53
        auto consumer_result = symbolic::delinearize(consumer_sub.at(0), consumer_assumptions);
26✔
54
        if (consumer_result.success) {
26✔
55
            consumer_sub = consumer_result.indices;
23✔
56
        }
23✔
57
    }
26✔
58

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

67
    // Extract producer indvars
68
    SymEngine::vec_sym producer_vars;
35✔
69
    for (auto* loop : producer_loops) {
44✔
70
        producer_vars.push_back(SymEngine::rcp_static_cast<const SymEngine::Symbol>(loop->indvar()));
44✔
71
    }
44✔
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;
35✔
77
    for (size_t d = 0; d < producer_sub.size(); ++d) {
79✔
78
        equations.push_back(symbolic::sub(producer_sub.at(d), consumer_sub.at(d)));
44✔
79
    }
44✔
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()) {
35✔
85
        return {};
×
86
    }
×
87

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

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

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

112
        // Validate that solution atoms are consumer vars or parameters
113
        for (const auto& atom : symbolic::atoms(sol)) {
46✔
114
            if (consumer_var_set.count(atom)) {
46✔
115
                continue;
46✔
116
            }
46✔
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)});
44✔
134
    }
44✔
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;
35✔
141
    for (auto* loop : producer_loops) {
44✔
142
        symbolic::Assumption a(loop->indvar());
44✔
143
        a.constant(false);
44✔
144
        unconstrained_producer[loop->indvar()] = a;
44✔
145
    }
44✔
146
    for (const auto& [sym, assump] : producer_assumptions) {
132✔
147
        if (assump.constant() && unconstrained_producer.find(sym) == unconstrained_producer.end()) {
132✔
148
            unconstrained_producer[sym] = assump;
44✔
149
        }
44✔
150
    }
132✔
151

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

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

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

162
    if (!producer_map || !consumer_map) {
35✔
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));
35✔
171
    isl_space* params_c = isl_space_params(isl_map_get_space(consumer_map));
35✔
172
    isl_space* unified = isl_space_align_params(isl_space_copy(params_p), isl_space_copy(params_c));
35✔
173
    isl_space_free(params_p);
35✔
174
    isl_space_free(params_c);
35✔
175

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

179
    // Save consumer domain before consuming consumer_map in composition
180
    isl_set* consumer_domain = isl_map_domain(isl_map_copy(consumer_map));
35✔
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);
35✔
186
    isl_map* composition = isl_map_apply_range(consumer_map, producer_inverse);
35✔
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;
35✔
190

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

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

196
    isl_set_free(comp_domain);
35✔
197
    isl_set_free(consumer_domain);
35✔
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;
35✔
203
    if (single_valued && domain_covered) {
35✔
204
        std::string constrained_producer_map_str = symbolic::expression_to_map_str(producer_sub, producer_assumptions);
33✔
205
        isl_map* constrained_producer = isl_map_read_from_str(ctx, constrained_producer_map_str.c_str());
33✔
206
        isl_map* consumer_map_copy = isl_map_read_from_str(ctx, consumer_map_str.c_str());
33✔
207

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

212
            isl_set* producer_range = isl_map_range(constrained_producer);
33✔
213
            isl_set* consumer_range = isl_map_range(consumer_map_copy);
33✔
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) {
33✔
219
                range_covered = isl_set_is_subset(producer_range, consumer_range) == isl_bool_true;
4✔
220
            } else {
29✔
221
                range_covered = isl_set_is_subset(consumer_range, producer_range) == isl_bool_true;
29✔
222
            }
29✔
223

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

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

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

239
    return mappings;
30✔
240
}
35✔
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) {
40✔
337
    fusion_candidates_.clear();
40✔
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) {
40✔
341
        return false;
×
342
    }
×
343

344
    // Criterion: Get parent scope and verify both loops are sequential children
345
    auto* first_parent = first_map_.get_parent();
40✔
346
    auto* second_parent = second_loop_.get_parent();
40✔
347
    if (first_parent == nullptr || second_parent == nullptr) {
40✔
348
        return false;
×
349
    }
×
350
    if (first_parent != second_parent) {
40✔
351
        return false;
×
352
    }
×
353

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

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

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

380
    bool first_nested = first_loop_info.is_perfectly_nested;
39✔
381
    bool second_nested = second_loop_info.is_perfectly_nested;
39✔
382

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

740
            fusion_candidates_.push_back(candidate);
30✔
741
        }
30✔
742
    }
31✔
743

744
    // Criterion: At least one valid fusion candidate
745
    return !fusion_candidates_.empty();
26✔
746
}
32✔
747

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

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

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

758
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
32✔
759
            auto& candidate = fusion_candidates_[cand_idx];
17✔
760

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

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

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

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

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

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

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

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

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

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

877
            auto& dataflow = block->dataflow();
17✔
878

879
            // Snapshot access nodes before mutation: adding new access nodes below
880
            // would rehash dataflow.nodes_ and invalidate the range iterator.
881
            std::vector<data_flow::AccessNode*> access_nodes;
17✔
882
            for (auto& node : dataflow.nodes()) {
60✔
883
                auto* an = dynamic_cast<data_flow::AccessNode*>(&node);
60✔
884
                if (an != nullptr && dataflow.out_degree(*an) > 0) {
60✔
885
                    access_nodes.push_back(an);
24✔
886
                }
24✔
887
            }
60✔
888

889
            for (auto* access : access_nodes) {
24✔
890
                std::string original_container = access->data();
24✔
891

892
                // Match each out-edge against a fusion candidate.
893
                struct Match {
24✔
894
                    data_flow::Memlet* memlet;
24✔
895
                    size_t cand_idx;
24✔
896
                };
24✔
897
                std::vector<Match> matches;
24✔
898
                for (auto& memlet : dataflow.out_edges(*access)) {
26✔
899
                    if (memlet.type() != data_flow::MemletType::Computational) {
26✔
900
                        continue;
×
901
                    }
×
902
                    const auto& memlet_subset = memlet.subset();
26✔
903
                    for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
35✔
904
                        auto& candidate = fusion_candidates_[cand_idx];
28✔
905
                        if (original_container != candidate.container) {
28✔
906
                            continue;
7✔
907
                        }
7✔
908
                        if (memlet_subset.size() != candidate.consumer_subset.size()) {
21✔
UNCOV
909
                            continue;
×
910
                        }
×
911
                        bool subset_matches = true;
21✔
912
                        for (size_t d = 0; d < memlet_subset.size(); ++d) {
43✔
913
                            if (!symbolic::eq(memlet_subset[d], candidate.consumer_subset[d])) {
24✔
914
                                subset_matches = false;
2✔
915
                                break;
2✔
916
                            }
2✔
917
                        }
24✔
918
                        if (subset_matches) {
21✔
919
                            matches.push_back({&memlet, cand_idx});
19✔
920
                            break;
19✔
921
                        }
19✔
922
                    }
21✔
923
                }
26✔
924
                if (matches.empty()) {
24✔
925
                    continue;
7✔
926
                }
7✔
927

928
                // Group matches by candidate index.
929
                std::unordered_set<size_t> distinct_cands;
17✔
930
                for (auto& m : matches) {
19✔
931
                    distinct_cands.insert(m.cand_idx);
19✔
932
                }
19✔
933

934
                if (distinct_cands.size() == 1) {
17✔
935
                    // Fast path: all matched out-edges resolve to the same candidate.
936
                    // Mutate the shared access node in place — this preserves the
937
                    // existing semantics for the single-read-per-container case.
938
                    size_t cand_idx = *distinct_cands.begin();
16✔
939
                    const auto& temp_name = candidate_temps[cand_idx];
16✔
940
                    auto& temp_type = sdfg.type(temp_name);
16✔
941

942
                    access->data(temp_name);
16✔
943

944
                    for (auto& m : matches) {
17✔
945
                        m.memlet->set_subset({});
17✔
946
                        m.memlet->set_base_type(temp_type);
17✔
947
                    }
17✔
948

949
                    for (auto& in_edge : dataflow.in_edges(*access)) {
16✔
NEW
950
                        in_edge.set_subset({});
×
NEW
951
                        in_edge.set_base_type(temp_type);
×
NEW
952
                    }
×
953
                } else {
16✔
954
                    // Stencil-like case: a single access node feeds reads at
955
                    // multiple distinct subsets (e.g. T[j-1] and T[j+1] sharing
956
                    // one AccessNode). Each must be rewired to its own
957
                    // candidate-specific temp scalar — otherwise mutating
958
                    // `access->data()` once per candidate makes all reads
959
                    // collapse onto the last temp, e.g. T[j+1]-T[j] becomes
960
                    // tmp-tmp == 0.
961
                    //
962
                    // Fix: for each distinct candidate, create one fresh
963
                    // AccessNode for its temp scalar and redirect the matched
964
                    // edges from the shared access node to the fresh nodes.
965
                    struct PendingRedirect {
1✔
966
                        data_flow::DataFlowNode* dst;
1✔
967
                        std::string src_conn;
1✔
968
                        std::string dst_conn;
1✔
969
                        DebugInfo debug_info;
1✔
970
                        size_t cand_idx;
1✔
971
                        const data_flow::Memlet* memlet_to_remove;
1✔
972
                    };
1✔
973
                    std::vector<PendingRedirect> pending;
1✔
974
                    pending.reserve(matches.size());
1✔
975
                    for (auto& m : matches) {
2✔
976
                        pending.push_back(
2✔
977
                            {&m.memlet->dst(),
2✔
978
                             m.memlet->src_conn(),
2✔
979
                             m.memlet->dst_conn(),
2✔
980
                             m.memlet->debug_info(),
2✔
981
                             m.cand_idx,
2✔
982
                             m.memlet}
2✔
983
                        );
2✔
984
                    }
2✔
985

986
                    std::unordered_map<size_t, data_flow::AccessNode*> per_cand_node;
1✔
987
                    for (auto& p : pending) {
2✔
988
                        auto it = per_cand_node.find(p.cand_idx);
2✔
989
                        if (it == per_cand_node.end()) {
2✔
990
                            auto& fresh = builder.add_access(*block, candidate_temps[p.cand_idx]);
2✔
991
                            it = per_cand_node.emplace(p.cand_idx, &fresh).first;
2✔
992
                        }
2✔
993
                        auto& temp_type = sdfg.type(candidate_temps[p.cand_idx]);
2✔
994
                        builder.remove_memlet(*block, *p.memlet_to_remove);
2✔
995
                        builder
2✔
996
                            .add_memlet(*block, *it->second, p.src_conn, *p.dst, p.dst_conn, {}, temp_type, p.debug_info);
2✔
997
                    }
2✔
998

999
                    // If the original shared access node now has no edges at all
1000
                    // it is dangling and should be removed. Keep it if it still
1001
                    // has out-edges (unmatched reads of the original container)
1002
                    // or in-edges (writes to the original container).
1003
                    if (dataflow.out_degree(*access) == 0 && dataflow.in_degree(*access) == 0) {
1✔
1004
                        builder.remove_node(*block, *access);
1✔
1005
                    }
1✔
1006
                }
1✔
1007
            }
17✔
1008
        }
17✔
1009

1010
    } else {
15✔
1011
        // ConsumerIntoProducer (Pattern 2): Inline consumer blocks into the producer's write body
1012
        // Modify the producer block in-place to write to a temp scalar, add a writeback block
1013
        // for the original array, then copy consumer blocks reading from the temp.
1014

1015
        std::vector<std::string> candidate_temps;
3✔
1016
        auto& producer_dataflow = producer_block_->dataflow();
3✔
1017

1018
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
6✔
1019
            auto& candidate = fusion_candidates_[cand_idx];
3✔
1020

1021
            auto& container_type = sdfg.type(candidate.container);
3✔
1022
            std::string temp_name = builder.find_new_name("_fused_tmp");
3✔
1023
            types::Scalar tmp_type(container_type.primitive_type());
3✔
1024
            builder.add_container(temp_name, tmp_type);
3✔
1025
            candidate_temps.push_back(temp_name);
3✔
1026

1027
            // Step 1: Modify the original producer block to write to _fused_tmp
1028
            data_flow::Subset original_write_subset;
3✔
1029
            for (auto& node : producer_dataflow.nodes()) {
5✔
1030
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
5✔
1031
                if (access == nullptr || access->data() != candidate.container) continue;
5✔
1032
                if (producer_dataflow.in_degree(*access) == 0) continue;
3✔
1033

1034
                // This is the write access node — save the original subset, then redirect
1035
                for (auto& in_edge : producer_dataflow.in_edges(*access)) {
3✔
1036
                    original_write_subset = in_edge.subset();
3✔
1037
                    in_edge.set_subset({});
3✔
1038
                    in_edge.set_base_type(tmp_type);
3✔
1039
                }
3✔
1040
                access->data(temp_name);
3✔
1041
                break;
3✔
1042
            }
3✔
1043

1044
            // Step 2: Add a writeback block: container[original_subset] = _fused_tmp
1045
            control_flow::Assignments empty_assignments;
3✔
1046
            auto& wb_block = builder.add_block_after(*producer_body_, *producer_block_, empty_assignments);
3✔
1047
            auto& wb_src = builder.add_access(wb_block, temp_name);
3✔
1048
            auto& wb_dst = builder.add_access(wb_block, candidate.container);
3✔
1049
            auto& wb_tasklet = builder.add_tasklet(wb_block, data_flow::TaskletCode::assign, "_out", {"_in"});
3✔
1050
            builder.add_computational_memlet(wb_block, wb_src, wb_tasklet, "_in", {});
3✔
1051
            builder.add_computational_memlet(wb_block, wb_tasklet, "_out", wb_dst, original_write_subset);
3✔
1052

1053
            // Step 3: Copy consumer blocks after the writeback block
1054
            structured_control_flow::ControlFlowNode* last_inserted = &wb_block;
3✔
1055

1056
            for (size_t i = 0; i < consumer_body_->size(); ++i) {
6✔
1057
                auto* consumer_block = dynamic_cast<structured_control_flow::Block*>(&consumer_body_->at(i).first);
3✔
1058
                if (consumer_block == nullptr) {
3✔
1059
                    continue;
×
1060
                }
×
1061

1062
                auto& consumer_dataflow = consumer_block->dataflow();
3✔
1063

1064
                // Check if this block reads from the fusion container
1065
                bool reads_container = false;
3✔
1066
                for (auto& node : consumer_dataflow.nodes()) {
11✔
1067
                    auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
11✔
1068
                    if (access != nullptr && access->data() == candidate.container &&
11✔
1069
                        consumer_dataflow.out_degree(*access) > 0) {
11✔
1070
                        reads_container = true;
3✔
1071
                        break;
3✔
1072
                    }
3✔
1073
                }
11✔
1074
                if (!reads_container) {
3✔
1075
                    continue;
×
1076
                }
×
1077

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

1082
                // Deep copy all nodes from consumer block
1083
                std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
3✔
1084
                std::unordered_map<std::string, std::string> intermediate_renames;
3✔
1085
                for (auto& node : consumer_dataflow.nodes()) {
11✔
1086
                    node_mapping[&node] = &builder.copy_node(new_block, node);
11✔
1087
                    auto* copied = node_mapping[&node];
11✔
1088
                    if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
11✔
1089
                        if (access_node->data() == candidate.container) {
8✔
1090
                            // Only rename read access nodes to temp; keep write access nodes
1091
                            // pointing to the original container
1092
                            if (consumer_dataflow.in_degree(node) == 0) {
4✔
1093
                                access_node->data(temp_name);
3✔
1094
                            }
3✔
1095
                        } else if (consumer_dataflow.in_degree(node) > 0 && consumer_dataflow.out_degree(node) > 0 &&
4✔
1096
                                   dynamic_cast<const types::Scalar*>(&sdfg.type(access_node->data())) != nullptr) {
4✔
1097
                            // SSA Dataflow required to check for non-local use of the access node's container.
1098
                            // Intermediate access node (e.g. from a prior BlockFusion): clone
1099
                            // its container so each inlined copy gets its own private scalar
1100
                            auto it = intermediate_renames.find(access_node->data());
×
1101
                            if (it == intermediate_renames.end()) {
×
1102
                                std::string fresh = builder.find_new_name(access_node->data());
×
1103
                                builder.add_container(fresh, sdfg.type(access_node->data()));
×
1104
                                intermediate_renames[access_node->data()] = fresh;
×
1105
                            }
×
1106
                            access_node->data(intermediate_renames[access_node->data()]);
×
1107
                        }
×
1108
                        if (access_node->data() == second_loop_.indvar()->get_name() &&
8✔
1109
                            consumer_dataflow.in_degree(node) == 0) {
8✔
1110
                            // Determine the new expression for the index variable of the second loop
1111
                            symbolic::Expression new_expr = SymEngine::null;
×
1112
                            for (auto& c : fusion_candidates_) {
×
1113
                                for (auto& [sym, expr] : c.index_mappings) {
×
1114
                                    if (symbolic::eq(sym, second_loop_.indvar())) {
×
1115
                                        new_expr = expr;
×
1116
                                        break;
×
1117
                                    }
×
1118
                                }
×
1119
                                if (!new_expr.is_null()) {
×
1120
                                    break;
×
1121
                                }
×
1122
                            }
×
1123

1124
                            if (new_expr.is_null() || symbolic::eq(new_expr, first_map_.indvar())) {
×
1125
                                // Simple case: The new expression is simply the index variable of the first map
1126
                                access_node->data(first_map_.indvar()->get_name());
×
1127
                            } else {
×
1128
                                // Complex case: Add an empty block before the new block (if necessary) and store the
1129
                                // shifted index into a new temporary variable with an assignment. Then, replace the
1130
                                // index variable with the new temporary variable
1131
                                if (!empty_block) {
×
1132
                                    empty_block =
×
1133
                                        &builder.add_block_before(*producer_body_, new_block, empty_assignments);
×
1134
                                }
×
1135
                                auto new_index_name = builder.find_new_name();
×
1136
                                builder.add_container(
×
1137
                                    new_index_name, builder.subject().type(first_map_.indvar()->get_name())
×
1138
                                );
×
1139
                                producer_body_->at(0)
×
1140
                                    .second.assignments()
×
1141
                                    .insert({symbolic::symbol(new_index_name), new_expr});
×
1142
                                access_node->data(new_index_name);
×
1143
                            }
×
1144
                        }
×
1145
                    }
8✔
1146
                }
11✔
1147

1148
                // Add memlets with index substitution (consumer indvars → producer expressions)
1149
                for (auto& edge : consumer_dataflow.edges()) {
8✔
1150
                    auto& src_node = edge.src();
8✔
1151
                    auto& dst_node = edge.dst();
8✔
1152

1153
                    const types::IType* base_type = &edge.base_type();
8✔
1154
                    data_flow::Subset new_subset;
8✔
1155
                    for (const auto& dim : edge.subset()) {
9✔
1156
                        auto new_dim = dim;
9✔
1157
                        for (const auto& [cvar, mapping] : candidate.index_mappings) {
13✔
1158
                            new_dim = symbolic::subs(new_dim, cvar, mapping);
13✔
1159
                        }
13✔
1160
                        new_dim = symbolic::expand(new_dim);
9✔
1161
                        new_subset.push_back(new_dim);
9✔
1162
                    }
9✔
1163

1164
                    // For read edges from temp scalar, use empty subset
1165
                    auto* src_access = dynamic_cast<data_flow::AccessNode*>(&src_node);
8✔
1166
                    if (src_access != nullptr && src_access->data() == candidate.container &&
8✔
1167
                        consumer_dataflow.in_degree(*src_access) == 0) {
8✔
1168
                        new_subset.clear();
3✔
1169
                        base_type = &tmp_type;
3✔
1170
                    }
3✔
1171

1172
                    builder.add_memlet(
8✔
1173
                        new_block,
8✔
1174
                        *node_mapping[&src_node],
8✔
1175
                        edge.src_conn(),
8✔
1176
                        *node_mapping[&dst_node],
8✔
1177
                        edge.dst_conn(),
8✔
1178
                        new_subset,
8✔
1179
                        *base_type,
8✔
1180
                        edge.debug_info()
8✔
1181
                    );
8✔
1182
                }
8✔
1183

1184
                last_inserted = &new_block;
3✔
1185
            }
3✔
1186
        }
3✔
1187

1188
        // Remove the consumer loop
1189
        auto* parent = second_loop_.get_parent();
3✔
1190
        auto* parent_seq = dynamic_cast<structured_control_flow::Sequence*>(parent);
3✔
1191
        if (parent_seq != nullptr) {
3✔
1192
            int idx = parent_seq->index(second_loop_);
3✔
1193
            if (idx >= 0) {
3✔
1194
                builder.remove_child(*parent_seq, static_cast<size_t>(idx));
3✔
1195
            }
3✔
1196
        }
3✔
1197
    }
3✔
1198

1199
    analysis_manager.invalidate_all();
18✔
1200
    applied_ = true;
18✔
1201
}
18✔
1202

1203
void MapFusion::to_json(nlohmann::json& j) const {
1✔
1204
    std::string second_type = "for";
1✔
1205
    if (dynamic_cast<structured_control_flow::Map*>(&second_loop_) != nullptr) {
1✔
1206
        second_type = "map";
1✔
1207
    }
1✔
1208
    j["transformation_type"] = this->name();
1✔
1209
    j["subgraph"] = {
1✔
1210
        {"0", {{"element_id", first_map_.element_id()}, {"type", "map"}}},
1✔
1211
        {"1", {{"element_id", second_loop_.element_id()}, {"type", second_type}}}
1✔
1212
    };
1✔
1213
}
1✔
1214

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

1219
    auto first_element = builder.find_element_by_id(first_map_id);
1✔
1220
    auto second_element = builder.find_element_by_id(second_loop_id);
1✔
1221

1222
    if (first_element == nullptr) {
1✔
1223
        throw InvalidTransformationDescriptionException("Element with ID " + std::to_string(first_map_id) + " not found.");
×
1224
    }
×
1225
    if (second_element == nullptr) {
1✔
1226
        throw InvalidTransformationDescriptionException(
×
1227
            "Element with ID " + std::to_string(second_loop_id) + " not found."
×
1228
        );
×
1229
    }
×
1230

1231
    auto* first_map = dynamic_cast<structured_control_flow::Map*>(first_element);
1✔
1232
    auto* second_loop = dynamic_cast<structured_control_flow::StructuredLoop*>(second_element);
1✔
1233

1234
    if (first_map == nullptr) {
1✔
1235
        throw InvalidTransformationDescriptionException(
×
1236
            "Element with ID " + std::to_string(first_map_id) + " is not a Map."
×
1237
        );
×
1238
    }
×
1239
    if (second_loop == nullptr) {
1✔
1240
        throw InvalidTransformationDescriptionException(
×
1241
            "Element with ID " + std::to_string(second_loop_id) + " is not a StructuredLoop."
×
1242
        );
×
1243
    }
×
1244

1245
    return MapFusion(*first_map, *second_loop);
1✔
1246
}
1✔
1247

1248
} // namespace transformations
1249
} // 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