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

daisytuner / docc / 24215882789

09 Apr 2026 10:12PM UTC coverage: 64.375% (-0.007%) from 64.382%
24215882789

Pull #668

github

web-flow
Merge 6f7f28e8f into bb3981349
Pull Request #668: Offload Memset to GPU

249 of 381 new or added lines in 18 files covered. (65.35%)

189 existing lines in 2 files now uncovered.

29942 of 46512 relevant lines covered (64.37%)

584.42 hits per line

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

82.63
/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(structured_control_flow::Map& first_map, structured_control_flow::StructuredLoop& second_loop)
26
    : first_map_(first_map), second_loop_(second_loop) {}
37✔
27

28
std::string MapFusion::name() const { return "MapFusion"; }
2✔
29

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

56
    // Subset dimensions must match
57
    if (producer_sub.size() != consumer_sub.size()) {
31✔
58
        return {};
×
59
    }
×
60
    if (producer_sub.empty()) {
31✔
61
        return {};
×
62
    }
×
63

64
    // Extract producer indvars
65
    SymEngine::vec_sym producer_vars;
31✔
66
    for (auto* loop : producer_loops) {
40✔
67
        producer_vars.push_back(SymEngine::rcp_static_cast<const SymEngine::Symbol>(loop->indvar()));
40✔
68
    }
40✔
69

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

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

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

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

104
        // Check for invalid solutions
105
        if (SymEngine::is_a<SymEngine::NaN>(*sol) || SymEngine::is_a<SymEngine::Infty>(*sol)) {
40✔
106
            return {};
×
107
        }
×
108

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

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

149
    std::string producer_map_str = symbolic::expression_to_map_str(producer_sub, unconstrained_producer);
31✔
150
    // Build consumer access map with full domain constraints
151
    std::string consumer_map_str = symbolic::expression_to_map_str(consumer_sub, consumer_assumptions);
31✔
152

153
    isl_ctx* ctx = isl_ctx_alloc();
31✔
154
    isl_options_set_on_error(ctx, ISL_ON_ERROR_CONTINUE);
31✔
155

156
    isl_map* producer_map = isl_map_read_from_str(ctx, producer_map_str.c_str());
31✔
157
    isl_map* consumer_map = isl_map_read_from_str(ctx, consumer_map_str.c_str());
31✔
158

159
    if (!producer_map || !consumer_map) {
31✔
160
        if (producer_map) isl_map_free(producer_map);
×
161
        if (consumer_map) isl_map_free(consumer_map);
×
162
        isl_ctx_free(ctx);
×
163
        return {};
×
164
    }
×
165

166
    // Align parameters between the two maps
167
    isl_space* params_p = isl_space_params(isl_map_get_space(producer_map));
31✔
168
    isl_space* params_c = isl_space_params(isl_map_get_space(consumer_map));
31✔
169
    isl_space* unified = isl_space_align_params(isl_space_copy(params_p), isl_space_copy(params_c));
31✔
170
    isl_space_free(params_p);
31✔
171
    isl_space_free(params_c);
31✔
172

173
    producer_map = isl_map_align_params(producer_map, isl_space_copy(unified));
31✔
174
    consumer_map = isl_map_align_params(consumer_map, isl_space_copy(unified));
31✔
175

176
    // Save consumer domain before consuming consumer_map in composition
177
    isl_set* consumer_domain = isl_map_domain(isl_map_copy(consumer_map));
31✔
178

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

185
    // Check single-valuedness: each consumer point maps to at most one producer point
186
    bool single_valued = isl_map_is_single_valued(composition) == isl_bool_true;
31✔
187

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

191
    bool domain_covered = isl_set_is_subset(consumer_domain, comp_domain) == isl_bool_true;
31✔
192

193
    isl_set_free(comp_domain);
31✔
194
    isl_set_free(consumer_domain);
31✔
195

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

