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

daisytuner / docc / 27220081254

09 Jun 2026 04:19PM UTC coverage: 61.275%. Remained the same
27220081254

Pull #744

github

web-flow
Merge a98f74ce1 into aacd50c09
Pull Request #744: Segformer fixes

4 of 6 new or added lines in 1 file covered. (66.67%)

60 existing lines in 2 files now uncovered.

35596 of 58092 relevant lines covered (61.28%)

11014.16 hits per line

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

82.59
/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
#include "sdfg/analysis/users.h"
13

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

22
namespace sdfg {
23
namespace transformations {
24

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

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

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

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

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

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

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

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

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

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

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

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

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

157
    isl_ctx* ctx = isl_ctx_alloc();
32✔
158
    isl_options_set_on_error(ctx, ISL_ON_ERROR_CONTINUE);
32✔
159

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

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

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

177
    producer_map = isl_map_align_params(producer_map, isl_space_copy(unified));
32✔
178
    consumer_map = isl_map_align_params(consumer_map, isl_space_copy(unified));
32✔
179

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

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

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

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

195
    bool domain_covered = isl_set_is_subset(consumer_domain, comp_domain) == isl_bool_true;
32✔
196

197
    isl_set_free(comp_domain);
32✔
198
    isl_set_free(consumer_domain);
32✔
199

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

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

213
            isl_set* producer_range = isl_map_range(constrained_producer);
30✔
214
            isl_set* consumer_range = isl_map_range(consumer_map_copy);
30✔
215

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

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

233
    isl_space_free(unified);
32✔
234
    isl_ctx_free(ctx);
32✔
235

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

240
    return mappings;
27✔
241
}
32✔
242

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

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

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

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

290
    return true;
4✔
291
}
4✔
292

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

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

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

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

334
    return true;
2✔
335
}
2✔
336

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

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

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

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

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

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

382
    bool first_nested = first_loop_info.is_perfectly_nested;
37✔
383
    bool second_nested = second_loop_info.is_perfectly_nested;
37✔
384

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

742
            fusion_candidates_.push_back(candidate);
27✔
743
        }
27✔
744
    }
29✔
745

746
    // Criterion: At least one valid fusion candidate
747
    return !fusion_candidates_.empty();
24✔
748
}
30✔
749

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

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

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

760
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
27✔
761
            auto& candidate = fusion_candidates_[cand_idx];
14✔
762

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

769
            // Insert a producer block at the beginning of the consumer's body
770
            auto& first_child = consumer_body_->at(0).first;
14✔
771
            control_flow::Assignments empty_assignments;
14✔
772
            auto& new_block = builder.add_block_before(*consumer_body_, first_child, empty_assignments);
14✔
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;
14✔
776
            for (auto& node : first_dataflow.nodes()) {
45✔
777
                node_mapping[&node] = &builder.copy_node(new_block, node);
45✔
778
                auto* copied = node_mapping[&node];
45✔
779
                if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
45✔
780
                    if (access_node->data() == candidate.container) {
31✔
781
                        access_node->data(temp_name);
14✔
782
                    } else if (access_node->data() == first_map_.indvar()->get_name()) {
17✔
783
                        access_node->data(second_loop_.indvar()->get_name());
1✔
784
                    }
1✔
785
                }
31✔
786
            }
45✔
787

788
            // Add memlets with index substitution (producer indvars → consumer expressions)
789
            for (auto& edge : first_dataflow.edges()) {
31✔
790
                auto& src_node = edge.src();
31✔
791
                auto& dst_node = edge.dst();
31✔
792

793
                const types::IType* base_type = &edge.base_type();
31✔
794
                data_flow::Subset new_subset;
31✔
795
                for (const auto& dim : edge.subset()) {
33✔
796
                    auto new_dim = dim;
33✔
797
                    for (const auto& [pvar, mapping] : candidate.index_mappings) {
45✔
798
                        new_dim = symbolic::subs(new_dim, pvar, mapping);
45✔
799
                    }
45✔
800
                    new_dim = symbolic::expand(new_dim);
33✔
801
                    new_subset.push_back(new_dim);
33✔
802
                }
33✔
803

804
                // For output edges to temp scalar, use empty subset
805
                auto* dst_access = dynamic_cast<data_flow::AccessNode*>(&dst_node);
31✔
806
                if (dst_access != nullptr && dst_access->data() == candidate.container &&
31✔
807
                    first_dataflow.in_degree(*dst_access) > 0) {
31✔
808
                    new_subset.clear();
14✔
809
                    base_type = &tmp_type;
14✔
810
                }
14✔
811

812
                builder.add_memlet(
31✔
813
                    new_block,
31✔
814
                    *node_mapping[&src_node],
31✔
815
                    edge.src_conn(),
31✔
816
                    *node_mapping[&dst_node],
31✔
817
                    edge.dst_conn(),
31✔
818
                    new_subset,
31✔
819
                    *base_type,
31✔
820
                    edge.debug_info()
31✔
821
                );
31✔
822
            }
