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

daisytuner / docc / 26809042868

02 Jun 2026 08:49AM UTC coverage: 60.831% (-0.04%) from 60.869%
26809042868

Pull #725

github

web-flow
Merge eceff48b9 into cd25c9278
Pull Request #725: Tensor node backport

599 of 1255 new or added lines in 51 files covered. (47.73%)

538 existing lines in 45 files now uncovered.

35099 of 57699 relevant lines covered (60.83%)

11080.96 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

240
    return mappings;
26✔
241
}
31✔
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) {
37✔
338
    fusion_candidates_.clear();
37✔
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) {
37✔
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>();
37✔
347
    auto* first_parent = scope_analysis.parent_scope(&first_map_);
37✔
348
    auto* second_parent = scope_analysis.parent_scope(&second_loop_);
37✔
349
    if (first_parent == nullptr || second_parent == nullptr) {
37✔
350
        return false;
×
351
    }
×
352
    if (first_parent != second_parent) {
37✔
353
        return false;
×
354
    }
×
355

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

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

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

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

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

390
    if (first_nested && second_nested) {
35✔
391
        // Pattern 1: Both perfectly nested — producer into consumer (original path)
392
        direction_ = FusionDirection::ProducerIntoConsumer;
32✔
393
    } else if (!first_nested && second_nested) {
32✔
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();
35✔
403
    producer_body_ = nullptr;
35✔
404
    producer_block_ = nullptr;
35✔
405

406
    if (first_nested) {
35✔
407
        // Perfectly nested: walk the at(0).first chain
408
        producer_loops_.push_back(&first_map_);
33✔
409
        producer_body_ = &first_map_.root();
33✔
410
        structured_control_flow::ControlFlowNode* node = &first_map_.root().at(0).first;
33✔
411
        while (auto* nested = dynamic_cast<structured_control_flow::StructuredLoop*>(node)) {
42✔
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);
32✔
418
        if (producer_block_ == nullptr) {
32✔
419
            return false;
×
UNCOV
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) {
32✔
UNCOV
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_.
UNCOV
429
        }
×
430
    } else {
32✔
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();
34✔
438
    consumer_body_ = nullptr;
34✔
439

440
    if (second_nested) {
34✔
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_);
33✔
445
        consumer_body_ = &second_loop_.root();
33✔
446
        structured_control_flow::ControlFlowNode* node = &second_loop_.root().at(0).first;
33✔
447
        while (auto* nested = dynamic_cast<structured_control_flow::StructuredLoop*>(node)) {
43✔
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 {
33✔
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>();
33✔
459
    auto first_args = arguments_analysis.arguments(analysis_manager, first_map_);
33✔
460
    auto second_args = arguments_analysis.arguments(analysis_manager, second_loop_);
33✔
461

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

473
    // First pass: identify fusion containers (producer writes, consumer reads)
474
    std::unordered_set<std::string> fusion_containers;
33✔
475
    for (const auto& [name, arg] : second_args) {
114✔
476
        if (first_outputs.contains(name) && arg.is_input) {
114✔
477
            fusion_containers.insert(name);
32✔
478
        }
32✔
479
    }
114✔
480
    if (fusion_containers.empty()) {
33✔
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) {
109✔
486
        bool is_fusion = fusion_containers.contains(name);
109✔
487
        if (first_outputs.contains(name) && arg.is_output && !is_fusion) {
109✔
UNCOV
488
            return false;
×
UNCOV
489
        }
×
490
        if (first_inputs.contains(name) && arg.is_output && !is_fusion) {
109✔
491
            return false;
1✔
492
        }
1✔
493
    }
109✔
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) {
31✔
502
        if (!first_loop_info.is_perfectly_parallel || !second_loop_info.is_perfectly_parallel) {
29✔
503
            return false;
2✔
504
        }
2✔
505
    } else {
29✔
506
        if (!second_loop_info.is_perfectly_parallel) {
2✔
UNCOV
507
            return false;
×
UNCOV
508
        }
×
509
    }
2✔
510

511
    // Now that we know the fusion containers, resolve deferred locations
512
    if (producer_block_ == nullptr) {
29✔
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✔
UNCOV
522
                return false;
×
523
            }
×
524
            if (write_block == nullptr) {
2✔
UNCOV
525
                return false;
×
UNCOV
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;
×
UNCOV
537
                }
×
UNCOV
538
            }
×
539
        }
2✔
540
    }
2✔
541

542
    if (!second_nested) {
29✔
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✔
UNCOV
550
                return false;
×
551
            }
×
552
            if (read_body == nullptr) {
1✔
UNCOV
553
                return false;
×
UNCOV
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;
×
UNCOV
564
                }
×
UNCOV
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>();
29✔
572
    auto& producer_assumptions = assumptions_analysis.get(*producer_block_, true);
29✔
573
    auto& consumer_assumptions = assumptions_analysis.get(consumer_body_->at(0).first, true);
29✔
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) {
29✔
581
        auto& first_dataflow_check = producer_block_->dataflow();
27✔
582
        bool producer_reads_fusion = false;
27✔
583
        for (const auto& container : fusion_containers) {
27✔
584
            for (auto& node : first_dataflow_check.nodes()) {
86✔
585
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
86✔
586
                if (access != nullptr && access->data() == container && first_dataflow_check.out_degree(*access) > 0) {
86✔
587
                    producer_reads_fusion = true;
2✔
588
                    break;
2✔
589
                }
2✔
590
            }
86✔
591
            if (producer_reads_fusion) break;
27✔
592
        }
27✔
593
        if (producer_reads_fusion) {
27✔
594
            direction_ = FusionDirection::ConsumerIntoProducer;
2✔
595
            // Re-check: consumer must be all-parallel for ConsumerIntoProducer
596
            if (!second_loop_info.is_perfectly_parallel) {
2✔
UNCOV
597
                return false;
×
UNCOV
598
            }
×
599
        }
2✔
600
    }