205
        if (constrained_producer && consumer_map_copy) {
29✔
206
            constrained_producer = isl_map_align_params(constrained_producer, isl_space_copy(unified));
29✔
207
            consumer_map_copy = isl_map_align_params(consumer_map_copy, isl_space_copy(unified));
29✔
208

209
            isl_set* producer_range = isl_map_range(constrained_producer);
29✔
210
            isl_set* consumer_range = isl_map_range(consumer_map_copy);
29✔
211

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

221
            isl_set_free(producer_range);
29✔
222
            isl_set_free(consumer_range);
29✔
223
        } else {
29✔
224
            if (constrained_producer) isl_map_free(constrained_producer);
×
225
            if (consumer_map_copy) isl_map_free(consumer_map_copy);
×
226
        }
×
227
    }
29✔
228

229
    isl_space_free(unified);
31✔
230
    isl_ctx_free(ctx);
31✔
231

232
    if (!single_valued || !domain_covered || !range_covered) {
31✔
233
        return {};
5✔
234
    }
5✔
235

236
    return mappings;
26✔
237
}
31✔
238

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

249
    for (size_t i = 0; i < seq.size(); ++i) {
10✔
250
        auto& child = seq.at(i).first;
6✔
251

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

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

286
    return true;
4✔
287
}
4✔
288

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

298
    for (size_t i = 0; i < seq.size(); ++i) {
5✔
299
        auto& child = seq.at(i).first;
3✔
300

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

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

330
    return true;
2✔
331
}
2✔
332

333
bool MapFusion::can_be_applied(builder::StructuredSDFGBuilder& builder, analysis::AnalysisManager& analysis_manager) {
35✔
334
    fusion_candidates_.clear();
35✔
335

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

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

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

357
    int first_index = parent_sequence->index(first_map_);
35✔
358
    int second_index = parent_sequence->index(second_loop_);
35✔
359
    if (first_index == -1 || second_index == -1) {
35✔
360
        return false;
×
361
    }
×
362
    if (second_index != first_index + 1) {
35✔
363
        return false;
1✔
364
    }
1✔
365

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

376
    bool first_nested = first_loop_info.is_perfectly_nested;
34✔
377
    bool second_nested = second_loop_info.is_perfectly_nested;
34✔
378

379
    // Both non-perfectly-nested: not supported
380
    if (!first_nested && !second_nested) {
34✔
381
        return false;
1✔
382
    }
1✔
383

384
    if (first_nested && second_nested) {
33✔
385
        // Pattern 1: Both perfectly nested — producer into consumer (original path)
386
        direction_ = FusionDirection::ProducerIntoConsumer;
30✔
387
    } else if (!first_nested && second_nested) {
30✔
388
        // Pattern 2: Producer non-perfectly-nested, consumer perfectly nested
389
        direction_ = FusionDirection::ConsumerIntoProducer;
2✔
390
    } else {
2✔
391
        // Reverse Pattern 2: Producer perfectly nested, consumer non-perfectly-nested
392
        direction_ = FusionDirection::ProducerIntoConsumer;
1✔
393
    }
1✔
394

395
    // Locate producer write point
396
    producer_loops_.clear();
33✔
397
    producer_body_ = nullptr;
33✔
398
    producer_block_ = nullptr;
33✔
399

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

429
    // Locate consumer read point
430
    consumer_loops_.clear();
33✔
431
    consumer_body_ = nullptr;
33✔
432

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

449
    // Get arguments analysis to identify inputs/outputs of each loop
450
    auto& arguments_analysis = analysis_manager.get<analysis::ArgumentsAnalysis>();
33✔
451
    auto first_args = arguments_analysis.arguments(analysis_manager, first_map_);
33✔
452
    auto second_args = arguments_analysis.arguments(analysis_manager, second_loop_);
33✔
453

454
    std::unordered_set<std::string> first_inputs;
33✔
455
    std::unordered_set<std::string> first_outputs;
33✔
456
    for (const auto& [name, arg] : first_args) {
110✔
457
        if (arg.is_output) {
110✔
458
            first_outputs.insert(name);
34✔
459
        }
34✔
460
        if (arg.is_input) {
110✔
461
            first_inputs.insert(name);
110✔
462
        }
110✔
463
    }
110✔
464

465
    // First pass: identify fusion containers (producer writes, consumer reads)
466
    std::unordered_set<std::string> fusion_containers;
33✔
467
    for (const auto& [name, arg] : second_args) {
114✔
468
        if (first_outputs.contains(name) && arg.is_input) {
114✔
469
            fusion_containers.insert(name);
32✔
470
        }
32✔
471
    }
114✔
472
    if (fusion_containers.empty()) {
33✔
473
        return false;
1✔
474
    }
1✔
475

476
    // Second pass: check for conflicts on non-fusion containers
477
    for (const auto& [name, arg] : second_args) {
109✔
478
        bool is_fusion = fusion_containers.contains(name);
109✔
479
        if (first_outputs.contains(name) && arg.is_output && !is_fusion) {
109✔
UNCOV
480
            return false;
×
UNCOV
481
        }
×
482
        if (first_inputs.contains(name) && arg.is_output && !is_fusion) {
109✔
483
            return false;
1✔
484
        }
1✔
485
    }
109✔
486

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

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

513
            if (!find_write_location(first_map_, container, write_loops, write_body, write_block)) {
2✔
UNCOV
514
                return false;
×
UNCOV
515
            }
×
516
            if (write_block == nullptr) {
2✔
UNCOV
517
                return false;
×
UNCOV
518
            }
×
519

520
            if (producer_block_ == nullptr) {
2✔
521
                // First container: set the locations
522
                producer_loops_ = write_loops;
2✔
523
                producer_body_ = write_body;
2✔
524
                producer_block_ = write_block;
2✔
525
            } else {
2✔
526
                // Subsequent containers must be in the same block
527
                if (write_block != producer_block_) {
×
UNCOV
528
                    return false;
×
UNCOV
529
                }
×
UNCOV
530
            }
×
531
        }