31✔
823
        }
14✔
824

825
        // Update all read accesses in consumer blocks to point to the appropriate temp
826
        size_t num_producer_blocks = fusion_candidates_.size();
13✔
827

828
        for (size_t block_idx = num_producer_blocks; block_idx < consumer_body_->size(); ++block_idx) {
27✔
829
            auto* block = dynamic_cast<structured_control_flow::Block*>(&consumer_body_->at(block_idx).first);
14✔
830
            if (block == nullptr) {
14✔
UNCOV
831
                continue;
×
UNCOV
832
            }
×
833

834
            auto& dataflow = block->dataflow();
14✔
835

836
            for (auto& node : dataflow.nodes()) {
50✔
837
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
50✔
838
                if (access == nullptr || dataflow.out_degree(*access) == 0) {
50✔
839
                    continue;
30✔
840
                }
30✔
841

842
                std::string original_container = access->data();
20✔
843

844
                for (auto& memlet : dataflow.out_edges(*access)) {
21✔
845
                    if (memlet.type() != data_flow::MemletType::Computational) {
21✔
UNCOV
846
                        continue;
×
UNCOV
847
                    }
×
848

849
                    const auto& memlet_subset = memlet.subset();
21✔
850

851
                    for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
27✔
852
                        auto& candidate = fusion_candidates_[cand_idx];
22✔
853

854
                        if (original_container != candidate.container) {
22✔
855
                            continue;
5✔
856
                        }
5✔
857

858
                        if (memlet_subset.size() != candidate.consumer_subset.size()) {
17✔
UNCOV
859
                            continue;
×
UNCOV
860
                        }
×
861

862
                        bool subset_matches = true;
17✔
863
                        for (size_t d = 0; d < memlet_subset.size(); ++d) {
36✔
864
                            if (!symbolic::eq(memlet_subset[d], candidate.consumer_subset[d])) {
20✔
865
                                subset_matches = false;
1✔
866
                                break;
1✔
867
                            }
1✔
868
                        }
20✔
869

870
                        if (!subset_matches) {
17✔
871
                            continue;
1✔
872
                        }
1✔
873

874
                        const auto& temp_name = candidate_temps[cand_idx];
16✔
875
                        auto& temp_type = sdfg.type(temp_name);
16✔
876

877
                        access->data(temp_name);
16✔
878

879
                        memlet.set_subset({});
16✔
880
                        memlet.set_base_type(temp_type);
16✔
881

882
                        for (auto& in_edge : dataflow.in_edges(*access)) {
16✔
UNCOV
883
                            in_edge.set_subset({});
×
UNCOV
884
                            in_edge.set_base_type(temp_type);
×
UNCOV
885
                        }
×
886

887
                        break;
16✔
888
                    }
17✔
889
                }
21✔
890
            }
20✔
891
        }
14✔
892