27✔
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) {
29✔
UNCOV
607
        return false;
×
UNCOV
608
    }
×
609

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

616
        for (auto& node : first_dataflow.nodes()) {
93✔
617
            auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
93✔
618
            if (access == nullptr || access->data() != container) {
93✔
619
                continue;
62✔
620
            }
62✔
621
            // Skip read-only access nodes (producer reads the fusion container)
622
            if (first_dataflow.in_degree(*access) == 0) {
31✔
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) {
29✔
UNCOV
627
                return false;
×
UNCOV
628
            }
×
629
            auto& iedge = *first_dataflow.in_edges(*access).begin();
29✔
630
            if (iedge.type() != data_flow::MemletType::Computational) {
29✔
UNCOV
631
                return false;
×
632
            }
×
633
            if (producer_memlet != nullptr) {
29✔
UNCOV
634
                return false;
×
UNCOV
635
            }
×
636
            producer_memlet = &iedge;
29✔
637
        }
29✔
638
        if (producer_memlet == nullptr) {
29✔
UNCOV
639
            return false;
×
UNCOV
640
        }
×
641

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

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

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

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

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

681
                    // Check if this subset is already in unique_subsets
682
                    bool found = false;
33✔
683
                    for (const auto& existing : unique_subsets) {
33✔
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) {
33✔
698
                        unique_subsets.push_back(consumer_subset);
31✔
699
                    }
31✔
700
                }
33✔
701
            }
33✔
702
        }
30✔
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) {
31✔
707
            std::vector<std::pair<symbolic::Symbol, symbolic::Expression>> mappings;
31✔
708

709
            if (direction_ == FusionDirection::ProducerIntoConsumer) {
31✔
710
                // Solve producer indvars in terms of consumer indvars
711
                mappings = solve_subsets(
27✔
712
                    producer_subset,
27✔
713
                    consumer_subset,
27✔
714
                    producer_loops_,
27✔
715
                    consumer_loops_,
27✔
716
                    producer_assumptions,
27✔
717
                    consumer_assumptions
27✔
718
                );
27✔
719
            } else {
27✔
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()) {
31✔
734
                return false;
5✔
735
            }
5✔
736

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

742
            fusion_candidates_.push_back(candidate);
26✔
743
        }
26✔
744
    }
28✔
745

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

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

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

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

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

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

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

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

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

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

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

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

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

832
            auto& dataflow = block->dataflow();
13✔
833

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

840
                std::string original_container = access->data();
18✔
841

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

847
                    const auto& memlet_subset = memlet.subset();
19✔
848

849
                    for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
24✔
850
                        auto& candidate = fusion_candidates_[cand_idx];
20✔
851

852
                        if (original_container != candidate.container) {
20✔
853
                            continue;
4✔
854
                        }
4✔
855