2✔
532
    }
2✔
533

534
    if (!second_nested) {
29✔
535
        // Non-perfectly-nested consumer: find read location for the first fusion container
536
        // All fusion containers must be read at the same sequence for this to work
537
        for (const auto& container : fusion_containers) {
1✔
538
            std::vector<structured_control_flow::StructuredLoop*> read_loops;
1✔
539
            structured_control_flow::Sequence* read_body = nullptr;
1✔
540

541
            if (!find_read_location(second_loop_, container, read_loops, read_body)) {
1✔
UNCOV
542
                return false;
×
UNCOV
543
            }
×
544
            if (read_body == nullptr) {
1✔
UNCOV
545
                return false;
×
UNCOV
546
            }
×
547

548
            if (consumer_body_ == nullptr) {
1✔
549
                // First container: set the locations
550
                consumer_loops_ = read_loops;
1✔
551
                consumer_body_ = read_body;
1✔
552
            } else {
1✔
553
                // Subsequent containers must be at the same sequence
UNCOV
554
                if (read_body != consumer_body_) {
×
UNCOV
555
                    return false;
×
UNCOV
556
                }
×
UNCOV
557
            }
×
558
        }
1✔
559
    }
1✔
560

561
    // Get assumptions for the resolved write/read locations
562
    auto& assumptions_analysis = analysis_manager.get<analysis::AssumptionsAnalysis>();
29✔
563
    auto& producer_assumptions = assumptions_analysis.get(*producer_block_);
29✔
564
    auto& consumer_assumptions = assumptions_analysis.get(consumer_body_->at(0).first);
29✔
565

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

593
    // ProducerIntoConsumer only deep-copies producer_block_ into the consumer body.
594
    // If the producer body has multiple blocks (e.g. from prior BlockFusion merging
595
    // a previous fusion's writeback + inlined blocks), the write block may depend on
596
    // intermediates produced by earlier blocks that would NOT be copied. Reject.
597
    if (direction_ == FusionDirection::ProducerIntoConsumer && producer_body_->size() > 1) {
29✔
UNCOV
598
        return false;
×
UNCOV
599
    }
×
600

601
    // For each fusion container, find the producer memlet and collect unique consumer subsets
602
    auto& first_dataflow = producer_block_->dataflow();
29✔
603
    for (const auto& container : fusion_containers) {
29✔
604
        // Find unique producer write in first map
605
        data_flow::Memlet* producer_memlet = nullptr;
29✔
606

607
        for (auto& node : first_dataflow.nodes()) {
93✔
608
            auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
93✔
609
            if (access == nullptr || access->data() != container) {
93✔
610
                continue;
62✔
611
            }
62✔
612
            // Skip read-only access nodes (producer reads the fusion container)
613
            if (first_dataflow.in_degree(*access) == 0) {
31✔
614
                continue;
2✔
615
            }
2✔
616
            // Write access: must have exactly one incoming edge and no outgoing
617
            if (first_dataflow.in_degree(*access) != 1 || first_dataflow.out_degree(*access) != 0) {
29✔
UNCOV
618
                return false;
×
UNCOV
619
            }
×
620
            auto& iedge = *first_dataflow.in_edges(*access).begin();
29✔
621
            if (iedge.type() != data_flow::MemletType::Computational) {
29✔
UNCOV
622
                return false;
×
UNCOV
623
            }
×
624
            if (producer_memlet != nullptr) {
29✔
UNCOV
625
                return false;
×
UNCOV
626
            }
×
627
            producer_memlet = &iedge;
29✔
628
        }
29✔
629
        if (producer_memlet == nullptr) {
29✔
UNCOV
630
            return false;
×
UNCOV
631
        }
×
632

633
        const auto& producer_subset = producer_memlet->subset();
29✔
634
        if (producer_subset.empty()) {
29✔
UNCOV
635
            return false;
×
UNCOV
636
        }
×
637

638
        // Collect all unique subsets from consumer blocks
639
        std::vector<data_flow::Subset> unique_subsets;
29✔
640
        for (size_t i = 0; i < consumer_body_->size(); ++i) {
58✔
641
            auto* block = dynamic_cast<structured_control_flow::Block*>(&consumer_body_->at(i).first);
30✔
642
            if (block == nullptr) {
30✔
643
                // Skip non-block children (e.g. nested loops that are not related)
UNCOV
644
                continue;
×
UNCOV
645
            }
×
646

647
            auto& dataflow = block->dataflow();
30✔
648
            for (auto& node : dataflow.nodes()) {
104✔
649
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
104✔
650
                if (access == nullptr || access->data() != container) {
104✔
651
                    continue;
69✔
652
                }
69✔
653
                // Skip write-only access nodes (consumer also writes the fusion container)
654
                if (dataflow.in_degree(*access) > 0 && dataflow.out_degree(*access) == 0) {
35✔
655
                    continue;
2✔
656
                }
2✔
657
                if (dataflow.in_degree(*access) != 0 || dataflow.out_degree(*access) == 0) {
33✔
UNCOV
658
                    return false;
×
UNCOV
659
                }
×
660

661
                // Check all read memlets from this access
662
                for (auto& memlet : dataflow.out_edges(*access)) {
34✔
663
                    if (memlet.type() != data_flow::MemletType::Computational) {
34✔
UNCOV
664
                        return false;
×
UNCOV
665
                    }
×
666

667
                    auto& consumer_subset = memlet.subset();
34✔
668
                    if (consumer_subset.size() != producer_subset.size()) {
34✔
669
                        return false;
1✔
670
                    }
1✔
671

672
                    // Check if this subset is already in unique_subsets
673
                    bool found = false;
33✔
674
                    for (const auto& existing : unique_subsets) {
33✔
675
                        if (existing.size() != consumer_subset.size()) continue;
6✔
676
                        bool match = true;
6✔
677
                        for (size_t d = 0; d < existing.size(); ++d) {
8✔
678
                            if (!symbolic::eq(existing[d], consumer_subset[d])) {
6✔
679
                                match = false;
4✔
680
                                break;
4✔
681
                            }
4✔
682
                        }
6✔
683
                        if (match) {
6✔
684
                            found = true;
2✔
685
                            break;
2✔
686
                        }
2✔
687
                    }
6✔
688
                    if (!found) {
33✔
689
                        unique_subsets.push_back(consumer_subset);
31✔
690
                    }
31✔
691
                }
33✔
692
            }
33✔
693
        }
30✔
694

695
        // For each unique consumer subset, solve index mappings and create a FusionCandidate
696
        // The direction determines which side's indvars are solved for
697
        for (const auto& consumer_subset : unique_subsets) {
31✔
698
            std::vector<std::pair<symbolic::Symbol, symbolic::Expression>> mappings;
31✔
699

700
            if (direction_ == FusionDirection::ProducerIntoConsumer) {
31✔
701
                // Solve producer indvars in terms of consumer indvars
702
                mappings = solve_subsets(
27✔
703
                    producer_subset,
27✔
704
                    consumer_subset,
27✔
705
                    producer_loops_,
27✔
706
                    consumer_loops_,
27✔
707
                    producer_assumptions,
27✔
708
                    consumer_assumptions
27✔
709
                );
27✔
710
            } else {
27✔
711
                // ConsumerIntoProducer: solve consumer indvars in terms of producer indvars
712
                // Arguments are swapped, so invert the range check direction
713
                mappings = solve_subsets(
4✔
714
                    consumer_subset,
4✔
715
                    producer_subset,
4✔
716
                    consumer_loops_,
4✔
717
                    producer_loops_,
4✔
718
                    consumer_assumptions,
4✔
719
                    producer_assumptions,
4✔
720
                    true
4✔
721
                );
4✔
722
            }
4✔
723

724
            if (mappings.empty()) {
31✔
725
                return false;
5✔
726
            }
5✔
727

728
            FusionCandidate candidate;
26✔
729
            candidate.container = container;
26✔
730
            candidate.consumer_subset = consumer_subset;
26✔
731
            candidate.index_mappings = std::move(mappings);
26✔
732

733
            fusion_candidates_.push_back(candidate);
26✔
734
        }
26✔
735
    }
