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

Alan-Jowett / ebpf-verifier / 29279923487

12 Jul 2026 11:38AM UTC coverage: 86.649% (+0.3%) from 86.386%
29279923487

push

github

web-flow
Drive sample verification tests from the inventory; parallelize CI (#1195)

Full-program verification of the ebpf-samples corpus was 17 C++ files
generated by scripts/generate_verify_project_tests.py from
test-data/elf_inventory.json, then compiled into the test binary. The
generated files duplicated the inventory and could drift from it (there
was no check keeping them in sync), and the whole corpus ran in one
single-threaded Catch2 process.

Data-driven tests (dedup)
-------------------------
- Add src/test/test_verify_samples.cpp, which reads elf_inventory.json at
  runtime (via yaml-cpp; JSON is a subset of YAML) and registers one
  Catch2 DYNAMIC_SECTION per (object, section[, program]). Adding or
  retagging a sample is now a JSON edit: no code generation, no recompile.
- Delete the 17 generated test_verify_<project>.cpp files and
  scripts/generate_verify_project_tests.py. The inventory is the single
  source of truth. A coverage-guard test asserts the registered projects
  match the inventory exactly, so a new project cannot be left untested.
- Trim test_verify.hpp to the ELF-parse cache and the VerifyIssueKind
  taxonomy the driver and the multithreading test still use.

  Catch2's [!shouldfail] is a compile-time per-TEST_CASE tag and cannot be
  applied per data-driven entry, so an expected_failure (a safe program
  the verifier is currently too imprecise to accept) is asserted as
  *still rejected* -- an xfail/golden marker. A verifier improvement that
  starts accepting it fails the case, prompting an inventory update; a
  regression on a passing program fails it too.

Parallel CI (ctest)
-------------------
- Register ctest entries: one per project (sharded by tag) plus a "unit"
  entry for everything else. The shard list is derived from the inventory
  via string(JSON), so it cannot drift from the corpus.
- CI runs `ctest -j` instead of a single `bin/tests` process. Locally, a
  full run drops from ~104s to ~42s on 16 cores (bounded by t... (continued)

9313 of 10748 relevant lines covered (86.65%)

6391896.2 hits per line

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

88.65
/src/ir/cfg_builder.cpp
1
// Copyright (c) Prevail Verifier contributors.
2
// SPDX-License-Identifier: MIT
3
#include <algorithm>
4
#include <cassert>
5
#include <limits>
6
#include <map>
7
#include <optional>
8
#include <set>
9
#include <string>
10
#include <vector>
11

12
#include "cfg/cfg.hpp"
13
#include "cfg/wto.hpp"
14
#include "config.hpp"
15
#include "ir/call_resolver.hpp"
16
#include "ir/program.hpp"
17
#include "ir/syntax.hpp"
18
#include "platform.hpp"
19

20
using std::optional;
21
using std::set;
22
using std::string;
23
using std::to_string;
24
using std::vector;
25

26
namespace prevail {
27
struct CallbackMetadata {
2,078✔
28
    std::set<int32_t> target_labels;
29
    std::set<int32_t> targets_with_exit;
30
};
31

32
struct CfgBuilder final {
8✔
33
    Program prog;
34

35
    // TODO: ins should be inserted elsewhere
36
    void insert_after(const Label& prev_label, const Label& new_label, const Instruction& ins) {
44✔
37
        if (prev_label == new_label) {
44✔
38
            CRAB_ERROR("Cannot insert after the same label ", to_string(new_label));
×
39
        }
40
        std::set<Label> prev_children;
44✔
41
        std::swap(prev_children, prog.m_cfg.get_node(prev_label).children);
44✔
42

43
        for (const Label& next_label : prev_children) {
96✔
44
            prog.m_cfg.get_node(next_label).parents.erase(prev_label);
52✔
45
        }
46

47
        insert(new_label, ins);
44✔
48
        for (const Label& next_label : prev_children) {
96✔
49
            add_child(prev_label, new_label);
52✔
50
            add_child(new_label, next_label);
52✔
51
        }
52
    }
44✔
53

54
    // TODO: ins should be inserted elsewhere
55
    void insert(const Label& _label, const Instruction& ins) {
1,318,088✔
56
        if (const auto it = prog.m_cfg.neighbours.find(_label); it != prog.m_cfg.neighbours.end()) {
1,318,088✔
57
            CRAB_ERROR("Label ", to_string(_label), " already exists");
×
58
        }
59
        prog.m_cfg.neighbours.emplace(_label, Cfg::Adjacent{});
1,318,088✔
60
        prog.m_instructions.emplace(_label, ins);
1,318,088✔
61
    }
1,318,088✔
62

63
    // TODO: ins should be inserted elsewhere
64
    Label insert_jump(const Label& from, const Label& to, const Instruction& ins) {
198,764✔
65
        const Label jump_label = Label::make_jump(from, to);
198,764✔
66
        if (prog.m_cfg.contains(jump_label)) {
198,764✔
67
            CRAB_ERROR("Jump label ", to_string(jump_label), " already exists");
×
68
        }
69
        insert(jump_label, ins);
198,764✔
70
        add_child(from, jump_label);
198,764✔
71
        add_child(jump_label, to);
198,764✔
72
        return jump_label;
198,764✔
73
    }
×
74

75
    void add_child(const Label& a, const Label& b) {
1,426,042✔
76
        assert(b != Label::entry);
1,426,042✔
77
        assert(a != Label::exit);
1,426,042✔
78
        prog.m_cfg.neighbours.at(a).children.insert(b);
1,426,042✔
79
        prog.m_cfg.neighbours.at(b).parents.insert(a);
1,426,042✔
80
    }
1,426,042✔
81

82
    void remove_child(const Label& a, const Label& b) {
710✔
83
        prog.m_cfg.get_node(a).children.erase(b);
710✔
84
        prog.m_cfg.get_node(b).parents.erase(a);
710✔
85
    }
710✔
86

87
    void set_assertions(const Label& label, const std::vector<Assertion>& assertions) {
1,326,146✔
88
        if (!prog.m_cfg.contains(label)) {
1,326,146✔
89
            CRAB_ERROR("Label ", to_string(label), " not found in the CFG: ");
×
90
        }
91
        prog.m_assertions.insert_or_assign(label, assertions);
1,326,146✔
92
    }
1,326,146✔
93

94
    void set_callback_metadata(CallbackMetadata md) {
4,156✔
95
        prog.m_callback_target_labels = std::move(md.target_labels);
4,156✔
96
        prog.m_callback_targets_with_exit = std::move(md.targets_with_exit);
4,156✔
97
    }
4,156✔
98
};
99

100
/// Get the inverse of a given comparison operation.
101
static Condition::Op reverse(const Condition::Op op) {
99,382✔
102
    switch (op) {
99,382✔
103
    case Condition::Op::EQ: return Condition::Op::NE;
22,662✔
104
    case Condition::Op::NE: return Condition::Op::EQ;
13,859✔
105

106
    case Condition::Op::GE: return Condition::Op::LT;
366✔
107
    case Condition::Op::LT: return Condition::Op::GE;
1,315✔
108

109
    case Condition::Op::SGE: return Condition::Op::SLT;
56✔
110
    case Condition::Op::SLT: return Condition::Op::SGE;
3,210✔
111

112
    case Condition::Op::LE: return Condition::Op::GT;
141✔
113
    case Condition::Op::GT: return Condition::Op::LE;
4,696✔
114

115
    case Condition::Op::SLE: return Condition::Op::SGT;
22✔
116
    case Condition::Op::SGT: return Condition::Op::SLE;
3,345✔
117

118
    case Condition::Op::SET: return Condition::Op::NSET;
17✔
119
    case Condition::Op::NSET: return Condition::Op::SET;
2✔
120
    }
121
    std::unreachable();
122
}
123

124
/// Get the inverse of a given comparison condition.
125
static Condition reverse(const Condition& cond) {
99,382✔
126
    return {.op = reverse(cond.op), .left = cond.left, .right = cond.right, .is64 = cond.is64};
99,382✔
127
}
128

129
static bool has_fall(const Instruction& ins) {
971,762✔
130
    if (std::holds_alternative<Exit>(ins)) {
971,762✔
131
        return false;
3,034✔
132
    }
133

134
    if (const auto pins = std::get_if<Jmp>(&ins)) {
965,694✔
135
        if (!pins->cond) {
502✔
136
            return false;
251✔
137
        }
138
    }
139

140
    return true;
482,596✔
141
}
142

143
enum class RejectKind {
144
    NotImplemented,
145
    Capability,
146
};
147

148
struct RejectionReason {
46✔
149
    RejectKind kind{};
150
    std::string detail;
151
};
152

153
static bool supports(const ebpf_platform_t& platform, const bpf_conformance_groups_t group) {
1,656,470✔
154
    return platform.supports_group(group);
1,651,894✔
155
}
156

157
static bool un_requires_base64(const Un& un) {
11,412✔
158
    switch (un.op) {
11,412✔
159
    case Un::Op::BE64:
49✔
160
    case Un::Op::LE64:
161
    case Un::Op::SWAP64: return true;
49✔
162
    default: return false;
7,558✔
163
    }
164
}
165

166
static std::optional<ResolvedCall> resolve_kfunc_call(const CallBtf& call_btf, const ProgramInfo& info,
64✔
167
                                                      std::string* why_not) {
168
    if (!info.platform || !info.platform->resolve_kfunc_call) {
64✔
169
        if (why_not) {
×
170
            *why_not = "kfunc resolution is unavailable on this platform";
×
171
        }
172
        return std::nullopt;
×
173
    }
174
    return info.platform->resolve_kfunc_call(call_btf.btf_id, call_btf.module, info.type, why_not);
64✔
175
}
176

177
// CallBtf in the instruction stream is replaced by a key-only Call{func,
178
// kind=kfunc}; the ResolvedCall is reproduced on demand via resolve(call, info).
179
using ResolvedKfuncCalls = std::map<Label, Call>;
180

181
[[nodiscard]]
182
static std::optional<RejectionReason> check_instruction_feature_support(const Instruction& ins,
1,645,663✔
183
                                                                        const ProgramInfo& info) {
184
    const ebpf_platform_t& platform = *info.platform;
1,645,663✔
185
    auto reject_not_implemented = [](std::string detail) {
550,501✔
186
        return RejectionReason{.kind = RejectKind::NotImplemented, .detail = std::move(detail)};
×
187
    };
188
    auto reject_capability = [](std::string detail) {
550,547✔
189
        return RejectionReason{.kind = RejectKind::Capability, .detail = std::move(detail)};
46✔
190
    };
191

192
    if (const auto p = std::get_if<Call>(&ins)) {
1,645,663✔
193
        const auto resolved = resolve(*p, info);
89,846✔
194
        if (!resolved.is_supported) {
89,846✔
195
            return reject_capability(resolved.unsupported_reason);
9✔
196
        }
197
    }
89,846✔
198
    if (std::holds_alternative<Callx>(ins) && !supports(platform, bpf_conformance_groups_t::callx)) {
1,645,657✔
199
        return reject_capability("requires conformance group callx");
×
200
    }
201
    if ((std::holds_alternative<Call>(ins) || std::holds_alternative<CallLocal>(ins) ||
3,715,448✔
202
         std::holds_alternative<Callx>(ins) || std::holds_alternative<CallBtf>(ins) ||
3,623,353✔
203
         std::holds_alternative<Exit>(ins)) &&
4,295,169✔
204
        !supports(platform, bpf_conformance_groups_t::base32)) {
97,281✔
205
        return reject_capability("requires conformance group base32");
×
206
    }
207
    if (const auto p = std::get_if<Bin>(&ins)) {
1,934,933✔
208
        if (!supports(platform, p->is64 ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)) {
955,083✔
209
            return reject_capability(p->is64 ? "requires conformance group base64"
×
210
                                             : "requires conformance group base32");
×
211
        }
212
        if ((p->op == Bin::Op::MUL || p->op == Bin::Op::UDIV || p->op == Bin::Op::UMOD || p->op == Bin::Op::SDIV ||
860,689✔
213
             p->op == Bin::Op::SMOD) &&
1,441,350✔
214
            !supports(platform, p->is64 ? bpf_conformance_groups_t::divmul64 : bpf_conformance_groups_t::divmul32)) {
13,618✔
215
            return reject_capability(p->is64 ? "requires conformance group divmul64"
×
216
                                             : "requires conformance group divmul32");
×
217
        }
218
    }
219
    if (const auto p = std::get_if<Un>(&ins)) {
1,645,656✔
220
        const bool need_base64 = p->is64 || un_requires_base64(*p);
15,624✔
221
        if (!supports(platform, need_base64 ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)) {
11,717✔
222
            return reject_capability(need_base64 ? "requires conformance group base64"
6✔
223
                                                 : "requires conformance group base32");
2✔
224
        }
225
    }
226
    if (const auto p = std::get_if<Jmp>(&ins)) {
1,645,655✔
227
        if (!supports(platform,
283,735✔
228
                      p->cond ? (p->cond->is64 ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)
283,735✔
229
                              : bpf_conformance_groups_t::base32)) {
230
            return reject_capability((p->cond && p->cond->is64) ? "requires conformance group base64"
×
231
                                                                : "requires conformance group base32");
×
232
        }
233
    }
234
    if (const auto p = std::get_if<LoadPseudo>(&ins)) {
1,645,669✔
235
        if (!supports(platform, bpf_conformance_groups_t::base64)) {
40✔
236
            return reject_capability("requires conformance group base64");
×
237
        }
238
        switch (p->addr.kind) {
40✔
239
        case PseudoAddress::Kind::VARIABLE_ADDR:
26✔
240
        case PseudoAddress::Kind::CODE_ADDR:
241
        case PseudoAddress::Kind::MAP_BY_IDX:
242
        case PseudoAddress::Kind::MAP_VALUE_BY_IDX: break; // Resolved during CFG construction.
26✔
243
        default: return reject_not_implemented("lddw unknown pseudo");
×
244
        }
245
    }
246
    if ((std::holds_alternative<LoadMapFd>(ins) || std::holds_alternative<LoadMapAddress>(ins)) &&
1,686,472✔
247
        !supports(platform, bpf_conformance_groups_t::base64)) {
61,582✔
248
        return reject_capability("requires conformance group base64");
×
249
    }
250
    if (const auto p = std::get_if<Mem>(&ins)) {
1,645,655✔
251
        if (!supports(platform,
423,027✔
252
                      (p->access.width == 8) ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)) {
423,027✔
253
            return reject_capability((p->access.width == 8) ? "requires conformance group base64"
×
254
                                                            : "requires conformance group base32");
×
255
        }
256
        if (p->is_signed && !supports(platform, bpf_conformance_groups_t::base64)) {
423,027✔
257
            return reject_capability("requires conformance group base64");
×
258
        }
259
    }
260
    if (std::holds_alternative<Packet>(ins) && !supports(platform, bpf_conformance_groups_t::packet)) {
1,645,655✔
261
        return reject_capability("requires conformance group packet");
76✔
262
    }
263
    if (const auto p = std::get_if<Atomic>(&ins)) {
1,645,617✔
264
        const auto group =
2,620✔
265
            (p->access.width == 8) ? bpf_conformance_groups_t::atomic64 : bpf_conformance_groups_t::atomic32;
1,965✔
266
        if (!supports(platform, group)) {
1,965✔
267
            return reject_capability((group == bpf_conformance_groups_t::atomic64)
×
268
                                         ? "requires conformance group atomic64"
269
                                         : "requires conformance group atomic32");
×
270
        }
271
    }
272
    return {};
1,645,617✔
273
}
274

275
// Pass: ValidateInstructionSupport
276
// Throwing wrapper around the value-returning check_instruction_feature_support.
277
// Single named place that converts a rejection into the throw shape; the
278
// value-returning checker is preserved for sites that want to query without
279
// throwing (see the assert at pass_populate_nodes).
280
[[noreturn]]
281
static void throw_unsupported(const RejectionReason& reason, const Label& label) {
46✔
282
    const std::string prefix = (reason.kind == RejectKind::NotImplemented) ? "not implemented: " : "rejected: ";
69✔
283
    throw InvalidControlFlow{prefix + reason.detail + " (at " + to_string(label) + ")"};
92✔
284
}
46✔
285

286
static void enforce_instruction_feature_support(const Instruction& ins, const Label& label, const ProgramInfo& info) {
1,101,002✔
287
    if (const auto reason = check_instruction_feature_support(ins, info)) {
1,101,002✔
288
        throw_unsupported(*reason, label);
46✔
289
    }
1,101,002✔
290
}
1,100,956✔
291

292
// Reads    : instruction sequence, platform conformance groups.
293
// Writes   : nothing.
294
// Throws   : InvalidControlFlow on any instruction the platform cannot run.
295
// Invariant: must run before CFG construction; pass_populate_nodes assumes
296
//            every instruction has been vetted here.
297
static void pass_validate_instruction_support(const InstructionSeq& insts, const ProgramInfo& info) {
4,248✔
298
    for (const auto& [label, inst, _] : insts) {
1,105,204✔
299
        enforce_instruction_feature_support(inst, label, info);
1,101,002✔
300
    }
301
}
4,202✔
302

303
// Pass: ResolveKfuncCalls
304
// Reads    : instruction sequence, platform kfunc resolver.
305
// Writes   : returns a Label -> Call map for every CallBtf in the sequence.
306
// Throws   : InvalidControlFlow if any CallBtf cannot be resolved for this platform.
307
// Invariant: pass_populate_nodes consults this map to replace CallBtf with the resolved Call.
308
static ResolvedKfuncCalls pass_resolve_kfunc_calls(const InstructionSeq& insts, const ProgramInfo& info) {
4,202✔
309
    ResolvedKfuncCalls resolved;
4,202✔
310
    for (const auto& [label, inst, _] : insts) {
1,093,752✔
311
        const auto* call_btf = std::get_if<CallBtf>(&inst);
1,089,576✔
312
        if (!call_btf) {
1,089,576✔
313
            continue;
1,089,512✔
314
        }
315
        std::string why_not;
64✔
316
        const auto r = resolve_kfunc_call(*call_btf, info, &why_not);
64✔
317
        if (!r) {
64✔
318
            throw InvalidControlFlow{"not implemented: " + why_not + " (at " + to_string(label) + ")"};
52✔
319
        }
320
        // Build the lowered Call key directly from the source CallBtf rather
321
        // than trusting any field the platform returned. The lowered IR's
322
        // identity is the pre-resolution (btf_id, module) pair plus the fixed
323
        // CallKind::kfunc tag; nothing else can soundly change it. Constructing
324
        // the key here makes that invariant audit-visible and removes a class
325
        // of bugs where a misbehaving resolver mis-keys the lowered IR — in
326
        // particular, two kfuncs sharing a BTF id across modules must remain
327
        // distinguishable.
328
        const Call lowered{
38✔
329
            .func = call_btf->btf_id,
38✔
330
            .kind = CallKind::kfunc,
331
            .module = call_btf->module,
38✔
332
        };
38✔
333
        resolved.insert_or_assign(label, lowered);
38✔
334
    }
90✔
335
    return resolved;
4,176✔
336
}
26✔
337

338
/// Update a control-flow graph to inline function macros.
339
static void add_cfg_nodes(CfgBuilder& builder, const Label& caller_label, const Label& entry_label,
718✔
340
                          const int max_call_stack_frames) {
341
    const string caller_label_str = to_string(caller_label);
718✔
342
    const long stack_frame_depth = std::ranges::count(caller_label_str, STACK_FRAME_DELIMITER) + 2;
718✔
343
    if (stack_frame_depth > max_call_stack_frames) {
718✔
344
        throw InvalidControlFlow{"too many call stack frames"};
10✔
345
    }
346

347
    bool first = true;
714✔
348

349
    // Get the label of the node to go to on returning from the macro.
350
    Label exit_to_label = builder.prog.cfg().get_child(caller_label);
714✔
351

352
    // Construct the variable prefix to use for the new stack frame
353
    // and store a copy in the CallLocal instruction since the instruction-specific
354
    // labels may only exist until the CFG is simplified.
355
    const std::string stack_frame_prefix = to_string(caller_label);
714✔
356
    if (const auto pcall = std::get_if<CallLocal>(&builder.prog.instruction_at(caller_label))) {
1,071✔
357
        pcall->stack_frame_prefix = stack_frame_prefix;
714✔
358
    }
359

360
    // Walk the transitive closure of CFG nodes starting at entry_label and ending at
361
    // any exit instruction.
362
    std::set macro_labels{entry_label};
1,802✔
363
    std::set seen_labels{entry_label};
1,802✔
364
    while (!macro_labels.empty()) {
30,622✔
365
        Label macro_label = *macro_labels.begin();
29,912✔
366
        macro_labels.erase(macro_label);
29,912✔
367

368
        if (stack_frame_prefix == macro_label.stack_frame_prefix) {
29,912✔
369
            throw InvalidControlFlow{stack_frame_prefix + ": illegal recursion"};
6✔
370
        }
371

372
        // Clone the macro block into a new block with the new stack frame prefix.
373
        const Label label{macro_label.from, macro_label.to, stack_frame_prefix};
44,864✔
374
        auto inst = builder.prog.instruction_at(macro_label);
29,908✔
375
        if (const auto pexit = std::get_if<Exit>(&inst)) {
30,266✔
376
            pexit->stack_frame_prefix = label.stack_frame_prefix;
716✔
377
        }
378
        builder.insert(label, inst);
29,908✔
379

380
        if (first) {
29,908✔
381
            // Add an edge from the caller to the new block.
382
            first = false;
714✔
383
            builder.add_child(caller_label, label);
714✔
384
        }
385

386
        // Add an edge from any other predecessors.
387
        for (const auto& prev_macro_nodes = builder.prog.cfg().parents_of(macro_label);
44,862✔
388
             const auto& prev_macro_label : prev_macro_nodes) {
77,668✔
389
            const Label prev_label(prev_macro_label.from, prev_macro_label.to, to_string(caller_label));
32,806✔
390
            if (const auto& labels = builder.prog.cfg().labels();
32,806✔
391
                std::ranges::find(labels, prev_label) != labels.end()) {
32,806✔
392
                builder.add_child(prev_label, label);
32,800✔
393
            }
394
        }
32,806✔
395

396
        // Walk all successor nodes.
397
        for (const auto& next_macro_nodes = builder.prog.cfg().children_of(macro_label);
61,621✔
398
             const auto& next_macro_label : next_macro_nodes) {
78,380✔
399
            if (next_macro_label == builder.prog.cfg().exit_label()) {
50,277✔
400
                // This is an exit transition, so add edge to the block to execute
401
                // upon returning from the macro.
402
                builder.add_child(label, exit_to_label);
714✔
403
            } else if (!seen_labels.contains(next_macro_label)) {
32,804✔
404
                // Push any other unprocessed successor label onto the list to be processed.
405
                if (!macro_labels.contains(next_macro_label)) {
29,198✔
406
                    macro_labels.insert(next_macro_label);
29,198✔
407
                }
408
                seen_labels.insert(next_macro_label);
29,198✔
409
            }
410
        }
411
    }
59,820✔
412

413
    // Remove the original edge from the caller node to its successor,
414
    // since processing now goes through the function macro instead.
415
    builder.remove_child(caller_label, exit_to_label);
710✔
416

417
    // Finally, recurse to replace any nested function macros.
418
    for (const auto& macro_label : seen_labels) {
30,546✔
419
        const Label label{macro_label.from, macro_label.to, caller_label_str};
44,816✔
420
        if (const auto pins = std::get_if<CallLocal>(&builder.prog.instruction_at(label))) {
44,784✔
421
            add_cfg_nodes(builder, label, pins->target, max_call_stack_frames);
132✔
422
        }
423
    }
29,866✔
424
}
2,603✔
425

426
struct Imm64Parts {
427
    int32_t lo{};
428
    int32_t hi{};
429
};
430

431
static uint64_t merge_imm32_to_u64(const Imm64Parts parts) {
4✔
432
    return static_cast<uint64_t>(static_cast<uint32_t>(parts.lo)) |
4✔
433
           (static_cast<uint64_t>(static_cast<uint32_t>(parts.hi)) << 32);
4✔
434
}
435

436
/// Lower a single LoadPseudo to a concrete instruction.
437
/// VARIABLE_ADDR is lowered to an immediate scalar MOV; MAP_BY_IDX / MAP_VALUE_BY_IDX are
438
/// rewritten against the current map descriptor table. CODE_ADDR is kept as LoadPseudo by
439
/// pass_lower_pseudo_loads so the abstract transformer can type it as T_FUNC; this helper
440
/// is never called for CODE_ADDR.
441
static Instruction lower_pseudo_load(const LoadPseudo& pseudo, const ProgramInfo& info) {
12✔
442
    if (pseudo.addr.kind == PseudoAddress::Kind::VARIABLE_ADDR) {
12✔
443
        return Bin{
10✔
444
            .op = Bin::Op::MOV,
445
            .dst = pseudo.dst,
446
            .v = Imm{merge_imm32_to_u64({.lo = pseudo.addr.imm, .hi = pseudo.addr.next_imm})},
6✔
447
            .is64 = true,
448
            .lddw = true,
449
        };
4✔
450
    }
451

452
    const auto& descriptors = info.map_descriptors;
8✔
453
    if (pseudo.addr.imm < 0 || static_cast<size_t>(pseudo.addr.imm) >= descriptors.size()) {
8✔
454
        throw InvalidControlFlow{"invalid map index " + std::to_string(pseudo.addr.imm) + " (have " +
10✔
455
                                 std::to_string(descriptors.size()) + " maps)"};
12✔
456
    }
457
    const auto map_idx = static_cast<size_t>(pseudo.addr.imm);
4✔
458
    const int mapfd = descriptors.at(map_idx).original_fd;
4✔
459
    switch (pseudo.addr.kind) {
4✔
460
    case PseudoAddress::Kind::MAP_BY_IDX: return LoadMapFd{.dst = pseudo.dst, .mapfd = mapfd};
2✔
461
    case PseudoAddress::Kind::MAP_VALUE_BY_IDX:
2✔
462
        return LoadMapAddress{.dst = pseudo.dst, .mapfd = mapfd, .offset = pseudo.addr.next_imm};
2✔
463
    default: CRAB_ERROR("Invalid address kind: ", static_cast<int>(pseudo.addr.kind));
×
464
    }
465
}
466

467
using LoweredPseudoLoads = std::map<Label, Instruction>;
468

469
// Pass: LowerPseudoLoads
470
// Reads    : instruction sequence, program info (map_descriptors).
471
// Writes   : returns a Label -> Instruction map with the concrete replacement for every
472
//            LoadPseudo that is lowered. CODE_ADDR LoadPseudo instructions are intentionally
473
//            excluded so they remain observable to the abstract transformer (which types
474
//            them as T_FUNC); every other kind is replaced.
475
// Throws   : InvalidControlFlow if a MAP_BY_IDX / MAP_VALUE_BY_IDX references an out-of-range
476
//            map descriptor.
477
// Invariant: pass_populate_nodes consults this map to substitute LoadPseudo with its lowered
478
//            form while inserting CFG nodes.
479
static LoweredPseudoLoads pass_lower_pseudo_loads(const InstructionSeq& insts, const ProgramInfo& info) {
4,176✔
480
    LoweredPseudoLoads lowered;
4,176✔
481
    for (const auto& [label, inst, _] : insts) {
1,093,498✔
482
        const auto* pseudo = std::get_if<LoadPseudo>(&inst);
1,089,326✔
483
        if (!pseudo || pseudo->addr.kind == PseudoAddress::Kind::CODE_ADDR) {
544,677✔
484
            continue;
1,089,314✔
485
        }
486
        lowered.insert_or_assign(label, lower_pseudo_load(*pseudo, info));
16✔
487
    }
488
    return lowered;
4,172✔
489
}
4✔
490

491
// Pass: BuildInitialCfg -- populate_nodes step.
492
// Reads    : instruction sequence, program info, resolved kfunc map, lowered pseudo-load map.
493
// Writes   : inserts one CFG node per live instruction into builder (labels + instructions).
494
//            CallBtf is replaced with the resolved Call; non-CODE_ADDR LoadPseudo is replaced
495
//            with its lowered form; every other instruction is inserted verbatim.
496
// Throws   : InvalidControlFlow if either substitution map is inconsistent with the sequence
497
//            (internal error; indicates a missing prior pass).
498
// Invariant: pass_validate_instruction_support, pass_resolve_kfunc_calls and
499
//            pass_lower_pseudo_loads have been applied on the same instruction sequence.
500
static void pass_populate_nodes(CfgBuilder& builder, const InstructionSeq& insts, const ProgramInfo& info,
4,172✔
501
                                const ResolvedKfuncCalls& resolved_kfunc_calls,
502
                                const LoweredPseudoLoads& lowered_pseudo_loads) {
503
    for (const auto& [label, inst, _] : insts) {
1,093,494✔
504
        assert(!check_instruction_feature_support(inst, info).has_value() &&
1,089,322✔
505
               "instruction support must be validated before CFG construction");
506
        if (std::holds_alternative<Undefined>(inst)) {
1,089,322✔
507
            continue;
×
508
        }
509
        if (std::holds_alternative<CallBtf>(inst)) {
1,089,322✔
510
            const auto it = resolved_kfunc_calls.find(label);
38✔
511
            if (it == resolved_kfunc_calls.end()) {
38✔
512
                CRAB_ERROR("missing validated kfunc resolution at ", to_string(label));
×
513
            }
514
            builder.insert(label, it->second);
38✔
515
            continue;
38✔
516
        }
38✔
517
        if (const auto it = lowered_pseudo_loads.find(label); it != lowered_pseudo_loads.end()) {
1,089,284✔
518
            builder.insert(label, it->second);
8✔
519
            continue;
8✔
520
        }
521
        builder.insert(label, inst);
1,089,276✔
522
    }
523
}
4,172✔
524

525
// Pass: BuildInitialCfg -- connect_edges step (also performs InsertAssumeEdges).
526
// Reads    : instruction sequence, must_have_exit flag.
527
// Writes   : CFG edges from entry, and for every populated node to its successors.
528
//            Conditional Jmp instructions are materialised as two synthetic Assume
529
//            jump-labels (insert_jump) carrying the positive and negated conditions.
530
// Throws   : InvalidControlFlow on empty sequence, fallthrough past the final instruction,
531
//            or a jump whose target label is not in the CFG.
532
// Invariant: pass_populate_nodes has been applied (all nodes exist before edges are added).
533
static void pass_connect_edges(CfgBuilder& builder, const InstructionSeq& insts, const bool must_have_exit) {
4,172✔
534
    if (insts.empty()) {
4,172✔
535
        throw InvalidControlFlow{"empty instruction sequence"};
5✔
536
    }
537
    // Ordering check: pass_populate_nodes must run first so that every non-Undefined label
538
    // referenced below (the entry's target, jump targets, fallthrough labels) already exists.
539
    assert(std::holds_alternative<Undefined>(std::get<1>(insts[0])) ||
4,170✔
540
           builder.prog.cfg().contains(std::get<0>(insts[0])));
541
    builder.add_child(builder.prog.cfg().entry_label(), std::get<0>(insts[0]));
4,170✔
542

543
    for (size_t i = 0; i < insts.size(); i++) {
1,093,486✔
544
        const auto& [label, inst, _0] = insts[i];
1,089,320✔
545

546
        if (std::holds_alternative<Undefined>(inst)) {
1,089,320✔
547
            continue;
172✔
548
        }
549
        Label fallthrough{builder.prog.cfg().exit_label()};
1,089,320✔
550
        if (i + 1 < insts.size()) {
1,089,320✔
551
            fallthrough = std::get<0>(insts[i + 1]);
1,085,152✔
552
        } else {
553
            if (has_fall(inst) && must_have_exit) {
5,072✔
554
                throw InvalidControlFlow{"fallthrough in last instruction"};
5✔
555
            }
556
        }
557
        if (const auto jmp = std::get_if<Jmp>(&inst)) {
1,089,318✔
558
            if (const auto cond = jmp->cond) {
121,724✔
559
                Label target_label = jmp->target;
99,556✔
560
                if (target_label == fallthrough) {
99,556✔
561
                    builder.add_child(label, fallthrough);
172✔
562
                    continue;
172✔
563
                }
564
                if (!builder.prog.cfg().contains(target_label)) {
99,384✔
565
                    throw InvalidControlFlow{"jump to undefined label " + to_string(target_label)};
3✔
566
                }
567
                builder.insert_jump(label, target_label, Assume{.cond = *cond, .is_implicit = true});
149,073✔
568
                builder.insert_jump(label, fallthrough, Assume{.cond = reverse(*cond), .is_implicit = true});
149,074✔
569
            } else {
99,556✔
570
                builder.add_child(label, jmp->target);
22,168✔
571
            }
572
        } else {
573
            if (has_fall(inst)) {
967,594✔
574
                builder.add_child(label, fallthrough);
963,886✔
575
            }
576
        }
577
        if (std::holds_alternative<Exit>(inst)) {
1,089,144✔
578
            builder.add_child(label, builder.prog.cfg().exit_label());
5,564✔
579
        }
580
    }
1,089,318✔
581
}
4,166✔
582

583
// Pass: InlineLocalCalls
584
// Reads    : instruction sequence, max_call_stack_frames bound.
585
// Writes   : for every CallLocal in the sequence, clones the callee region into the CFG
586
//            under a unique stack-frame prefix. Recurses into nested calls.
587
// Throws   : InvalidControlFlow on illegal recursion or exceeding the call-stack frame bound.
588
// Invariant: pass_connect_edges has been applied -- inlining walks existing parents/children.
589
//            Restricted to callees that are reachable after edge connection, which is why
590
//            this runs as a separate second pass rather than during population.
591
static void pass_inline_local_calls(CfgBuilder& builder, const InstructionSeq& insts, const int max_call_stack_frames) {
4,166✔
592
    // Ordering check: pass_connect_edges must have run. When insts is non-empty, its first
593
    // label has been wired as a child of Label::entry, so entry has at least one successor.
594
    assert(insts.empty() || !builder.prog.cfg().children_of(Label::entry).empty());
4,166✔
595
    for (const auto& [label, inst, _] : insts) {
1,093,426✔
596
        if (const auto pins = std::get_if<CallLocal>(&inst)) {
1,089,557✔
597
            add_cfg_nodes(builder, label, pins->target, max_call_stack_frames);
586✔
598
        }
599
    }
600
}
4,158✔
601

602
static bool is_tail_call_helper(const Call& call, const ebpf_platform_t& platform,
59,848✔
603
                                const EbpfProgramType& program_type) {
604
    if (call.kind != CallKind::helper) {
59,848✔
605
        return false;
1,248✔
606
    }
607
    if (!platform.is_helper_usable(call.func, program_type)) {
57,352✔
608
        return false;
609
    }
610
    return platform.get_helper_prototype(call.func, program_type).return_type ==
57,352✔
611
           EBPF_RETURN_TYPE_INTEGER_OR_NO_RETURN_IF_SUCCEED;
57,352✔
612
}
613

614
static bool is_tail_call_site(const Instruction& ins, const ebpf_platform_t& platform,
1,293,538✔
615
                              const EbpfProgramType& program_type) {
616
    if (const auto* call = std::get_if<Call>(&ins)) {
1,293,538✔
617
        return is_tail_call_helper(*call, platform, program_type);
59,848✔
618
    }
619
    if (std::holds_alternative<Callx>(ins)) {
1,233,690✔
620
        // At CFG-construction time, callx target ids are not available.
621
        // Conservatively treat callx as a potential tail-call site.
622
        return true;
50✔
623
    }
624
    return false;
616,820✔
625
}
626

627
static void collect_wto_labels(const CycleOrLabel& component, std::set<Label>& labels) {
1,292,906✔
628
    // Iterative (explicit work-stack) rather than recursive on cycle-nesting depth.
629
    // This runs unconditionally for every program via pass_validate_tail_call_depth,
630
    // so a crafted deeply-nested CFG must not be able to overflow the C++ stack here.
631
    std::vector<const CycleOrLabel*> stack{&component};
1,939,359✔
632
    while (!stack.empty()) {
2,586,528✔
633
        const CycleOrLabel* const current = stack.back();
1,293,622✔
634
        stack.pop_back();
1,293,622✔
635
        if (const auto plabel = std::get_if<Label>(current)) {
1,293,622✔
636
            labels.insert(*plabel);
1,293,538✔
637
            continue;
1,293,538✔
638
        }
639
        for (const auto& nested_component : *std::get<std::shared_ptr<WtoCycle>>(*current)) {
800✔
640
            stack.push_back(&nested_component);
716✔
641
        }
642
    }
643
}
1,292,906✔
644

645
// Pass: ValidateTailCallDepth
646
// Reads    : Program (CFG + instructions), Wto, platform, program type.
647
// Writes   : nothing.
648
// Throws   : InvalidControlFlow if the reachable tail-call chain exceeds the fixed limit.
649
// Notes    : Counts tail-call sites along the longest path through the reachable maximal-SCC DAG
650
//            so cycles do not inflate depth. Maximal SCCs are derived from WTO nesting: labels in
651
//            the same outermost WTO cycle are mutually reachable and form one maximal SCC.
652
static void pass_validate_tail_call_depth(const Program& prog, const Wto& wto, const ebpf_platform_t& platform,
4,158✔
653
                                          const EbpfProgramType& program_type) {
654
    constexpr int tail_call_chain_limit = 33;
4,158✔
655

656
    // WTO only covers labels reachable from entry.
657
    std::set<Label> reachable;
4,158✔
658
    for (const auto& component : wto) {
1,297,064✔
659
        collect_wto_labels(component, reachable);
1,292,906✔
660
    }
661

662
    // Partition reachable labels by maximal SCC representative:
663
    // the outermost containing WTO head, or the label itself if not in a cycle.
664
    std::map<Label, Label> maximal_scc_of;
4,158✔
665
    std::set<Label> maximal_scc_ids;
4,158✔
666
    for (const auto& label : reachable) {
1,297,696✔
667
        const Label scc_id = wto.nesting(label).outermost_head().value_or(label);
2,587,077✔
668
        maximal_scc_of.emplace(label, scc_id);
1,293,538✔
669
        maximal_scc_ids.insert(scc_id);
1,293,538✔
670
    }
1,293,538✔
671

672
    std::map<Label, int> tail_sites_per_scc;
4,158✔
673
    std::map<Label, std::optional<Label>> representative_tail_label;
4,158✔
674
    std::map<Label, std::set<Label>> dag_successors;
4,158✔
675
    std::map<Label, int> indegree;
4,158✔
676
    for (const auto& scc_id : maximal_scc_ids) {
1,297,064✔
677
        tail_sites_per_scc.emplace(scc_id, 0);
1,292,906✔
678
        representative_tail_label.emplace(scc_id, std::nullopt);
1,292,906✔
679
        dag_successors.emplace(scc_id, std::set<Label>{});
1,292,906✔
680
        indegree.emplace(scc_id, 0);
1,292,907✔
681
    }
682

683
    for (const auto& label : reachable) {
1,297,696✔
684
        const Label src_scc = maximal_scc_of.at(label);
1,293,538✔
685
        if (is_tail_call_site(prog.instruction_at(label), platform, program_type)) {
1,293,538✔
686
            ++tail_sites_per_scc.at(src_scc);
1,648✔
687
            auto& representative = representative_tail_label.at(src_scc);
1,648✔
688
            if (!representative.has_value()) {
1,648✔
689
                representative = label;
1,648✔
690
            }
691
        }
692
        for (const auto& child : prog.cfg().children_of(label)) {
2,682,380✔
693
            if (!reachable.contains(child)) {
1,388,842✔
694
                continue;
×
695
            }
696
            const Label dst_scc = maximal_scc_of.at(child);
1,388,842✔
697
            if (src_scc != dst_scc && dag_successors[src_scc].insert(dst_scc).second) {
1,388,842✔
698
                ++indegree.at(dst_scc);
1,388,108✔
699
            }
700
        }
1,388,842✔
701
    }
1,293,538✔
702

703
    std::map<Label, int> indegree_for_sources = indegree;
4,158✔
704
    std::vector<Label> topo_order;
4,158✔
705
    topo_order.reserve(maximal_scc_ids.size());
4,158✔
706
    for (const auto& scc_id : maximal_scc_ids) {
1,297,064✔
707
        if (indegree.at(scc_id) == 0) {
1,292,906✔
708
            topo_order.push_back(scc_id);
4,158✔
709
        }
710
    }
711
    for (size_t index = 0; index < topo_order.size(); ++index) {
1,297,064✔
712
        const Label scc_id = topo_order[index];
1,292,906✔
713
        for (const auto& succ : dag_successors.at(scc_id)) {
2,681,014✔
714
            --indegree.at(succ);
1,388,108✔
715
            if (indegree.at(succ) == 0) {
1,388,108✔
716
                topo_order.push_back(succ);
1,288,748✔
717
            }
718
        }
719
    }
1,292,906✔
720
    if (topo_order.size() != maximal_scc_ids.size()) {
4,158✔
721
        CRAB_ERROR("WTO-derived SCC graph must be acyclic");
×
722
    }
723

724
    // Longest path over maximal SCC DAG by tail-call site count.
725
    constexpr int uninitialized_depth = std::numeric_limits<int>::min();
4,158✔
726
    std::map<Label, int> max_tail_depth;
4,158✔
727
    std::map<Label, std::optional<Label>> depth_label;
4,158✔
728
    for (const auto& scc_id : maximal_scc_ids) {
1,297,064✔
729
        max_tail_depth.emplace(scc_id, uninitialized_depth);
1,292,906✔
730
        depth_label.emplace(scc_id, std::nullopt);
1,292,906✔
731
        if (indegree_for_sources.at(scc_id) == 0) {
1,292,906✔
732
            max_tail_depth.at(scc_id) = tail_sites_per_scc.at(scc_id);
4,158✔
733
            depth_label.at(scc_id) = representative_tail_label.at(scc_id);
650,611✔
734
        }
735
    }
736

737
    for (const auto& scc_id : topo_order) {
1,297,058✔
738
        const int current_depth = max_tail_depth.at(scc_id);
1,292,902✔
739
        if (current_depth == uninitialized_depth) {
1,292,902✔
740
            continue;
×
741
        }
742
        if (current_depth > tail_call_chain_limit) {
1,292,902✔
743
            const Label at = depth_label.at(scc_id).value_or(scc_id);
2✔
744
            throw InvalidControlFlow{"tail call chain depth exceeds " + std::to_string(tail_call_chain_limit) +
4✔
745
                                     " (at " + to_string(at) + ")"};
7✔
746
        }
2✔
747
        for (const auto& succ : dag_successors.at(scc_id)) {
2,681,004✔
748
            const int candidate_depth = current_depth + tail_sites_per_scc.at(succ);
1,388,104✔
749
            if (candidate_depth > max_tail_depth.at(succ)) {
1,388,104✔
750
                max_tail_depth.at(succ) = candidate_depth;
1,289,698✔
751
                depth_label.at(succ) = representative_tail_label.at(succ).has_value()
2,579,396✔
752
                                           ? representative_tail_label.at(succ)
1,650✔
753
                                           : depth_label.at(scc_id);
2,628,599✔
754
            }
755
        }
756
    }
757
}
4,176✔
758

759
// Pass: ComputeCallbackMetadata
760
// Reads    : Program (CFG + instructions).
761
// Writes   : builder.prog's callback metadata, via CfgBuilder::set_callback_metadata: the set
762
//            of top-level concrete-instruction labels eligible as PTR_TO_FUNC targets, and the
763
//            subset whose body can reach a top-level Exit.
764
// Notes    : Excludes Label::entry/Label::exit, synthetic jump labels, labels under an inlined
765
//            stack-frame prefix, and Exit instructions themselves.
766
static void pass_compute_callback_metadata(CfgBuilder& builder) {
4,156✔
767
    const Program& prog = builder.prog;
4,156✔
768
    CallbackMetadata md;
4,156✔
769
    for (const Label& label : prog.labels()) {
1,330,258✔
770
        if (label == Label::entry || label == Label::exit || label.isjump() || !label.stack_frame_prefix.empty()) {
1,326,102✔
771
            continue;
236,912✔
772
        }
773
        if (std::holds_alternative<Exit>(prog.instruction_at(label))) {
1,089,190✔
774
            continue;
3,678✔
775
        }
776
        md.target_labels.insert(label.from);
1,085,512✔
777
    }
778

779
    const auto has_reachable_top_level_exit = [&](const Label& start) {
1,087,590✔
780
        std::set<Label> seen;
1,085,512✔
781
        std::vector<Label> worklist{start};
2,713,780✔
782
        while (!worklist.empty()) {
303,231,644✔
783
            Label label = worklist.back();
303,231,630✔
784
            worklist.pop_back();
303,231,630✔
785
            if (seen.contains(label)) {
303,231,630✔
786
                continue;
264✔
787
            }
788
            seen.insert(label);
303,231,366✔
789
            if (label == Label::exit) {
303,231,366✔
790
                return true;
794✔
791
            }
792
            if (label != Label::entry && prog.cfg().contains(label) &&
758,074,445✔
793
                std::holds_alternative<Exit>(prog.instruction_at(label)) && label.stack_frame_prefix.empty()) {
758,074,445✔
794
                return true;
541,955✔
795
            }
796
            for (const Label& child : prog.cfg().children_of(label)) {
624,199,388✔
797
                worklist.push_back(child);
322,053,520✔
798
            }
799
        }
303,231,630✔
800
        return false;
7✔
801
    };
2,171,024✔
802
    for (const int32_t label_num : md.target_labels) {
1,089,668✔
803
        const Label label{gsl::narrow<int>(label_num)};
1,085,512✔
804
        if (has_reachable_top_level_exit(label)) {
1,085,512✔
805
            md.targets_with_exit.insert(label_num);
1,085,498✔
806
        }
807
    }
1,085,512✔
808
    builder.set_callback_metadata(std::move(md));
4,156✔
809
}
4,156✔
810

811
// Pass: InsertTerminationCounters
812
// Reads    : WTO of the current CFG.
813
// Writes   : For each WTO loop head, inserts an IncrementLoopCounter at a synthetic
814
//            increment-counter label placed between the head and its successors (CFG edges and
815
//            instructions are both mutated via CfgBuilder::insert_after).
816
// Notes    : WTO identifies every strongly connected component and its entry point(s), which
817
//            are the natural locations for counters that help verify program termination.
818
static void pass_insert_termination_counters(CfgBuilder& builder, const Wto& wto) {
46✔
819
    wto.for_each_loop_head([&](const Label& label) -> void {
91✔
820
        builder.insert_after(label, Label::make_increment_counter(label), IncrementLoopCounter{label});
88✔
821
    });
44✔
822
}
46✔
823

824
// Pass: ExtractAssertions
825
// Reads    : Program instructions, ProgramInfo, options.
826
// Writes   : Populates builder.prog.m_assertions with the per-label precondition vector
827
//            (memory bounds, type guards, etc.) produced by get_assertions.
828
// Notes    : Runs for every label in the CFG, including synthetic ones (Assume / counters).
829
static void pass_extract_assertions(CfgBuilder& builder, const ProgramInfo& info, const VerifierOptions& options) {
4,156✔
830
    for (const auto& label : builder.prog.labels()) {
1,330,302✔
831
        builder.set_assertions(label, get_assertions(builder.prog.instruction_at(label), info, options.runtime, label));
1,989,219✔
832
    }
833
}
4,156✔
834

835
// from_sequence orchestrates the preparation pipeline. Each pass has a documented
836
// pre/postcondition; this function's job is just to sequence them and hand the
837
// result off as a finalised Program.
838
Program Program::from_sequence(const InstructionSeq& inst_seq, const ProgramInfo& info,
4,248✔
839
                               const VerifierOptions& options) {
840
    // --- Pass: ValidateOptions --------------------------------------------
841
    options.runtime.validate();
4,248✔
842
    // Preserves the platform-non-null invariant for every subsequent pass in this pipeline.
843
    assert(info.platform != nullptr && "info.platform must be set before Program::from_sequence");
4,248✔
844

845
    // --- Pass: ValidateInstructionSupport ---------------------------------
846
    pass_validate_instruction_support(inst_seq, info);
4,248✔
847

848
    // --- Pass: ResolveKfuncCalls ------------------------------------------
849
    const ResolvedKfuncCalls resolved_kfunc_calls = pass_resolve_kfunc_calls(inst_seq, info);
4,202✔
850

851
    // --- Pass: LowerPseudoLoads -------------------------------------------
852
    const LoweredPseudoLoads lowered_pseudo_loads = pass_lower_pseudo_loads(inst_seq, info);
4,176✔
853

854
    // --- Pass: BuildInitialCfg (nodes, then edges with InsertAssumeEdges) -
855
    CfgBuilder builder;
4,172✔
856
    // Populate m_info up front so any pass that reads builder.prog.info() sees the real
857
    // ProgramInfo rather than a default-constructed (null-platform) one.
858
    builder.prog.m_info = info;
4,172✔
859
    pass_populate_nodes(builder, inst_seq, info, resolved_kfunc_calls, lowered_pseudo_loads);
4,172✔
860
    pass_connect_edges(builder, inst_seq, options.must_have_exit);
4,172✔
861

862
    // --- Pass: InlineLocalCalls -------------------------------------------
863
    pass_inline_local_calls(builder, inst_seq, options.runtime.max_call_stack_frames);
4,166✔
864

865
    // --- Pass: ValidateTailCallDepth --------------------------------------
866
    const Wto wto{builder.prog.cfg()};
4,158✔
867
    pass_validate_tail_call_depth(builder.prog, wto, *info.platform, info.type);
4,158✔
868

869
    // --- Pass: ComputeCallbackMetadata ------------------------------------
870
    pass_compute_callback_metadata(builder);
4,156✔
871

872
    // --- Pass: InsertTerminationCounters ----------------------------------
873
    if (options.runtime.check_for_termination) {
4,156✔
874
        pass_insert_termination_counters(builder, wto);
46✔
875
    }
876

877
    // --- Pass: ExtractAssertions ------------------------------------------
878
    pass_extract_assertions(builder, info, options);
4,156✔
879

880
    return std::move(builder.prog);
6,234✔
881
}
4,200✔
882

883
std::set<BasicBlock> BasicBlock::collect_basic_blocks(const Cfg& cfg, const bool simplify) {
2✔
884
    if (!simplify) {
2✔
885
        std::set<BasicBlock> res;
2✔
886
        for (const Label& label : cfg.labels()) {
32✔
887
            if (label != cfg.entry_label() && label != cfg.exit_label()) {
73✔
888
                res.insert(BasicBlock{label});
39✔
889
            }
890
        }
891
        return res;
2✔
892
    }
2✔
893

894
    std::set<BasicBlock> res;
×
895
    std::set<Label> worklist;
×
896
    for (const Label& label : cfg.labels()) {
×
897
        worklist.insert(label);
×
898
    }
899
    std::set<Label> seen;
×
900
    while (!worklist.empty()) {
×
901
        Label label = *worklist.begin();
×
902
        worklist.erase(label);
×
903
        if (seen.contains(label)) {
×
904
            continue;
×
905
        }
906
        seen.insert(label);
×
907

908
        if (cfg.in_degree(label) == 1 && cfg.num_siblings(label) == 1) {
×
909
            continue;
×
910
        }
911
        BasicBlock bb{label};
×
912
        while (cfg.out_degree(label) == 1) {
×
913
            const Label& next_label = cfg.get_child(bb.last_label());
×
914

915
            if (seen.contains(next_label) || next_label == cfg.exit_label() || cfg.in_degree(next_label) != 1) {
×
916
                break;
917
            }
918

919
            if (bb.first_label() == cfg.entry_label()) {
×
920
                // Entry instruction is Undefined. We want to start with 0
921
                bb.m_ts.clear();
×
922
            }
923
            bb.m_ts.push_back(next_label);
×
924

925
            worklist.erase(next_label);
×
926
            seen.insert(next_label);
×
927

928
            label = next_label;
×
929
        }
×
930
        res.emplace(std::move(bb));
×
931
    }
×
932
    return res;
×
933
}
×
934

935
/// Get the type of given Instruction.
936
/// Most of these type names are also statistics header labels.
937
Cfg cfg_from_adjacency_list(const std::map<Label, std::vector<Label>>& AdjList) {
8✔
938
    CfgBuilder builder;
8✔
939
    for (const auto& label : std::views::keys(AdjList)) {
66✔
940
        if (label == Label::entry || label == Label::exit) {
58✔
941
            continue;
8✔
942
        }
943
        builder.insert(label, Undefined{});
75✔
944
    }
945
    for (const auto& [label, children] : AdjList) {
66✔
946
        for (const auto& child : children) {
136✔
947
            builder.add_child(label, child);
78✔
948
        }
949
    }
950
    return builder.prog.cfg();
16✔
951
}
8✔
952
} // namespace prevail
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc