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

daisytuner / docc / 24227083775

09 Apr 2026 11:03PM UTC coverage: 64.394% (-0.005%) from 64.399%
24227083775

push

github

web-flow
Merge pull request #669 from daisytuner/map-fusion-lookahead-1

allows map fusion pass a lookahead-1

5 of 7 new or added lines in 1 file covered. (71.43%)

3 existing lines in 1 file now uncovered.

29707 of 46133 relevant lines covered (64.39%)

588.55 hits per line

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

82.67
/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) {}
37✔
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;
22✔
50
        }
22✔
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;
22✔
57
        }
22✔
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) {
80✔
148
        if (assump.constant() && unconstrained_producer.find(sym) == unconstrained_producer.end()) {
80✔
149
            unconstrained_producer[sym] = assump;
40✔
150
        }
40✔
151
    }
80✔
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) {
35✔
338
    fusion_candidates_.clear();
35✔
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) {
35✔
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>();
35✔
347
    auto* first_parent = scope_analysis.parent_scope(&first_map_);
35✔
348
    auto* second_parent = scope_analysis.parent_scope(&second_loop_);
35✔
349
    if (first_parent == nullptr || second_parent == nullptr) {
35✔
350
        return false;
×
351
    }
×
352
    if (first_parent != second_parent) {
35✔
353
        return false;
×
354
    }
×
355

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

567
    // Get assumptions for the resolved write/read locations
568
    auto& assumptions_analysis = analysis_manager.get<analysis::AssumptionsAnalysis>();
29✔
569
    auto& producer_assumptions = assumptions_analysis.get(*producer_block_);
29✔
570
    auto& consumer_assumptions = assumptions_analysis.get(consumer_body_->at(0).first);
29✔
571

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

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

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

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

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

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

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

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

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

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

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

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

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

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

739
            fusion_candidates_.push_back(candidate);
26✔
740
        }
26✔
741
    }
28✔
742

743
    // Criterion: At least one valid fusion candidate
744
    return !fusion_candidates_.empty();
23✔
745
}
29✔
746

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

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

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

757
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
25✔
758
            auto& candidate = fusion_candidates_[cand_idx];
13✔
759

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

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

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

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

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

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

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

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

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

829
            auto& dataflow = block->dataflow();
13✔
830

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

837
                std::string original_container = access->data();
18✔
838

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

844
                    const auto& memlet_subset = memlet.subset();
19✔
845

846
                    for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
24✔
847
                        auto& candidate = fusion_candidates_[cand_idx];
20✔
848

849
                        if (original_container != candidate.container) {
20✔
850
                            continue;
4✔
851
                        }
4✔
852

853
                        if (memlet_subset.size() != candidate.consumer_subset.size()) {
16✔
854
                            continue;
×
855
                        }
×
856

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

865
                        if (!subset_matches) {
16✔
866
                            continue;
1✔
867
                        }
1✔
868

869
                        const auto& temp_name = candidate_temps[cand_idx];
15✔
870
                        auto& temp_type = sdfg.type(temp_name);
15✔
871

872
                        access->data(temp_name);
15✔
873

874
                        memlet.set_subset({});
15✔
875
                        memlet.set_base_type(temp_type);
15✔
876

877
                        for (auto& in_edge : dataflow.in_edges(*access)) {
15✔
878
                            in_edge.set_subset({});
×
879
                            in_edge.set_base_type(temp_type);
×
880
                        }
×
881

882
                        break;
15✔
883
                    }
16✔
884
                }
19✔
885
            }
18✔
886
        }
13✔
887

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

893
        std::vector<std::string> candidate_temps;
3✔
894
        auto& producer_dataflow = producer_block_->dataflow();
3✔
895

896
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
6✔
897
            auto& candidate = fusion_candidates_[cand_idx];
3✔
898

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

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

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

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

931
            // Step 3: Copy consumer blocks after the writeback block
932
            structured_control_flow::ControlFlowNode* last_inserted = &wb_block;
3✔
933

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

940
                auto& consumer_dataflow = consumer_block->dataflow();
3✔
941

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

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

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

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

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

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

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

1011
                last_inserted = &new_block;
3✔
1012
            }
3✔
1013
        }
3✔
1014

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

1027
    analysis_manager.invalidate_all();
15✔
1028
    applied_ = true;
15✔
1029
}
15✔
1030

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

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

1047
    auto first_element = builder.find_element_by_id(first_map_id);
1✔
1048
    auto second_element = builder.find_element_by_id(second_loop_id);
1✔
1049

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

1059
    auto* first_map = dynamic_cast<structured_control_flow::Map*>(first_element);
1✔
1060
    auto* second_loop = dynamic_cast<structured_control_flow::StructuredLoop*>(second_element);
1✔
1061

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

1073
    return MapFusion(*first_map, *second_loop);
1✔
1074
}
1✔
1075

1076
} // namespace transformations
1077
} // 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