28✔
736

737
    // Criterion: At least one valid fusion candidate
738
    return !fusion_candidates_.empty();
23✔
739
}
29✔
740

741
void MapFusion::apply(builder::StructuredSDFGBuilder& builder, analysis::AnalysisManager& analysis_manager) {
15✔
742
    auto& sdfg = builder.subject();
15✔
743

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

748
        // For each fusion candidate, create a temp and insert a producer block
749
        std::vector<std::string> candidate_temps;
12✔
750

751
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
25✔
752
            auto& candidate = fusion_candidates_[cand_idx];
13✔
753

754
            auto& container_type = sdfg.type(candidate.container);
13✔
755
            std::string temp_name = builder.find_new_name("_fused_tmp");
13✔
756
            types::Scalar tmp_type(container_type.primitive_type());
13✔
757
            builder.add_container(temp_name, tmp_type);
13✔
758
            candidate_temps.push_back(temp_name);
13✔
759

760
            // Insert a producer block at the beginning of the consumer's body
761
            auto& first_child = consumer_body_->at(0).first;
13✔
762
            control_flow::Assignments empty_assignments;
13✔
763
            auto& new_block = builder.add_block_before(*consumer_body_, first_child, empty_assignments);
13✔
764

765
            // Deep copy all nodes from producer block to new block
766
            std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
13✔
767
            for (auto& node : first_dataflow.nodes()) {
42✔
768
                node_mapping[&node] = &builder.copy_node(new_block, node);
42✔
769
                auto* copied = node_mapping[&node];
42✔
770
                if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
42✔
771
                    if (access_node->data() == candidate.container) {
29✔
772
                        access_node->data(temp_name);
13✔
773
                    }
13✔
774
                }
29✔
775
            }
42✔
776

777
            // Add memlets with index substitution (producer indvars → consumer expressions)
778
            for (auto& edge : first_dataflow.edges()) {
29✔
779
                auto& src_node = edge.src();
29✔
780
                auto& dst_node = edge.dst();
29✔
781

782
                const types::IType* base_type = &edge.base_type();
29✔
783
                data_flow::Subset new_subset;
29✔
784
                for (const auto& dim : edge.subset()) {
32✔
785
                    auto new_dim = dim;
32✔
786
                    for (const auto& [pvar, mapping] : candidate.index_mappings) {
44✔
787
                        new_dim = symbolic::subs(new_dim, pvar, mapping);
44✔
788
                    }
44✔
789
                    new_dim = symbolic::expand(new_dim);
32✔
790
                    new_subset.push_back(new_dim);
32✔
791
                }
32✔
792

793
                // For output edges to temp scalar, use empty subset
794
                auto* dst_access = dynamic_cast<data_flow::AccessNode*>(&dst_node);
29✔
795
                if (dst_access != nullptr && dst_access->data() == candidate.container &&
29✔
796
                    first_dataflow.in_degree(*dst_access) > 0) {
29✔
797
                    new_subset.clear();
13✔
798
                    base_type = &tmp_type;
13✔
799
                }
13✔
800

801
                builder.add_memlet(
29✔
802
                    new_block,
29✔
803
                    *node_mapping[&src_node],
29✔
804
                    edge.src_conn(),
29✔
805
                    *node_mapping[&dst_node],
29✔
806
                    edge.dst_conn(),
29✔
807
                    new_subset,
29✔
808
                    *base_type,
29✔
809
                    edge.debug_info()
29✔
810
                );
29✔
811
            }
29✔
812
        }
13✔
813

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

817
        for (size_t block_idx = num_producer_blocks; block_idx < consumer_body_->size(); ++block_idx) {
25✔
818
            auto* block = dynamic_cast<structured_control_flow::Block*>(&consumer_body_->at(block_idx).first);
13✔
819
            if (block == nullptr) {
13✔
UNCOV
820
                continue;
×
UNCOV
821
            }
×
822

823
            auto& dataflow = block->dataflow();
13✔
824

825
            for (auto& node : dataflow.nodes()) {
46✔
826
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
46✔
827
                if (access == nullptr || dataflow.out_degree(*access) == 0) {
46✔
828
                    continue;
28✔
829
                }
28✔
830

831
                std::string original_container = access->data();
18✔
832

833
                for (auto& memlet : dataflow.out_edges(*access)) {
19✔
834
                    if (memlet.type() != data_flow::MemletType::Computational) {
19✔
UNCOV
835
                        continue;
×
UNCOV
836
                    }
×
837

838
                    const auto& memlet_subset = memlet.subset();
19✔
839

840
                    for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
24✔
841
                        auto& candidate = fusion_candidates_[cand_idx];
20✔
842

843
                        if (original_container != candidate.container) {
20✔
844
                            continue;
4✔
845
                        }
4✔
846

847
                        if (memlet_subset.size() != candidate.consumer_subset.size()) {
16✔
UNCOV
848
                            continue;
×
UNCOV
849
                        }
×
850

851
                        bool subset_matches = true;
16✔
852
                        for (size_t d = 0; d < memlet_subset.size(); ++d) {
34✔
853
                            if (!symbolic::eq(memlet_subset[d], candidate.consumer_subset[d])) {
19✔
854
                                subset_matches = false;
1✔
855
                                break;
1✔
856
                            }
1✔
857
                        }
19✔
858

859
                        if (!subset_matches) {
16✔
860
                            continue;
1✔
861
                        }
1✔
862

863
                        const auto& temp_name = candidate_temps[cand_idx];
15✔
864
                        auto& temp_type = sdfg.type(temp_name);
15✔
865

866
                        access->data(temp_name);
15✔
867

868
                        memlet.set_subset({});
15✔
869
                        memlet.set_base_type(temp_type);
15✔
870

871
                        for (auto& in_edge : dataflow.in_edges(*access)) {
15✔
UNCOV
872
                            in_edge.set_subset({});
×
UNCOV
873
                            in_edge.set_base_type(temp_type);
×
UNCOV
874
                        }
×
875

876
                        break;
15✔
877
                    }
16✔
878
                }
19✔
879
            }
18✔
880
        }
13✔
881

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

887
        std::vector<std::string> candidate_temps;
3✔
888
        auto& producer_dataflow = producer_block_->dataflow();
3✔
889

890
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
6✔
891
            auto& candidate = fusion_candidates_[cand_idx];
3✔
892

893
            auto& container_type = sdfg.type(candidate.container);
3✔
894
            std::string temp_name = builder.find_new_name("_fused_tmp");
3✔
895
            types::Scalar tmp_type(container_type.primitive_type());
3✔
896
            builder.add_container(temp_name, tmp_type);
3✔
897
            candidate_temps.push_back(temp_name);
3✔
898

899
            // Step 1: Modify the original producer block to write to _fused_tmp
900
            data_flow::Subset original_write_subset;
