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

Alan-Jowett / ebpf-verifier / 22235569093

20 Feb 2026 02:12AM UTC coverage: 88.002% (-0.2%) from 88.157%
22235569093

push

github

web-flow
Handle Call builtins: fix handling of Falco tests  (#1025)

* falco: fix raw_tracepoint privilege and group expected failures

Mark raw_tracepoint/raw_tracepoint_writable as privileged program types so Falco raw-tracepoint sections are not treated as unprivileged argument checks.

Update Falco sample matrix to move now-passing sections out of TEST_SECTION_FAIL and group the remaining expected failures by root-cause class (offset lower-bound loss vs size lower-bound loss at correlated joins).

Signed-off-by: Elazar Gershuni <elazarg@gmail.com>

* elf/unmarshal: gate builtin relocations via platform call model

Add platform hooks to resolve builtin symbols and provide builtin call contracts, thread relocation-gated builtin call offsets through ProgramInfo, and only treat static helper IDs as builtins at gated call sites.

Also extend platform-table, marshal, and YAML-platform tests to cover builtin resolver wiring and call unmarshal behavior.
* crab: canonicalize unsigned intervals in bitwise_and
When uvalue intervals temporarily carry signed lower bounds (e.g. after joins), Interval::bitwise_and asserted in debug builds. Canonicalize both operands via zero_extend(64) before unsigned bitwise reasoning, preserving soundness and avoiding debug aborts.

Validated by reproducing SIGABRT on reverted code in [falco][verify] and confirming the patched build completes with expected 73 pass / 20 failed-as-expected.

* Fix unsound bitwise_and case for non-singleton all-ones rhs

Signed-off-by: Elazar Gershuni <elazarg@gmail.com>

239 of 252 new or added lines in 9 files covered. (94.84%)

602 existing lines in 16 files now uncovered.

11743 of 13344 relevant lines covered (88.0%)

3262592.78 hits per line

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

57.85
/src/printing.cpp
1
// Copyright (c) Prevail Verifier contributors.
2
// SPDX-License-Identifier: MIT
3
#include <algorithm>
4
#include <fstream>
5
#include <iomanip>
6
#include <iostream>
7
#include <variant>
8
#include <vector>
9

10
#include "arith/num_big.hpp"
11
#include "arith/variable.hpp"
12
#include "cfg/cfg.hpp"
13
#include "crab/interval.hpp"
14
#include "crab/type_encoding.hpp"
15
#include "crab/var_registry.hpp"
16
#include "ir/syntax.hpp"
17
#include "platform.hpp"
18
#include "spec/function_prototypes.hpp"
19
#include "verifier.hpp"
20

21
using std::optional;
22
using std::string;
23
using std::vector;
24

25
namespace prevail {
26

27
std::ostream& operator<<(std::ostream& o, const Interval& interval) {
1,354✔
28
    if (interval.is_bottom()) {
1,354✔
29
        o << "_|_";
×
30
    } else {
31
        o << "[" << interval._lb << ", " << interval._ub << "]";
1,354✔
32
    }
33
    return o;
1,354✔
34
}
35
static std::string int128_to_string(Int128 n) {
503,266✔
36
    if (n == 0) {
503,266✔
37
        return "0";
1,479✔
38
    }
39
    bool negative = false;
502,280✔
40
    if (n < 0) {
502,280✔
41
        negative = true;
544✔
42
        // Handle kInt128Min: negate via unsigned to avoid overflow.
43
        if (n == kInt128Min) {
544✔
44
            // kInt128Min == -170141183460469231731687303715884105728
45
            // Build the string for the absolute value via unsigned arithmetic.
46
            auto u = static_cast<UInt128>(n);
×
47
            // Two's complement: UInt128(kInt128Min) is the correct magnitude.
48
            std::string result;
×
49
            while (u != 0) {
×
50
                result += static_cast<char>('0' + static_cast<int>(u % 10));
×
51
                u /= 10;
×
52
            }
53
            result += '-';
×
54
            std::ranges::reverse(result);
×
55
            return result;
×
56
        }
×
57
        n = -n;
544✔
58
    }
59
    std::string result;
502,280✔
60
    while (n > 0) {
2,510,882✔
61
        result += static_cast<char>('0' + static_cast<int>(n % 10));
2,008,602✔
62
        n /= 10;
2,008,602✔
63
    }
64
    if (negative) {
502,280✔
65
        result += '-';
544✔
66
    }
67
    std::ranges::reverse(result);
502,280✔
68
    return result;
502,280✔
69
}
502,280✔
70

71
std::ostream& operator<<(std::ostream& o, const Number& z) { return o << z.to_string(); }
503,266✔
72

73
std::string Number::to_string() const { return int128_to_string(_n); }
503,266✔
74

75
std::string Interval::to_string() const {
×
76
    std::ostringstream s;
×
77
    s << *this;
×
78
    return s.str();
×
79
}
×
80

81
std::ostream& operator<<(std::ostream& os, const Label& label) {
1,666✔
82
    if (label == Label::entry) {
1,666✔
83
        return os << "entry";
6✔
84
    }
85
    if (label == Label::exit) {
1,660✔
86
        return os << "exit";
10✔
87
    }
88
    if (!label.stack_frame_prefix.empty()) {
1,650✔
89
        os << label.stack_frame_prefix << STACK_FRAME_DELIMITER;
328✔
90
    }
91
    os << label.from;
1,650✔
92
    if (label.to != -1) {
1,650✔
93
        os << ":" << label.to;
50✔
94
    }
95
    if (!label.special_label.empty()) {
1,650✔
96
        os << " (" << label.special_label << ")";
22✔
97
    }
98
    return os;
825✔
99
}
100

101
string to_string(Label const& label) {
1,288✔
102
    std::stringstream str;
1,288✔
103
    str << label;
1,288✔
104
    return str.str();
2,576✔
105
}
1,288✔
106

107
struct LineInfoPrinter {
1✔
108
    std::ostream& os;
109
    std::string previous_source_line;
110

111
    void print_line_info(const Label& label) {
6✔
112
        if (thread_local_options.verbosity_opts.print_line_info) {
6✔
113
            const auto& line_info_map = thread_local_program_info.get().line_info;
×
114
            const auto& line_info = line_info_map.find(label.from);
×
115
            // Print line info only once.
116
            if (line_info != line_info_map.end() && line_info->second.source_line != previous_source_line) {
×
117
                os << "\n" << line_info->second << "\n";
×
118
                previous_source_line = line_info->second.source_line;
×
119
            }
120
        }
121
    }
6✔
122
};
123

124
struct DetailedPrinter : LineInfoPrinter {
125
    const Program& prog;
126

127
    DetailedPrinter(std::ostream& os, const Program& prog) : LineInfoPrinter{os}, prog(prog) {}
2✔
128

129
    void print_labels(const std::string& direction, const std::set<Label>& labels) {
6✔
130
        auto [it, et] = std::pair{labels.begin(), labels.end()};
6✔
131
        if (it != et) {
6✔
132
            os << "  " << direction << " ";
6✔
133
            while (it != et) {
12✔
134
                os << *it;
6✔
135
                ++it;
6✔
136
                if (it == et) {
6✔
137
                    os << ";";
6✔
138
                } else {
139
                    os << ",";
×
140
                }
141
            }
142
        }
143
        os << "\n";
6✔
144
    }
6✔
145

146
    void print_jump(const std::string& direction, const Label& label) {
6✔
147
        print_labels(direction, direction == "from" ? prog.cfg().parents_of(label) : prog.cfg().children_of(label));
6✔
148
    }
6✔
149

150
    void print_instruction(const Program& prog, const Label& label) {
×
151
        for (const auto& pre : prog.assertions_at(label)) {
×
152
            os << "  " << "assert " << pre << ";\n";
×
153
        }
×
154
        os << "  " << prog.instruction_at(label) << ";\n";
×
155
    }
×
156
};
157

158
void print_program(const Program& prog, std::ostream& os, const bool simplify) {
×
159
    DetailedPrinter printer{os, prog};
×
160
    for (const BasicBlock& bb : BasicBlock::collect_basic_blocks(prog.cfg(), simplify)) {
×
161
        printer.print_jump("from", bb.first_label());
×
162
        os << bb.first_label() << ":\n";
×
163
        for (const Label& label : bb) {
×
164
            printer.print_line_info(label);
×
165
            printer.print_instruction(prog, label);
×
166
        }
167
        printer.print_jump("goto", bb.last_label());
×
168
    }
×
169
    os << "\n";
×
170
}
×
171

172
void print_invariants(std::ostream& os, const Program& prog, const bool simplify, const AnalysisResult& result) {
×
173
    DetailedPrinter printer{os, prog};
×
174
    for (const BasicBlock& bb : BasicBlock::collect_basic_blocks(prog.cfg(), simplify)) {
×
175
        if (result.invariants.at(bb.first_label()).pre.is_bottom()) {
×
176
            continue;
×
177
        }
178
        os << "\nPre-invariant : " << result.invariants.at(bb.first_label()).pre << "\n";
×
179
        printer.print_jump("from", bb.first_label());
×
180
        os << bb.first_label() << ":\n";
×
181
        Label last_label = bb.first_label();
×
182
        for (const Label& label : bb) {
×
183
            printer.print_line_info(label);
×
184
            printer.print_instruction(prog, label);
×
185
            last_label = label;
×
186

187
            const auto& current = result.invariants.at(last_label);
×
188
            if (current.error) {
×
189
                os << "\nVerification error:\n";
×
190
                if (label != bb.last_label()) {
×
191
                    os << "After " << current.pre << "\n";
×
192
                }
193
                print_error(os, *current.error);
×
194
                os << "\n";
×
195
                return;
×
196
            }
197
        }
198
        const auto& current = result.invariants.at(last_label);
×
199
        if (!current.post.is_bottom()) {
×
200
            printer.print_jump("goto", last_label);
×
201
            os << "\nPost-invariant : " << current.post << "\n";
×
202
        }
203
    }
×
204
    os << "\n";
×
205
}
×
206

207
void print_dot(const Program& prog, std::ostream& out) {
×
208
    out << "digraph program {\n";
×
209
    out << "    node [shape = rectangle];\n";
×
210
    for (const auto& label : prog.labels()) {
×
211
        out << "    \"" << label << "\"[xlabel=\"" << label << "\",label=\"";
×
212

213
        for (const auto& pre : prog.assertions_at(label)) {
×
214
            out << "assert " << pre << "\\l";
×
215
        }
×
216
        out << prog.instruction_at(label) << "\\l";
×
217

218
        out << "\"];\n";
×
219
        for (const Label& next : prog.cfg().children_of(label)) {
×
220
            out << "    \"" << label << "\" -> \"" << next << "\";\n";
×
221
        }
222
        out << "\n";
×
223
    }
224
    out << "}\n";
×
225
}
×
226

227
void print_dot(const Program& prog, const std::string& outfile) {
×
228
    std::ofstream out{outfile};
×
229
    if (out.fail()) {
×
230
        throw std::runtime_error(std::string("Could not open file ") + outfile);
×
231
    }
232
    print_dot(prog, out);
×
233
}
×
234

235
void print_unreachable(std::ostream& os, const Program& prog, const AnalysisResult& result) {
×
236
    for (const auto& [label, notes] : result.find_unreachable(prog)) {
×
237
        for (const auto& msg : notes) {
×
238
            os << label << ": " << msg << "\n";
×
239
        }
240
    }
×
241
    os << "\n";
×
242
}
×
243

244
std::string to_string(const VerificationError& error) {
314✔
245
    std::stringstream ss;
314✔
246
    if (const auto& label = error.where) {
314✔
247
        ss << *label << ": ";
314✔
248
    }
249
    ss << error.what();
314✔
250
    return ss.str();
628✔
251
}
314✔
252

253
void print_error(std::ostream& os, const VerificationError& error) {
2✔
254
    LineInfoPrinter printer{os};
2✔
255
    if (const auto& label = error.where) {
2✔
256
        printer.print_line_info(*label);
2✔
257
        os << *label << ": ";
2✔
258
    }
259
    os << error.what() << "\n";
2✔
260
    os << "\n";
2✔
261
}
2✔
262

263
std::ostream& operator<<(std::ostream& os, const ArgSingle::Kind kind) {
74✔
264
    switch (kind) {
74✔
265
    case ArgSingle::Kind::ANYTHING: return os << "uint64_t";
14✔
266
    case ArgSingle::Kind::PTR_TO_CTX: return os << "ctx";
4✔
267
    case ArgSingle::Kind::PTR_TO_STACK: return os << "stack";
×
UNCOV
268
    case ArgSingle::Kind::PTR_TO_FUNC: return os << "func";
×
269
    case ArgSingle::Kind::MAP_FD: return os << "map_fd";
24✔
UNCOV
270
    case ArgSingle::Kind::MAP_FD_PROGRAMS: return os << "map_fd_programs";
×
271
    case ArgSingle::Kind::PTR_TO_MAP_KEY: return os << "map_key";
24✔
272
    case ArgSingle::Kind::PTR_TO_MAP_VALUE: return os << "map_value";
8✔
UNCOV
273
    case ArgSingle::Kind::PTR_TO_SOCKET: return os << "socket";
×
UNCOV
274
    case ArgSingle::Kind::PTR_TO_BTF_ID: return os << "btf_id";
×
UNCOV
275
    case ArgSingle::Kind::PTR_TO_ALLOC_MEM: return os << "alloc_mem";
×
UNCOV
276
    case ArgSingle::Kind::PTR_TO_SPIN_LOCK: return os << "spin_lock";
×
277
    case ArgSingle::Kind::PTR_TO_TIMER: return os << "timer";
×
278
    case ArgSingle::Kind::CONST_SIZE_OR_ZERO: return os << "size?";
×
279
    case ArgSingle::Kind::PTR_TO_WRITABLE_LONG: return os << "out_long";
×
280
    case ArgSingle::Kind::PTR_TO_WRITABLE_INT: return os << "out_int";
×
281
    }
282
    assert(false);
283
    return os;
284
}
285

UNCOV
286
std::ostream& operator<<(std::ostream& os, const ArgPair::Kind kind) {
×
UNCOV
287
    switch (kind) {
×
UNCOV
288
    case ArgPair::Kind::PTR_TO_READABLE_MEM: return os << "mem";
×
289
    case ArgPair::Kind::PTR_TO_WRITABLE_MEM: return os << "out";
×
290
    }
291
    assert(false);
292
    return os;
293
}
294

295
std::ostream& operator<<(std::ostream& os, const ArgSingle arg) {
74✔
296
    os << arg.kind;
74✔
297
    if (arg.or_null) {
74✔
298
        os << "?";
×
299
    }
300
    os << " " << arg.reg;
74✔
301
    return os;
74✔
302
}
303

304
std::ostream& operator<<(std::ostream& os, const ArgPair arg) {
×
305
    os << arg.kind;
×
UNCOV
306
    if (arg.or_null) {
×
UNCOV
307
        os << "?";
×
308
    }
UNCOV
309
    os << " " << arg.mem << "[" << arg.size;
×
UNCOV
310
    if (arg.can_be_zero) {
×
UNCOV
311
        os << "?";
×
312
    }
313
    os << "], uint64_t " << arg.size;
×
314
    return os;
×
315
}
316

317
std::ostream& operator<<(std::ostream& os, const Bin::Op op) {
280✔
318
    using Op = Bin::Op;
140✔
319
    switch (op) {
280✔
320
    case Op::MOV: return os;
81✔
321
    case Op::MOVSX8: return os << "s8";
×
322
    case Op::MOVSX16: return os << "s16";
×
UNCOV
323
    case Op::MOVSX32: return os << "s32";
×
324
    case Op::ADD: return os << "+";
72✔
325
    case Op::SUB: return os << "-";
×
UNCOV
326
    case Op::MUL: return os << "*";
×
UNCOV
327
    case Op::UDIV: return os << "/";
×
UNCOV
328
    case Op::SDIV: return os << "s/";
×
UNCOV
329
    case Op::UMOD: return os << "%";
×
UNCOV
330
    case Op::SMOD: return os << "s%";
×
UNCOV
331
    case Op::OR: return os << "|";
×
332
    case Op::AND: return os << "&";
30✔
333
    case Op::LSH: return os << "<<";
12✔
UNCOV
334
    case Op::RSH: return os << ">>";
×
335
    case Op::ARSH: return os << ">>>";
2✔
336
    case Op::XOR: return os << "^";
2✔
337
    }
338
    assert(false);
339
    return os;
340
}
341

342
std::ostream& operator<<(std::ostream& os, const Condition::Op op) {
222✔
343
    using Op = Condition::Op;
111✔
344
    switch (op) {
222✔
345
    case Op::EQ: return os << "==";
38✔
346
    case Op::NE: return os << "!=";
10✔
UNCOV
347
    case Op::SET: return os << "&==";
×
UNCOV
348
    case Op::NSET: return os << "&!="; // not in ebpf
×
349
    case Op::LT: return os << "<";     // TODO: os << "u<";
124✔
350
    case Op::LE: return os << "<=";    // TODO: os << "u<=";
12✔
351
    case Op::GT: return os << ">";     // TODO: os << "u>";
22✔
352
    case Op::GE: return os << ">=";    // TODO: os << "u>=";
6✔
353
    case Op::SLT: return os << "s<";
4✔
UNCOV
354
    case Op::SLE: return os << "s<=";
×
355
    case Op::SGT: return os << "s>";
4✔
356
    case Op::SGE: return os << "s>=";
2✔
357
    }
358
    assert(false);
359
    return os;
360
}
361

362
static string size(const int w, const bool is_signed = false) {
184✔
363
    return string(is_signed ? "s" : "u") + std::to_string(w * 8);
552✔
364
}
365

366
// ReSharper disable CppMemberFunctionMayBeConst
367
struct AssertionPrinterVisitor {
368
    std::ostream& _os;
369

370
    void operator()(ValidStore const& a) {
2✔
371
        _os << a.mem << ".type != stack -> " << TypeConstraint{a.val, TypeGroup::number};
2✔
372
    }
2✔
373

374
    void operator()(ValidAccess const& a) {
336✔
375
        if (a.or_null) {
336✔
376
            _os << "(" << TypeConstraint{a.reg, TypeGroup::number} << " and " << a.reg << ".value == 0) or ";
8✔
377
        }
378
        _os << "valid_access(" << a.reg << ".offset";
336✔
379
        if (a.offset > 0) {
336✔
380
            _os << "+" << a.offset;
52✔
381
        } else if (a.offset < 0) {
284✔
382
            _os << a.offset;
6✔
383
        }
384

385
        if (a.width == Value{Imm{0}}) {
336✔
386
            // a.width == 0, meaning we only care it's an in-bound pointer,
387
            // so it can be compared with another pointer to the same region.
UNCOV
388
            _os << ") for comparison/subtraction";
×
389
        } else {
390
            _os << ", width=" << a.width << ") for ";
336✔
391
            if (a.access_type == AccessType::read) {
336✔
392
                _os << "read";
280✔
393
            } else {
394
                _os << "write";
56✔
395
            }
396
        }
397
    }
336✔
398

399
    void operator()(const BoundedLoopCount& a) {
22✔
400
        _os << variable_registry->loop_counter(to_string(a.name)) << " < " << BoundedLoopCount::limit;
22✔
401
    }
22✔
402

403
    void operator()(ValidSize const& a) {
98✔
404
        const auto op = a.can_be_zero ? " >= " : " > ";
98✔
405
        _os << a.reg << ".value" << op << 0;
98✔
406
    }
98✔
407

408
    void operator()(ValidMapKeyValue const& a) {
42✔
409
        _os << "within(" << a.access_reg << ":" << (a.key ? "key_size" : "value_size") << "(" << a.map_fd_reg << "))";
51✔
410
    }
42✔
411

412
    void operator()(ValidCallbackTarget const& a) { _os << "valid_callback_target(" << a.reg << ")"; }
4✔
413

414
    void operator()(ZeroCtxOffset const& a) {
2✔
415
        _os << variable_registry->reg(DataKind::ctx_offsets, a.reg.v) << " == 0";
2✔
416
    }
2✔
417

418
    void operator()(Comparable const& a) {
10✔
419
        if (a.or_r2_is_number) {
10✔
420
            _os << TypeConstraint{a.r2, TypeGroup::number} << " or ";
15✔
421
        }
422
        _os << variable_registry->type_reg(a.r1.v) << " == " << variable_registry->type_reg(a.r2.v) << " in "
10✔
423
            << TypeGroup::singleton_ptr;
10✔
424
    }
10✔
425

426
    void operator()(Addable const& a) {
2✔
427
        _os << TypeConstraint{a.ptr, TypeGroup::pointer} << " -> " << TypeConstraint{a.num, TypeGroup::number};
4✔
428
    }
2✔
429

430
    void operator()(ValidDivisor const& a) { _os << a.reg << " != 0"; }
76✔
431

432
    void operator()(TypeConstraint const& tc) {
232✔
433
        const string cmp_op = is_singleton_type(tc.types) ? "==" : "in";
254✔
434
        _os << variable_registry->type_reg(tc.reg.v) << " " << cmp_op << " " << tc.types;
232✔
435
    }
232✔
436

437
    void operator()(FuncConstraint const& fc) { _os << variable_registry->type_reg(fc.reg.v) << " is helper"; }
6✔
438
};
439

440
// ReSharper disable CppMemberFunctionMayBeConst
441
struct CommandPrinterVisitor {
442
    std::ostream& os_;
443

444
    void visit(const auto& item) { std::visit(*this, item); }
445

446
    void operator()(Undefined const& a) { os_ << "Undefined{" << a.opcode << "}"; }
×
447

448
    void operator()(LoadMapFd const& b) { os_ << b.dst << " = map_fd " << b.mapfd; }
26✔
449

450
    void operator()(LoadMapAddress const& b) { os_ << b.dst << " = map_val(" << b.mapfd << ") + " << b.offset; }
×
451

452
    void operator()(LoadPseudo const& b) {
×
453
        os_ << b.dst << " = ";
×
454
        switch (b.addr.kind) {
×
UNCOV
455
        case PseudoAddress::Kind::VARIABLE_ADDR: os_ << "variable_addr(" << b.addr.imm << ")"; break;
×
456
        case PseudoAddress::Kind::CODE_ADDR: os_ << "code_addr(" << b.addr.imm << ")"; break;
×
UNCOV
457
        case PseudoAddress::Kind::MAP_BY_IDX: os_ << "map_by_idx(" << b.addr.imm << ")"; break;
×
UNCOV
458
        case PseudoAddress::Kind::MAP_VALUE_BY_IDX:
×
UNCOV
459
            os_ << "mva(map_by_idx(" << b.addr.imm << ")) + " << b.addr.next_imm;
×
UNCOV
460
            break;
×
461
        }
UNCOV
462
    }
×
463

464
    // llvm-objdump uses "w<number>" for 32-bit operations and "r<number>" for 64-bit operations.
465
    // We use the same convention here for consistency.
466
    static std::string reg_name(Reg const& a, const bool is64) { return ((is64) ? "r" : "w") + std::to_string(a.v); }
420✔
467

468
    void operator()(Bin const& b) {
280✔
469
        os_ << reg_name(b.dst, b.is64) << " " << b.op << "= " << b.v;
420✔
470
        if (b.lddw) {
280✔
471
            os_ << " ll";
2✔
472
        }
473
    }
280✔
474

475
    void operator()(Un const& b) {
12✔
476
        os_ << b.dst << " = ";
12✔
477
        switch (b.op) {
12✔
478
        case Un::Op::BE16: os_ << "be16 "; break;
2✔
479
        case Un::Op::BE32: os_ << "be32 "; break;
2✔
480
        case Un::Op::BE64: os_ << "be64 "; break;
2✔
481
        case Un::Op::LE16: os_ << "le16 "; break;
2✔
482
        case Un::Op::LE32: os_ << "le32 "; break;
2✔
483
        case Un::Op::LE64: os_ << "le64 "; break;
2✔
UNCOV
484
        case Un::Op::SWAP16: os_ << "swap16 "; break;
×
UNCOV
485
        case Un::Op::SWAP32: os_ << "swap32 "; break;
×
UNCOV
486
        case Un::Op::SWAP64: os_ << "swap64 "; break;
×
UNCOV
487
        case Un::Op::NEG: os_ << "-"; break;
×
488
        }
489
        os_ << b.dst;
12✔
490
    }
12✔
491

492
    void operator()(Call const& call) {
70✔
493
        os_ << "r0 = " << call.name << ":" << call.func << "(";
70✔
494
        for (uint8_t r = 1; r <= 5; r++) {
144✔
495
            // Look for a singleton.
496
            auto single = std::ranges::find_if(call.singles, [r](const ArgSingle arg) { return arg.reg.v == r; });
372✔
497
            if (single != call.singles.end()) {
144✔
498
                if (r > 1) {
74✔
499
                    os_ << ", ";
48✔
500
                }
501
                os_ << *single;
74✔
502
                continue;
74✔
503
            }
504

505
            // Look for the start of a pair.
506
            auto pair = std::ranges::find_if(call.pairs, [r](const ArgPair arg) { return arg.mem.v == r; });
70✔
507
            if (pair != call.pairs.end()) {
70✔
UNCOV
508
                if (r > 1) {
×
UNCOV
509
                    os_ << ", ";
×
510
                }
UNCOV
511
                os_ << *pair;
×
UNCOV
512
                r++;
×
UNCOV
513
                continue;
×
514
            }
515

516
            // Not found.
517
            break;
70✔
518
        }
519
        os_ << ")";
70✔
520
    }
70✔
521

UNCOV
522
    void operator()(CallLocal const& call) { os_ << "call <" << to_string(call.target) << ">"; }
×
523

524
    void operator()(Callx const& callx) { os_ << "callx " << callx.func; }
×
525

UNCOV
526
    void operator()(CallBtf const& call) { os_ << "call_btf " << call.btf_id; }
×
527

528
    void operator()(Exit const& b) { os_ << "exit"; }
50✔
529

530
    void operator()(Jmp const& b) {
×
531
        // A "standalone" jump Instruction.
532
        // Print the label without offset calculations.
533
        if (b.cond) {
×
UNCOV
534
            os_ << "if ";
×
UNCOV
535
            print(*b.cond);
×
UNCOV
536
            os_ << " ";
×
537
        }
UNCOV
538
        os_ << "goto label <" << to_string(b.target) << ">";
×
UNCOV
539
    }
×
540

541
    void operator()(Jmp const& b, const int offset) {
54✔
542
        const string sign = offset > 0 ? "+" : "";
54✔
543
        const string target = sign + std::to_string(offset) + " <" + to_string(b.target) + ">";
108✔
544

545
        if (b.cond) {
54✔
546
            os_ << "if ";
40✔
547
            print(*b.cond);
40✔
548
            os_ << " ";
40✔
549
        }
550
        os_ << "goto " << target;
54✔
551
    }
54✔
552

553
    void operator()(Packet const& b) {
×
554
        /* Direct packet access, R0 = *(uint *) (skb->data + imm32) */
555
        /* Indirect packet access, R0 = *(uint *) (skb->data + src_reg + imm32) */
556
        const string s = size(b.width);
×
557
        os_ << "r0 = ";
×
558
        os_ << "*(" << s << " *)skb[";
×
UNCOV
559
        if (b.regoffset) {
×
560
            os_ << *b.regoffset;
×
561
        }
562
        if (b.offset != 0) {
×
563
            if (b.regoffset) {
×
UNCOV
564
                os_ << " + ";
×
565
            }
UNCOV
566
            os_ << b.offset;
×
567
        }
UNCOV
568
        os_ << "]";
×
UNCOV
569
    }
×
570

571
    void print(Deref const& access) {
184✔
572
        const string sign = access.offset < 0 ? " - " : " + ";
212✔
573
        const int offset = std::abs(access.offset); // what about INT_MIN?
184✔
574
        os_ << "*(" << size(access.width) << " *)";
276✔
575
        os_ << "(" << access.basereg << sign << offset << ")";
184✔
576
    }
184✔
577

578
    void print(Condition const& cond) {
222✔
579
        os_ << cond.left << " " << ((!cond.is64) ? "w" : "") << cond.op << " " << cond.right;
261✔
580
    }
222✔
581

582
    void operator()(Mem const& b) {
184✔
583
        if (b.is_load) {
184✔
584
            os_ << b.value << " = ";
44✔
585
        }
586
        if (b.is_load && b.is_signed) {
184✔
UNCOV
587
            const string sign = b.access.offset < 0 ? " - " : " + ";
×
UNCOV
588
            const int offset = std::abs(b.access.offset);
×
UNCOV
589
            os_ << "*(" << size(b.access.width, true) << " *)";
×
UNCOV
590
            os_ << "(" << b.access.basereg << sign << offset << ")";
×
UNCOV
591
        } else {
×
592
            print(b.access);
184✔
593
        }
594
        if (!b.is_load) {
184✔
595
            os_ << " = " << b.value;
140✔
596
        }
597
    }
184✔
598

599
    void operator()(Atomic const& b) {
×
600
        os_ << "lock ";
×
601
        print(b.access);
×
602
        os_ << " ";
×
603
        bool showfetch = true;
×
604
        switch (b.op) {
×
605
        case Atomic::Op::ADD: os_ << "+"; break;
×
606
        case Atomic::Op::OR: os_ << "|"; break;
×
607
        case Atomic::Op::AND: os_ << "&"; break;
×
608
        case Atomic::Op::XOR: os_ << "^"; break;
×
609
        case Atomic::Op::XCHG:
×
610
            os_ << "x";
×
UNCOV
611
            showfetch = false;
×
612
            break;
×
UNCOV
613
        case Atomic::Op::CMPXCHG:
×
614
            os_ << "cx";
×
615
            showfetch = false;
×
UNCOV
616
            break;
×
617
        }
UNCOV
618
        os_ << "= " << b.valreg;
×
619

UNCOV
620
        if (showfetch && b.fetch) {
×
UNCOV
621
            os_ << " fetch";
×
622
        }
UNCOV
623
    }
×
624

625
    void operator()(Assume const& b) {
182✔
626
        os_ << "assume ";
182✔
627
        print(b.cond);
182✔
628
    }
182✔
629

UNCOV
630
    void operator()(IncrementLoopCounter const& a) {
×
UNCOV
631
        os_ << variable_registry->loop_counter(to_string(a.name)) << "++";
×
UNCOV
632
    }
×
633
};
634
// ReSharper restore CppMemberFunctionMayBeConst
635

636
std::ostream& operator<<(std::ostream& os, Instruction const& ins) {
188✔
637
    std::visit(CommandPrinterVisitor{os}, ins);
94✔
638
    return os;
94✔
639
}
640

641
string to_string(Instruction const& ins) {
184✔
642
    std::stringstream str;
184✔
643
    str << ins;
184✔
644
    return str.str();
368✔
645
}
184✔
646

647
std::ostream& operator<<(std::ostream& os, const Assertion& a) {
832✔
648
    std::visit(AssertionPrinterVisitor{os}, a);
426✔
649
    return os;
426✔
650
}
651

652
string to_string(Assertion const& constraint) {
810✔
653
    std::stringstream str;
810✔
654
    str << constraint;
810✔
655
    return str.str();
1,620✔
656
}
810✔
657

658
auto get_labels(const InstructionSeq& insts) {
38✔
659
    Pc pc = 0;
38✔
660
    std::map<Label, Pc> pc_of_label;
38✔
661
    for (const auto& [label, inst, _] : insts) {
708✔
662
        pc_of_label[label] = pc;
670✔
663
        pc += size(inst);
670✔
664
    }
665
    return pc_of_label;
38✔
UNCOV
666
}
×
667

668
void print(const InstructionSeq& insts, std::ostream& out, const std::optional<const Label>& label_to_print,
38✔
669
           const bool print_line_info) {
670
    const auto pc_of_label = get_labels(insts);
38✔
671
    Pc pc = 0;
38✔
672
    std::string previous_source;
38✔
673
    CommandPrinterVisitor visitor{out};
38✔
674
    for (const LabeledInstruction& labeled_inst : insts) {
708✔
675
        const auto& [label, ins, line_info] = labeled_inst;
670✔
676
        if (!label_to_print.has_value() || label == label_to_print) {
670✔
677
            if (line_info.has_value() && print_line_info) {
670✔
UNCOV
678
                auto& [file, source, line, column] = line_info.value();
×
679
                // Only decorate the first instruction associated with a source line.
680
                if (source != previous_source) {
×
681
                    out << line_info.value();
×
UNCOV
682
                    previous_source = source;
×
683
                }
684
            }
685
            if (label.isjump()) {
670✔
UNCOV
686
                out << "\n";
×
UNCOV
687
                out << label << ":\n";
×
688
            }
689
            if (label_to_print.has_value()) {
670✔
690
                out << pc << ": ";
×
691
            } else {
692
                out << std::setw(8) << pc << ":\t";
670✔
693
            }
694
            if (const auto jmp = std::get_if<Jmp>(&ins)) {
670✔
695
                if (!pc_of_label.contains(jmp->target)) {
54✔
UNCOV
696
                    throw std::runtime_error(string("Cannot find label ") + to_string(jmp->target));
×
697
                }
698
                const Pc target_pc = pc_of_label.at(jmp->target);
54✔
699
                visitor(*jmp, gsl::narrow<int>(target_pc) - static_cast<int>(pc) - 1);
54✔
700
            } else {
701
                std::visit(visitor, ins);
616✔
702
            }
703
            out << "\n";
670✔
704
        }
705
        pc += size(ins);
670✔
706
    }
707
}
38✔
708

709
std::ostream& operator<<(std::ostream& o, const EbpfMapDescriptor& desc) {
28✔
710
    return o << "(" << "original_fd = " << desc.original_fd << ", " << "inner_map_fd = " << desc.inner_map_fd << ", "
28✔
711
             << "type = " << desc.type << ", " << "max_entries = " << desc.max_entries << ", "
28✔
712
             << "value_size = " << desc.value_size << ", " << "key_size = " << desc.key_size << ")";
28✔
713
}
714

715
void print_map_descriptors(const std::vector<EbpfMapDescriptor>& descriptors, std::ostream& o) {
38✔
716
    int i = 0;
38✔
717
    for (const auto& desc : descriptors) {
66✔
718
        o << "map " << i << ":" << desc << "\n";
28✔
719
        i++;
28✔
720
    }
721
}
38✔
722

UNCOV
723
std::ostream& operator<<(std::ostream& os, const btf_line_info_t& line_info) {
×
UNCOV
724
    os << "; " << line_info.file_name << ":" << line_info.line_number << "\n";
×
UNCOV
725
    os << "; " << line_info.source_line << "\n";
×
UNCOV
726
    return os;
×
727
}
728

729
void print_invariants_filtered(std::ostream& os, const Program& prog, const bool simplify, const AnalysisResult& result,
2✔
730
                               const std::set<Label>& filter, const bool compact,
731
                               const std::map<Label, RelevantState>* relevance) {
732
    DetailedPrinter printer{os, prog};
2✔
733
    const auto basic_blocks = BasicBlock::collect_basic_blocks(prog.cfg(), simplify);
2✔
734

735
    // Build a mapping from each label in a basic block to the block's first label.
736
    // Needed to look up post-invariants for mid-block predecessor labels at join points.
737
    std::map<Label, Label> label_to_block_leader;
2✔
738
    for (const BasicBlock& bb : basic_blocks) {
28✔
739
        for (const Label& label : bb) {
52✔
740
            label_to_block_leader.insert({label, bb.first_label()});
52✔
741
        }
742
    }
743

744
    // Helper to look up the post-invariant for a predecessor label.
745
    // Mid-block labels don't have direct invariant entries, so we map
746
    // through the block leader to find the containing block's post-state.
747
    // Note: when simplify=true, the block leader's post represents the
748
    // entire collapsed block, which is correct for the last instruction
749
    // but approximate for mid-block predecessors. Failure slicing defaults
750
    // to simplify=false, so this approximation is rarely triggered.
751
    auto get_parent_post_invariant = [&](const Label& parent) -> const EbpfDomain* {
1✔
UNCOV
752
        const auto leader_it = label_to_block_leader.find(parent);
×
UNCOV
753
        const Label& lookup_label = (leader_it != label_to_block_leader.end()) ? leader_it->second : parent;
×
UNCOV
754
        const auto inv_it = result.invariants.find(lookup_label);
×
UNCOV
755
        if (inv_it != result.invariants.end() && !inv_it->second.post.is_bottom()) {
×
756
            return &inv_it->second.post;
757
        }
758
        return nullptr;
759
    };
2✔
760

761
    for (const BasicBlock& bb : basic_blocks) {
28✔
762
        // Check if any label in this basic block is in the filter
763
        bool bb_has_filtered_label = false;
26✔
764
        for (const Label& label : bb) {
48✔
765
            if (filter.contains(label)) {
26✔
766
                bb_has_filtered_label = true;
2✔
767
                break;
2✔
768
            }
769
        }
770
        if (!bb_has_filtered_label) {
26✔
771
            continue;
22✔
772
        }
773

774
        // Find the first filtered label in this block to use as the block header
775
        Label first_filtered_label = bb.first_label();
4✔
776
        for (const Label& label : bb) {
4✔
777
            if (filter.contains(label)) {
4✔
778
                first_filtered_label = label;
4✔
779
                break;
2✔
780
            }
781
        }
782

783
        // Use bb.first_label() for reachability check: if the block's entry is unreachable,
784
        // skip the entire block. The filtered label's pre-invariant is printed below.
785
        if (result.invariants.at(bb.first_label()).pre.is_bottom()) {
6✔
UNCOV
786
            continue;
×
787
        }
788

789
        // Print pre-invariant for first filtered label in block (unless compact)
790
        if (!compact) {
4✔
791
            // Set invariant filter if we have relevance info for this label
792
            const auto* label_relevance =
2✔
793
                relevance ? (relevance->contains(first_filtered_label) ? &relevance->at(first_filtered_label) : nullptr)
4✔
794
                          : nullptr;
4✔
795
            os << invariant_filter(label_relevance);
4✔
796
            os << "\nPre-invariant : " << result.invariants.at(first_filtered_label).pre << "\n";
4✔
797
            os << invariant_filter(nullptr); // Clear filter
4✔
798
        }
799

800
        // Print the jump and block header anchored to the basic block entry label
801
        // for correct CFG structure representation.
802
        printer.print_jump("from", bb.first_label());
10✔
803
        os << bb.first_label() << ":\n";
4✔
804

805
        // R3: Show per-predecessor invariants at join points.
806
        // When multiple predecessors exist and at least 2 are in the slice,
807
        // show what each incoming edge contributed to help diagnose lost correlations.
808
        if (!compact && relevance) {
4✔
809
            const auto parents = prog.cfg().parents_of(bb.first_label());
4✔
810
            std::vector<Label> in_slice_parents;
4✔
811
            for (const auto& parent : parents) {
8✔
812
                if (filter.contains(parent)) {
4✔
813
                    in_slice_parents.push_back(parent);
2✔
814
                }
815
            }
816
            if (in_slice_parents.size() >= 2) {
4✔
817
                // Build the union of relevant registers from this label and all in-slice parents
818
                RelevantState join_relevance;
×
819
                if (relevance->contains(first_filtered_label)) {
×
820
                    const auto& fl = relevance->at(first_filtered_label);
×
821
                    join_relevance.registers.insert(fl.registers.begin(), fl.registers.end());
×
822
                    join_relevance.stack_offsets.insert(fl.stack_offsets.begin(), fl.stack_offsets.end());
×
823
                }
UNCOV
824
                for (const auto& parent : in_slice_parents) {
×
UNCOV
825
                    if (relevance->contains(parent)) {
×
826
                        const auto& pr = relevance->at(parent);
×
827
                        join_relevance.registers.insert(pr.registers.begin(), pr.registers.end());
×
828
                        join_relevance.stack_offsets.insert(pr.stack_offsets.begin(), pr.stack_offsets.end());
×
829
                    }
830
                }
831

832
                os << "  --- join point: per-predecessor state ---\n";
×
UNCOV
833
                for (const auto& parent : in_slice_parents) {
×
UNCOV
834
                    const auto* post = get_parent_post_invariant(parent);
×
835
                    if (post) {
×
836
                        os << invariant_filter(&join_relevance);
×
UNCOV
837
                        os << "  from " << parent << ": " << *post << "\n";
×
UNCOV
838
                        os << invariant_filter(nullptr);
×
839
                    }
840
                }
841
                os << "  --- end join point ---\n";
×
UNCOV
842
            }
×
843
        }
4✔
844

845
        if (first_filtered_label != bb.first_label()) {
6✔
846
            // Indicate that some labels/instructions were skipped due to filtering.
UNCOV
847
            os << "  ... skipped ...\n";
×
848
        }
849

850
        Label last_label = bb.first_label();
4✔
851
        Label prev_filtered_label = bb.first_label();
4✔
852
        bool has_prev_filtered = false;
4✔
853
        for (const Label& label : bb) {
8✔
854
            if (!filter.contains(label)) {
4✔
UNCOV
855
                continue;
×
856
            }
857

858
            // If there was a gap since the previous filtered label in this block,
859
            // close the previous label's output and show a skip indicator.
860
            if (has_prev_filtered && prev_filtered_label != label) {
4✔
861
                // Print post-invariant and goto for the previous filtered label
862
                if (!compact) {
×
863
                    const auto& prev_current = result.invariants.at(prev_filtered_label);
×
864
                    if (!prev_current.post.is_bottom()) {
×
865
                        const auto* prev_label_relevance =
866
                            relevance ? (relevance->contains(prev_filtered_label) ? &relevance->at(prev_filtered_label)
×
867
                                                                                  : nullptr)
UNCOV
868
                                      : nullptr;
×
UNCOV
869
                        os << invariant_filter(prev_label_relevance);
×
870
                        printer.print_jump("goto", prev_filtered_label);
×
871
                        os << "\nPost-invariant : " << prev_current.post << "\n";
×
872
                        os << invariant_filter(nullptr);
×
873
                    }
874
                }
875
                // Check if there are skipped labels between prev and current
UNCOV
876
                bool has_gap = false;
×
UNCOV
877
                for (const Label& mid : bb) {
×
878
                    if (mid <= prev_filtered_label) {
×
879
                        continue;
×
880
                    }
881
                    if (mid >= label) {
×
882
                        break;
883
                    }
UNCOV
884
                    has_gap = true;
×
885
                    break;
×
886
                }
887
                if (has_gap) {
×
888
                    os << "  ... skipped ...\n";
×
889
                }
890
                // Print pre-invariant for this label
891
                if (!compact) {
×
892
                    const auto* label_rel =
UNCOV
893
                        relevance ? (relevance->contains(label) ? &relevance->at(label) : nullptr) : nullptr;
×
UNCOV
894
                    os << invariant_filter(label_rel);
×
UNCOV
895
                    os << "\nPre-invariant : " << result.invariants.at(label).pre << "\n";
×
UNCOV
896
                    os << invariant_filter(nullptr);
×
UNCOV
897
                    printer.print_jump("from", label);
×
898
                }
899
            }
900

901
            printer.print_line_info(label);
4✔
902

903
            // Print assertions, filtered by relevance if provided
904
            const auto* label_relevance =
2✔
905
                relevance ? (relevance->contains(label) ? &relevance->at(label) : nullptr) : nullptr;
4✔
906
            for (const auto& assertion : prog.assertions_at(label)) {
6✔
907
                // If we have relevance info, only print assertions involving relevant registers.
908
                // Assertions with no register deps (e.g., BoundedLoopCount) are always
909
                // printed to avoid hiding the failing assertion from the slice output.
910
                if (label_relevance) {
2✔
911
                    auto assertion_regs = extract_assertion_registers(assertion);
2✔
912
                    if (!assertion_regs.empty()) {
2✔
913
                        bool is_relevant = false;
2✔
914
                        for (const auto& reg : assertion_regs) {
2✔
915
                            if (label_relevance->registers.contains(reg)) {
2✔
916
                                is_relevant = true;
1✔
917
                                break;
1✔
918
                            }
919
                        }
920
                        if (!is_relevant) {
2✔
UNCOV
921
                            continue; // Skip this assertion
×
922
                        }
923
                    }
924
                }
2✔
925
                os << "  assert " << assertion << ";\n";
2✔
926
            }
4✔
927
            os << "  " << prog.instruction_at(label) << ";\n";
4✔
928

929
            last_label = label;
4✔
930
            prev_filtered_label = label;
4✔
931
            has_prev_filtered = true;
4✔
932

933
            const auto& current = result.invariants.at(label);
4✔
934
            if (current.error) {
4✔
935
                os << "\nVerification error:\n";
2✔
936
                print_error(os, *current.error);
2✔
937
                os << "\n";
2✔
938
            }
939
        }
940

941
        // Print post-invariant (unless compact)
942
        if (!compact) {
4✔
943
            const auto& current = result.invariants.at(last_label);
4✔
944
            if (!current.post.is_bottom()) {
4✔
945
                // Set invariant filter for post-invariant
946
                const auto* label_relevance =
1✔
947
                    relevance ? (relevance->contains(last_label) ? &relevance->at(last_label) : nullptr) : nullptr;
2✔
948
                os << invariant_filter(label_relevance);
2✔
949
                printer.print_jump("goto", last_label);
2✔
950
                os << "\nPost-invariant : " << current.post << "\n";
2✔
951
                os << invariant_filter(nullptr); // Clear filter
2✔
952
            }
953
        }
954
    }
8✔
955
    os << "\n";
2✔
956
}
2✔
957

958
void print_failure_slices(std::ostream& os, const Program& prog, const bool simplify, const AnalysisResult& result,
2✔
959
                          const std::vector<FailureSlice>& slices, const bool compact) {
960
    if (slices.empty()) {
2✔
UNCOV
961
        os << "No verification failures found.\n";
×
UNCOV
962
        return;
×
963
    }
964

965
    for (size_t i = 0; i < slices.size(); ++i) {
4✔
966
        const auto& slice = slices[i];
2✔
967

968
        os << "=== Failure Slice " << (i + 1) << " of " << slices.size() << " ===\n\n";
2✔
969

970
        // Print error summary
971
        os << "[ERROR] " << slice.error.what() << "\n";
2✔
972
        os << "[LOCATION] " << slice.failing_label << "\n";
2✔
973

974
        // Print relevant registers at failure point
975
        const auto it = slice.relevance.find(slice.failing_label);
2✔
976
        if (it != slice.relevance.end()) {
2✔
977
            os << "[RELEVANT REGISTERS] ";
2✔
978
            bool first = true;
2✔
979
            for (const auto& reg : it->second.registers) {
4✔
980
                if (!first) {
2✔
981
                    os << ", ";
×
982
                }
983
                os << "r" << static_cast<int>(reg.v);
2✔
984
                first = false;
2✔
985
            }
986
            if (!it->second.stack_offsets.empty()) {
2✔
UNCOV
987
                for (const auto& offset : it->second.stack_offsets) {
×
UNCOV
988
                    if (!first) {
×
UNCOV
989
                        os << ", ";
×
990
                    }
UNCOV
991
                    os << "stack[" << offset << "]";
×
UNCOV
992
                    first = false;
×
993
                }
994
            }
995
            os << "\n";
2✔
996
        }
997

998
        os << "[SLICE SIZE] " << slice.relevance.size() << " program points\n\n";
2✔
999

1000
        // Print a compact control-flow summary showing the branch-path skeleton
1001
        // through the slice. Lists labels in order with Assume/Jmp annotations.
1002
        // At join points (labels with ≥2 in-slice predecessors), the converging
1003
        // predecessors are grouped as {pred1 | pred2} → join_label.
1004
        {
1✔
1005
            os << "[CONTROL FLOW] ";
2✔
1006
            // Collect and sort impacted labels
1007
            auto labels = slice.impacted_labels();
2✔
1008

1009
            // Build a map: join_label → set of in-slice predecessors
1010
            // Also collect which labels are convergence predecessors (to skip them in linear output)
1011
            std::map<Label, std::vector<Label>> join_predecessors;
2✔
1012
            for (const auto& lbl : labels) {
6✔
1013
                const auto& parents = prog.cfg().parents_of(lbl);
4✔
1014
                std::vector<Label> in_slice_parents;
4✔
1015
                for (const auto& p : parents) {
8✔
1016
                    if (labels.contains(p)) {
4✔
1017
                        in_slice_parents.push_back(p);
2✔
1018
                    }
1019
                }
1020
                if (in_slice_parents.size() >= 2) {
4✔
UNCOV
1021
                    join_predecessors[lbl] = in_slice_parents;
×
1022
                }
1023
            }
4✔
1024
            // Labels consumed by a {..|..} group are skipped in linear output,
1025
            // unless they are themselves join points (nested joins).
1026
            std::set<Label> convergence_members;
2✔
1027
            for (const auto& [join_lbl, preds] : join_predecessors) {
2✔
UNCOV
1028
                for (const auto& p : preds) {
×
UNCOV
1029
                    if (!join_predecessors.contains(p)) {
×
UNCOV
1030
                        convergence_members.insert(p);
×
1031
                    }
1032
                }
1033
            }
1034

1035
            // Helper to annotate a label with its instruction type
1036
            auto annotate_label = [&](const Label& lbl) {
5✔
1037
                os << lbl;
4✔
1038
                const auto& ins = prog.instruction_at(lbl);
4✔
1039
                if (const auto* assume = std::get_if<Assume>(&ins)) {
4✔
UNCOV
1040
                    os << " (assume " << assume->cond.left << " " << assume->cond.op << " " << assume->cond.right
×
UNCOV
1041
                       << ")";
×
1042
                } else if (const auto* jmp = std::get_if<Jmp>(&ins)) {
4✔
UNCOV
1043
                    if (jmp->cond) {
×
UNCOV
1044
                        os << " (if " << jmp->cond->left << " " << jmp->cond->op << " " << jmp->cond->right << ")";
×
1045
                    }
1046
                }
1047
            };
6✔
1048

1049
            bool first_cf = true;
2✔
1050
            for (const auto& lbl : labels) {
6✔
1051
                // Skip labels that are part of a convergence group (printed with their join)
1052
                if (convergence_members.contains(lbl)) {
4✔
UNCOV
1053
                    continue;
×
1054
                }
1055

1056
                if (!first_cf) {
4✔
1057
                    os << ", ";
2✔
1058
                }
1059
                first_cf = false;
4✔
1060

1061
                // If this label is a join point, print {pred1 | pred2} → lbl
1062
                if (join_predecessors.contains(lbl)) {
4✔
1063
                    os << "{";
×
1064
                    bool first_pred = true;
×
UNCOV
1065
                    for (const auto& pred : join_predecessors.at(lbl)) {
×
1066
                        if (!first_pred) {
×
UNCOV
1067
                            os << " | ";
×
1068
                        }
UNCOV
1069
                        first_pred = false;
×
UNCOV
1070
                        annotate_label(pred);
×
1071
                    }
UNCOV
1072
                    os << "} -> ";
×
1073
                }
1074

1075
                annotate_label(lbl);
4✔
1076
            }
1077
            if (labels.contains(slice.failing_label)) {
2✔
1078
                os << " FAIL";
2✔
1079
            }
1080
            os << "\n\n";
2✔
1081
        }
2✔
1082

1083
        // Print the filtered CFG with assertion filtering based on relevance
1084
        os << "[CAUSAL TRACE]\n";
2✔
1085
        print_invariants_filtered(os, prog, simplify, result, slice.impacted_labels(), compact, &slice.relevance);
2✔
1086

1087
        if (i + 1 < slices.size()) {
2✔
UNCOV
1088
            os << "\n";
×
1089
        }
1090
    }
1091
}
1092

1093
} // namespace prevail
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