893
    } else {
13✔
894
        // ConsumerIntoProducer (Pattern 2): Inline consumer blocks into the producer's write body
895
        // Modify the producer block in-place to write to a temp scalar, add a writeback block
896
        // for the original array, then copy consumer blocks reading from the temp.
897

898
        std::vector<std::string> candidate_temps;
3✔
899
        auto& producer_dataflow = producer_block_->dataflow();
3✔
900

901
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
6✔
902
            auto& candidate = fusion_candidates_[cand_idx];
3✔
903

904
            auto& container_type = sdfg.type(candidate.container);
3✔
905
            std::string temp_name = builder.find_new_name("_fused_tmp");
3✔
906
            types::Scalar tmp_type(container_type.primitive_type());
3✔
907
            builder.add_container(temp_name, tmp_type);
3✔
908
            candidate_temps.push_back(temp_name);
3✔
909

910
            // Step 1: Modify the original producer block to write to _fused_tmp
911
            data_flow::Subset original_write_subset;
3✔
912
            for (auto& node : producer_dataflow.nodes()) {
6✔
913
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
6✔
914
                if (access == nullptr || access->data() != candidate.container) continue;
6✔
915
                if (producer_dataflow.in_degree(*access) == 0) continue;
3✔
916

917
                // This is the write access node — save the original subset, then redirect
918
                for (auto& in_edge : producer_dataflow.in_edges(*access)) {
3✔
919
                    original_write_subset = in_edge.subset();
3✔
920
                    in_edge.set_subset({});
3✔
921
                    in_edge.set_base_type(tmp_type);
3✔
922
                }
3✔
923
                access->data(temp_name);
3✔
924
                break;
3✔
925
            }
3✔
926

927
            // Step 2: Add a writeback block: container[original_subset] = _fused_tmp
928
            control_flow::Assignments empty_assignments;
3✔
929
            auto& wb_block = builder.add_block_after(*producer_body_, *producer_block_, empty_assignments);
3✔
930
            auto& wb_src = builder.add_access(wb_block, temp_name);
3✔
931
            auto& wb_dst = builder.add_access(wb_block, candidate.container);
3✔
932
            auto& wb_tasklet = builder.add_tasklet(wb_block, data_flow::TaskletCode::assign, "_out", {"_in"});
3✔
933
            builder.add_computational_memlet(wb_block, wb_src, wb_tasklet, "_in", {});
3✔
934
            builder.add_computational_memlet(wb_block, wb_tasklet, "_out", wb_dst, original_write_subset);
3✔
935

936
            // Step 3: Copy consumer blocks after the writeback block
937
            structured_control_flow::ControlFlowNode* last_inserted = &wb_block;
3✔
938

939
            for (size_t i = 0; i < consumer_body_->size(); ++i) {
6✔
940
                auto* consumer_block = dynamic_cast<structured_control_flow::Block*>(&consumer_body_->at(i).first);
3✔
941
                if (consumer_block == nullptr) {
3✔
UNCOV
942
                    continue;
×
UNCOV
943
                }
×
944

945
                auto& consumer_dataflow = consumer_block->dataflow();
3✔
946

947
                // Check if this block reads from the fusion container
948
                bool reads_container = false;
3✔
949
                for (auto& node : consumer_dataflow.nodes()) {
11✔
950
                    auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
11✔
951
                    if (access != nullptr && access->data() == candidate.container &&
11✔
952
                        consumer_dataflow.out_degree(*access) > 0) {
11✔
953
                        reads_container = true;
3✔
954
                        break;
3✔
955
                    }
3✔
956
                }
11✔
957
                if (!reads_container) {
3✔
UNCOV
958
                    continue;
×
UNCOV
959
                }
×
960

961
                // Insert a new block after the last inserted block in the producer's body
962
                auto& new_block = builder.add_block_after(*producer_body_, *last_inserted, empty_assignments);
3✔
963

964
                // Deep copy all nodes from consumer block
965
                std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
3✔
966
                for (auto& node : consumer_dataflow.nodes()) {
11✔
967
                    node_mapping[&node] = &builder.copy_node(new_block, node);
11✔
968
                    auto* copied = node_mapping[&node];
11✔
969
                    if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
11✔
970
                        if (access_node->data() == candidate.container) {
8✔
971
                            // Only rename read access nodes to temp; keep write access nodes
972
                            // pointing to the original container
973
                            if (consumer_dataflow.in_degree(node) == 0) {
4✔
974
                                access_node->data(temp_name);
3✔
975
                            }
3✔
976
                        }
4✔
977
                        if (access_node->data() == second_loop_.indvar()->get_name() &&
8✔
978
                            consumer_dataflow.in_degree(node) == 0) {
8✔
NEW
UNCOV
979
                            access_node->data(first_map_.indvar()->get_name());
×
NEW
UNCOV
980
                        }
×
981
                    }
8✔
982
                }
11✔
983

984
                // Add memlets with index substitution (consumer indvars → producer expressions)
985
                for (auto& edge : consumer_dataflow.edges()) {
8✔
986
                    auto& src_node = edge.src();
8✔
987
                    auto& dst_node = edge.dst();
8✔
988

989
                    const types::IType* base_type = &edge.base_type();
8✔
990
                    data_flow::Subset new_subset;
8✔
991
                    for (const auto& dim : edge.subset()) {
9✔
992
                        auto new_dim = dim;
9✔
993
                        for (const auto& [cvar, mapping] : candidate.index_mappings) {
13✔
994
                            new_dim = symbolic::subs(new_dim, cvar, mapping);
13✔
995
                        }
13✔
996
                        new_dim = symbolic::expand(new_dim);
9✔
997
                        new_subset.push_back(new_dim);
9✔
998
                    }
9✔
999

1000
                    // For read edges from temp scalar, use empty subset
1001
                    auto* src_access = dynamic_cast<data_flow::AccessNode*>(&src_node);
8✔
1002
                    if (src_access != nullptr && src_access->data() == candidate.container &&
8✔
1003
                        consumer_dataflow.in_degree(*src_access) == 0) {
8✔
1004
                        new_subset.clear();
3✔
1005
                        base_type = &tmp_type;
3✔
1006
                    }
3✔
1007

1008
                    builder.add_memlet(
8✔
1009
                        new_block,
8✔
1010
                        *node_mapping[&src_node],
8✔
1011
                        edge.src_conn(),
8✔
1012
                        *node_mapping[&dst_node],
8✔
1013
                        edge.dst_conn(),
8✔
1014
                        new_subset,
8✔
1015
                        *base_type,
8✔
1016
                        edge.debug_info()
8✔
1017
                    );
8✔
1018
                }
8✔
1019

1020
                last_inserted = &new_block;
3✔
1021
            }