856
                        if (memlet_subset.size() != candidate.consumer_subset.size()) {
16✔
UNCOV
857
                            continue;
×
UNCOV
858
                        }
×
859

860
                        bool subset_matches = true;
16✔
861
                        for (size_t d = 0; d < memlet_subset.size(); ++d) {
34✔
862
                            if (!symbolic::eq(memlet_subset[d], candidate.consumer_subset[d])) {
19✔
863
                                subset_matches = false;
1✔
864
                                break;
1✔
865
                            }
1✔
866
                        }
19✔
867

868
                        if (!subset_matches) {
16✔
869
                            continue;
1✔
870
                        }
1✔
871

872
                        const auto& temp_name = candidate_temps[cand_idx];
15✔
873
                        auto& temp_type = sdfg.type(temp_name);
15✔
874

875
                        access->data(temp_name);
15✔
876

877
                        memlet.set_subset({});
15✔
878
                        memlet.set_base_type(temp_type);
15✔
879

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

885
                        break;
15✔
886
                    }
16✔
887
                }
19✔
888
            }
18✔
889
        }
13✔
890

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

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

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

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

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

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

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

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

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

943
                auto& consumer_dataflow = consumer_block->dataflow();
3✔
944

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

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

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

978
                // Add memlets with index substitution (consumer indvars → producer expressions)
979
                for (auto& edge : consumer_dataflow.edges()) {
8✔
980
                    auto& src_node = edge.src();
8✔
981
                    auto& dst_node = edge.dst();
8✔
982

983
                    const types::IType* base_type = &edge.base_type();
8✔
984
                    data_flow::Subset new_subset;
8✔
985
                    for (const auto& dim : edge.subset()) {
9✔
986
                        auto new_dim = dim;
9✔
987
                        for (const auto& [cvar, mapping] : candidate.index_mappings) {
13✔
988
                            new_dim = symbolic::subs(new_dim, cvar, mapping);
13✔
989
                        }
13✔
990
                        new_dim = symbolic::expand(new_dim);
9✔
991
                        new_subset.push_back(new_dim);
9✔
992
                    }
9✔
993

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

1002
                    builder.add_memlet(
8✔
1003
                        new_block,
8✔
1004
                        *node_mapping[&src_node],
8✔
1005
                        edge.src_conn(),
8✔
1006
                        *node_mapping[&dst_node],
8✔
1007
                        edge.dst_conn(),
8✔
1008
                        new_subset,
8✔
1009
                        *base_type,
8✔
1010
                        edge.debug_info()
8✔
1011
                    );
8✔
1012
                }
8✔
1013

1014
                last_inserted = &new_block;
3✔
1015
            }
3✔
1016
        }
3✔
1017

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

1030
    analysis_manager.invalidate_all();
15✔
1031
    applied_ = true;
15✔
1032
}
15✔
1033

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

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

1050
    auto first_element = builder.find_element_by_id(first_map_id);
1✔
1051
    auto second_element = builder.find_element_by_id(second_loop_id);
1✔
1052

1053
    if (first_element == nullptr) {
1✔
UNCOV
1054
        throw InvalidTransformationDescriptionException("Element with ID " + std::to_string(first_map_id) + " not found.");
×
1055
    }
×
1056
    if (second_element == nullptr) {
1✔
1057
        throw InvalidTransformationDescriptionException(
×
1058
            "Element with ID " + std::to_string(second_loop_id) + " not found."
×
UNCOV
1059
        );
×
UNCOV
1060
    }
×
1061

1062
    auto* first_map = dynamic_cast<structured_control_flow::Map*>(first_element);
1✔
1063
    auto* second_loop = dynamic_cast<structured_control_flow::StructuredLoop*>(second_element);
1✔
1064

1065
    if (first_map == nullptr) {
1✔
1066
        throw InvalidTransformationDescriptionException(
×
1067
            "Element with ID " + std::to_string(first_map_id) + " is not a Map."
×
UNCOV
1068
        );
×
1069
    }
×
1070
    if (second_loop == nullptr) {
1✔
1071
        throw InvalidTransformationDescriptionException(
×
1072
            "Element with ID " + std::to_string(second_loop_id) + " is not a StructuredLoop."
×
UNCOV
1073
        );
×
UNCOV
1074
    }
×
1075

1076
    return MapFusion(*first_map, *second_loop);
1✔
1077
}
1✔
1078

1079
} // namespace transformations
1080
} // 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