3✔
901
            for (auto& node : producer_dataflow.nodes()) {
6✔
902
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
6✔
903
                if (access == nullptr || access->data() != candidate.container) continue;
6✔
904
                if (producer_dataflow.in_degree(*access) == 0) continue;
3✔
905

906
                // This is the write access node — save the original subset, then redirect
907
                for (auto& in_edge : producer_dataflow.in_edges(*access)) {
3✔
908
                    original_write_subset = in_edge.subset();
3✔
909
                    in_edge.set_subset({});
3✔
910
                    in_edge.set_base_type(tmp_type);
3✔
911
                }
3✔
912
                access->data(temp_name);
3✔
913
                break;
3✔
914
            }
3✔
915

916
            // Step 2: Add a writeback block: container[original_subset] = _fused_tmp
917
            control_flow::Assignments empty_assignments;
3✔
918
            auto& wb_block = builder.add_block_after(*producer_body_, *producer_block_, empty_assignments);
3✔
919
            auto& wb_src = builder.add_access(wb_block, temp_name);
3✔
920
            auto& wb_dst = builder.add_access(wb_block, candidate.container);
3✔
921
            auto& wb_tasklet = builder.add_tasklet(wb_block, data_flow::TaskletCode::assign, "_out", {"_in"});
3✔
922
            builder.add_computational_memlet(wb_block, wb_src, wb_tasklet, "_in", {});
3✔
923
            builder.add_computational_memlet(wb_block, wb_tasklet, "_out", wb_dst, original_write_subset);
3✔
924

925
            // Step 3: Copy consumer blocks after the writeback block
926
            structured_control_flow::ControlFlowNode* last_inserted = &wb_block;
3✔
927

928
            for (size_t i = 0; i < consumer_body_->size(); ++i) {
6✔
929
                auto* consumer_block = dynamic_cast<structured_control_flow::Block*>(&consumer_body_->at(i).first);
3✔
930
                if (consumer_block == nullptr) {
3✔
UNCOV
931
                    continue;
×
UNCOV
932
                }
×
933

934
                auto& consumer_dataflow = consumer_block->dataflow();
3✔
935

936
                // Check if this block reads from the fusion container
937
                bool reads_container = false;
3✔
938
                for (auto& node : consumer_dataflow.nodes()) {
11✔
939
                    auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
11✔
940
                    if (access != nullptr && access->data() == candidate.container &&
11✔
941
                        consumer_dataflow.out_degree(*access) > 0) {
11✔
942
                        reads_container = true;
3✔
943
                        break;
3✔
944
                    }
3✔
945
                }
11✔
946
                if (!reads_container) {
3✔
UNCOV
947
                    continue;
×
UNCOV
948
                }
×
949

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

953
                // Deep copy all nodes from consumer block
954
                std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
3✔
955
                for (auto& node : consumer_dataflow.nodes()) {
11✔
956
                    node_mapping[&node] = &builder.copy_node(new_block, node);
11✔
957
                    auto* copied = node_mapping[&node];
11✔
958
                    if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
11✔
959
                        if (access_node->data() == candidate.container) {
8✔
960
                            // Only rename read access nodes to temp; keep write access nodes
961
                            // pointing to the original container
962
                            if (consumer_dataflow.in_degree(node) == 0) {
4✔
963
                                access_node->data(temp_name);
3✔
964
                            }
3✔
965
                        }
4✔
966
                    }
8✔
967
                }
11✔
968

969
                // Add memlets with index substitution (consumer indvars → producer expressions)