3✔
1022
        }
3✔
1023

1024
        // Remove the consumer loop
1025
        auto& scope_analysis = analysis_manager.get<analysis::ScopeAnalysis>();
3✔
1026
        auto* parent = scope_analysis.parent_scope(&second_loop_);
3✔
1027
        auto* parent_seq = dynamic_cast<structured_control_flow::Sequence*>(parent);
3✔
1028
        if (parent_seq != nullptr) {
3✔
1029
            int idx = parent_seq->index(second_loop_);
3✔
1030
            if (idx >= 0) {
3✔
1031
                builder.remove_child(*parent_seq, static_cast<size_t>(idx));
3✔
1032
            }
3✔
1033
        }
3✔
1034
    }
3✔
1035

1036
    analysis_manager.invalidate_all();
16✔
1037
    applied_ = true;
16✔
1038
}
16✔
1039

1040
void MapFusion::to_json(nlohmann::json& j) const {
1✔
1041
    std::string second_type = "for";
1✔
1042
    if (dynamic_cast<structured_control_flow::Map*>(&second_loop_) != nullptr) {
1✔
1043
        second_type = "map";
1✔
1044
    }
1✔
1045
    j["transformation_type"] = this->name();
1✔
1046
    j["subgraph"] = {
1✔
1047
        {"0", {{"element_id", first_map_.element_id()}, {"type", "map"}}},
1✔
1048
        {"1", {{"element_id", second_loop_.element_id()}, {"type", second_type}}}
1✔
1049
    };
1✔
1050
}
1✔
1051

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

1056
    auto first_element = builder.find_element_by_id(first_map_id);
1✔
1057
    auto second_element = builder.find_element_by_id(second_loop_id);
1✔
1058

1059
    if (first_element == nullptr) {
1✔
1060
        throw InvalidTransformationDescriptionException("Element with ID " + std::to_string(first_map_id) + " not found.");
×
1061
    }
×
1062
    if (second_element == nullptr) {
1✔
1063
        throw InvalidTransformationDescriptionException(
×
1064
            "Element with ID " + std::to_string(second_loop_id) + " not found."
×
1065
        );
×
1066
    }
×
1067

1068
    auto* first_map = dynamic_cast<structured_control_flow::Map*>(first_element);
1✔
1069
    auto* second_loop = dynamic_cast<structured_control_flow::StructuredLoop*>(second_element);
1✔
1070

1071
    if (first_map == nullptr) {
1✔
UNCOV
1072
        throw InvalidTransformationDescriptionException(
×
UNCOV
1073
            "Element with ID " + std::to_string(first_map_id) + " is not a Map."
×
UNCOV
1074
        );
×
UNCOV
1075
    }
×
1076
    if (second_loop == nullptr) {
1✔
UNCOV
1077
        throw InvalidTransformationDescriptionException(
×
UNCOV
1078
            "Element with ID " + std::to_string(second_loop_id) + " is not a StructuredLoop."
×
UNCOV
1079
        );
×
UNCOV
1080
    }
×
1081

1082
    return MapFusion(*first_map, *second_loop);
1✔
1083
}
1✔
1084

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

© 2026 Coveralls, Inc