970
                for (auto& edge : consumer_dataflow.edges()) {
8✔
971
                    auto& src_node = edge.src();
8✔
972
                    auto& dst_node = edge.dst();
8✔
973

974
                    const types::IType* base_type = &edge.base_type();
8✔
975
                    data_flow::Subset new_subset;
8✔
976
                    for (const auto& dim : edge.subset()) {
9✔
977
                        auto new_dim = dim;
9✔
978
                        for (const auto& [cvar, mapping] : candidate.index_mappings) {
13✔
979
                            new_dim = symbolic::subs(new_dim, cvar, mapping);
13✔
980
                        }
13✔
981
                        new_dim = symbolic::expand(new_dim);
9✔
982
                        new_subset.push_back(new_dim);
9✔
983
                    }
9✔
984

985
                    // For read edges from temp scalar, use empty subset
986
                    auto* src_access = dynamic_cast<data_flow::AccessNode*>(&src_node);
8✔
987
                    if (src_access != nullptr && src_access->data() == candidate.container &&
8✔
988
                        consumer_dataflow.in_degree(*src_access) == 0) {
8✔
989
                        new_subset.clear();
3✔
990
                        base_type = &tmp_type;
3✔
991
                    }
3✔
992

993
                    builder.add_memlet(
8✔
994
                        new_block,
8✔
995
                        *node_mapping[&src_node],
8✔
996
                        edge.src_conn(),
8✔
997
                        *node_mapping[&dst_node],
8✔
998
                        edge.dst_conn(),
8✔
999
                        new_subset,
8✔
1000
                        *base_type,
8✔
1001
                        edge.debug_info()
8✔
1002
                    );
8✔
1003
                }
8✔
1004

1005
                last_inserted = &new_block;
3✔
1006
            }
3✔
1007
        }
3✔
1008

1009
        // Remove the consumer loop
1010
        auto& scope_analysis = analysis_manager.get<analysis::ScopeAnalysis>();
3✔
1011
        auto* parent = scope_analysis.parent_scope(&second_loop_);
3✔
1012
        auto* parent_seq = dynamic_cast<structured_control_flow::Sequence*>(parent);
3✔
1013
        if (parent_seq != nullptr) {
3✔
1014
            int idx = parent_seq->index(second_loop_);
3✔
1015
            if (idx >= 0) {
3✔
1016
                builder.remove_child(*parent_seq, static_cast<size_t>(idx));
3✔
1017
            }
3✔
1018
        }
3✔
1019
    }
3✔
1020

1021
    analysis_manager.invalidate_all();
15✔
1022
    applied_ = true;
15✔
1023
}
15✔
1024

1025
void MapFusion::to_json(nlohmann::json& j) const {
1✔
1026
    std::string second_type = "for";
1✔
1027
    if (dynamic_cast<structured_control_flow::Map*>(&second_loop_) != nullptr) {
1✔
1028
        second_type = "map";
1✔
1029
    }
1✔
1030
    j["transformation_type"] = this->name();
1✔
1031
    j["subgraph"] = {
1✔
1032
        {"0", {{"element_id", first_map_.element_id()}, {"type", "map"}}},
1✔
1033
        {"1", {{"element_id", second_loop_.element_id()}, {"type", second_type}}}
1✔
1034
    };
1✔
1035
}
1✔
1036

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

1041
    auto first_element = builder.find_element_by_id(first_map_id);
1✔
1042
    auto second_element = builder.find_element_by_id(second_loop_id);
1✔
1043

1044
    if (first_element == nullptr) {
1✔
UNCOV
1045
        throw InvalidTransformationDescriptionException("Element with ID " + std::to_string(first_map_id) + " not found.");
×
UNCOV
1046
    }
×
1047
    if (second_element == nullptr) {
1✔
UNCOV
1048
        throw InvalidTransformationDescriptionException(
×
UNCOV
1049
            "Element with ID " + std::to_string(second_loop_id) + " not found."
×
UNCOV
1050
        );
×
UNCOV
1051
    }
×
1052

1053
    auto* first_map = dynamic_cast<structured_control_flow::Map*>(first_element);
1✔
1054
    auto* second_loop = dynamic_cast<structured_control_flow::StructuredLoop*>(second_element);
1✔
1055

1056
    if (first_map == nullptr) {
1✔
UNCOV
1057
        throw InvalidTransformationDescriptionException(
×
UNCOV
1058
            "Element with ID " + std::to_string(first_map_id) + " is not a Map."
×
UNCOV
1059
        );
×
UNCOV
1060
    }
×
1061
    if (second_loop == nullptr) {
1✔
UNCOV
1062
        throw InvalidTransformationDescriptionException(
×
UNCOV
1063
            "Element with ID " + std::to_string(second_loop_id) + " is not a StructuredLoop."
×
UNCOV
1064
        );
×
UNCOV
1065
    }
×
1066

1067
    return MapFusion(*first_map, *second_loop);
1✔
1068
}
1✔
1069

1070
} // namespace transformations
1071
} // 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