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

Alan-Jowett / ebpf-verifier / 27778108035

07 Jun 2026 06:51PM UTC coverage: 86.386% (-2.5%) from 88.93%
27778108035

push

github

elazarg
Release v0.2.5

Bump project version to 0.2.5 and add a CHANGELOG entry covering ELF loader hardening, numeric-domain soundness fixes, and the writable helper output initialization documentation update since v0.2.4. Also updates the using_installed_package example version requirement.

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

9125 of 10563 relevant lines covered (86.39%)

6334294.72 hits per line

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

57.04
/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 <optional>
8
#include <variant>
9
#include <vector>
10

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

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

26
namespace prevail {
27

28
// Rich-printing helper: render a single instruction, resolving Calls against
29
// the program's platform so helper names and arg lists appear in the output.
30
// Defined below near CommandPrinterVisitor; forward-declared here so early
31
// top-level printers (print_dot, DetailedPrinter::print_instruction) can use it.
32
static void print_instruction_rich(std::ostream& os, const Program& prog, const Instruction& ins);
33

34
std::ostream& operator<<(std::ostream& o, const Interval& interval) {
1,356✔
35
    if (interval.is_bottom()) {
1,356✔
36
        o << "_|_";
×
37
    } else {
38
        o << "[" << interval._lb << ", " << interval._ub << "]";
1,356✔
39
    }
40
    return o;
1,356✔
41
}
42
static std::string int128_to_string(Int128 n) {
1,128,576✔
43
    if (n == 0) {
1,128,576✔
44
        return "0";
1,533✔
45
    }
46
    bool negative = false;
1,127,554✔
47
    if (n < 0) {
1,127,554✔
48
        negative = true;
542✔
49
        // Handle kInt128Min: negate via unsigned to avoid overflow.
50
        if (n == kInt128Min) {
542✔
51
            // kInt128Min == -170141183460469231731687303715884105728
52
            // Build the string for the absolute value via unsigned arithmetic.
53
            auto u = static_cast<UInt128>(n);
×
54
            // Two's complement: UInt128(kInt128Min) is the correct magnitude.
55
            std::string result;
×
56
            while (u != 0) {
×
57
                result += static_cast<char>('0' + static_cast<int>(u % 10));
×
58
                u /= 10;
×
59
            }
60
            result += '-';
×
61
            std::ranges::reverse(result);
×
62
            return result;
×
63
        }
×
64
        n = -n;
542✔
65
    }
66
    std::string result;
1,127,554✔
67
    while (n > 0) {
5,636,332✔
68
        result += static_cast<char>('0' + static_cast<int>(n % 10));
4,508,778✔
69
        n /= 10;
4,508,778✔
70
    }
71
    if (negative) {
1,127,554✔
72
        result += '-';
542✔
73
    }
74
    std::ranges::reverse(result);
1,127,554✔
75
    return result;
1,127,554✔
76
}
1,127,554✔
77

78
std::ostream& operator<<(std::ostream& o, const Number& z) { return o << z.to_string(); }
1,128,576✔
79

80
std::string Number::to_string() const { return int128_to_string(_n); }
1,128,576✔
81

82
std::string Interval::to_string() const {
×
83
    std::ostringstream s;
×
84
    s << *this;
×
85
    return s.str();
×
86
}
×
87

88
std::ostream& operator<<(std::ostream& os, const Label& label) {
35,322✔
89
    if (label == Label::entry) {
35,322✔
90
        return os << "entry";
6✔
91
    }
92
    if (label == Label::exit) {
35,316✔
93
        return os << "exit";
10✔
94
    }
95
    if (!label.stack_frame_prefix.empty()) {
35,306✔
96
        os << label.stack_frame_prefix << STACK_FRAME_DELIMITER;
758✔
97
    }
98
    os << label.from;
35,306✔
99
    if (label.to != -1) {
35,306✔
100
        os << ":" << label.to;
52✔
101
    }
102
    if (!label.special_label.empty()) {
35,306✔
103
        os << " (" << label.special_label << ")";
22✔
104
    }
105
    return os;
17,653✔
106
}
107

108
string to_string(Label const& label) {
34,920✔
109
    std::stringstream str;
34,920✔
110
    str << label;
34,920✔
111
    return str.str();
69,840✔
112
}
34,920✔
113

114
struct LineInfoPrinter {
115
    std::ostream& os;
116
    const std::map<size_t, btf_line_info_t> line_info_map;
117
    bool print_line_info_enabled{};
118
    std::string previous_source_line;
119

120
    void print_line_info(const Label& label) {
6✔
121
        if (print_line_info_enabled) {
6✔
122
            const auto& line_info = line_info_map.find(label.from);
×
123
            if (line_info != line_info_map.end() && line_info->second.source_line != previous_source_line) {
×
124
                os << "\n" << line_info->second << "\n";
×
125
                previous_source_line = line_info->second.source_line;
×
126
            }
127
        }
128
    }
6✔
129
};
130

131
struct DetailedPrinter : LineInfoPrinter {
132
    const Program& prog;
133

134
    DetailedPrinter(std::ostream& os, const Program& prog, const bool print_line_info = false)
2✔
135
        : LineInfoPrinter{os, prog.info().line_info, print_line_info}, prog(prog) {}
2✔
136

137
    void print_labels(const std::string& direction, const std::set<Label>& labels) {
6✔
138
        auto [it, et] = std::pair{labels.begin(), labels.end()};
6✔
139
        if (it != et) {
6✔
140
            os << "  " << direction << " ";
6✔
141
            while (it != et) {
12✔
142
                os << *it;
6✔
143
                ++it;
6✔
144
                if (it == et) {
6✔
145
                    os << ";";
6✔
146
                } else {
147
                    os << ",";
×
148
                }
149
            }
150
        }
151
        os << "\n";
6✔
152
    }
6✔
153

154
    void print_jump(const std::string& direction, const Label& label) {
6✔
155
        print_labels(direction, direction == "from" ? prog.cfg().parents_of(label) : prog.cfg().children_of(label));
6✔
156
    }
6✔
157

158
    void print_instruction(const Program& prog, const Label& label) {
×
159
        for (const auto& pre : prog.assertions_at(label)) {
×
160
            os << "  " << "assert " << pre << ";\n";
×
161
        }
×
162
        os << "  ";
×
163
        print_instruction_rich(os, prog, prog.instruction_at(label));
×
164
        os << ";\n";
×
165
    }
×
166
};
167

168
void print_program(const Program& prog, std::ostream& os, const VerbosityOptions& verbosity) {
×
169
    DetailedPrinter printer{os, prog, verbosity.print_line_info};
×
170
    for (const BasicBlock& bb : BasicBlock::collect_basic_blocks(prog.cfg(), verbosity.simplify)) {
×
171
        printer.print_jump("from", bb.first_label());
×
172
        os << bb.first_label() << ":\n";
×
173
        for (const Label& label : bb) {
×
174
            printer.print_line_info(label);
×
175
            printer.print_instruction(prog, label);
×
176
        }
177
        printer.print_jump("goto", bb.last_label());
×
178
    }
×
179
    os << "\n";
×
180
}
×
181

182
void print_invariants(std::ostream& os, const Program& prog, const AnalysisResult& result,
×
183
                      const VerbosityOptions& verbosity) {
184
    DetailedPrinter printer{os, prog, verbosity.print_line_info};
×
185
    for (const BasicBlock& bb : BasicBlock::collect_basic_blocks(prog.cfg(), verbosity.simplify)) {
×
186
        if (result.invariants.at(bb.first_label()).pre.is_bottom()) {
×
187
            continue;
×
188
        }
189
        os << "\nPre-invariant : " << result.invariants.at(bb.first_label()).pre << "\n";
×
190
        printer.print_jump("from", bb.first_label());
×
191
        os << bb.first_label() << ":\n";
×
192
        Label last_label = bb.first_label();
×
193
        for (const Label& label : bb) {
×
194
            printer.print_line_info(label);
×
195
            printer.print_instruction(prog, label);
×
196
            last_label = label;
×
197

198
            const auto& current = result.invariants.at(last_label);
×
199
            if (current.error) {
×
200
                os << "\nVerification error:\n";
×
201
                if (label != bb.last_label()) {
×
202
                    os << "After " << current.pre << "\n";
×
203
                }
204
                print_error(os, *current.error, prog, verbosity);
×
205
                os << "\n";
×
206
                return;
×
207
            }
208
        }
209
        const auto& current = result.invariants.at(last_label);
×
210
        if (!current.post.is_bottom()) {
×
211
            printer.print_jump("goto", last_label);
×
212
            os << "\nPost-invariant : " << current.post << "\n";
×
213
        }
214
    }
×
215
    os << "\n";
×
216
}
×
217

218
void print_dot(const Program& prog, std::ostream& out) {
×
219
    out << "digraph program {\n";
×
220
    out << "    node [shape = rectangle];\n";
×
221
    for (const auto& label : prog.labels()) {
×
222
        out << "    \"" << label << "\"[xlabel=\"" << label << "\",label=\"";
×
223

224
        for (const auto& pre : prog.assertions_at(label)) {
×
225
            out << "assert " << pre << "\\l";
×
226
        }
×
227
        print_instruction_rich(out, prog, prog.instruction_at(label));
×
228
        out << "\\l";
×
229

230
        out << "\"];\n";
×
231
        for (const Label& next : prog.cfg().children_of(label)) {
×
232
            out << "    \"" << label << "\" -> \"" << next << "\";\n";
×
233
        }
234
        out << "\n";
×
235
    }
236
    out << "}\n";
×
237
}
×
238

239
void print_dot(const Program& prog, const std::string& outfile) {
×
240
    std::ofstream out{outfile};
×
241
    if (out.fail()) {
×
242
        throw RuntimeInputError(std::string("Could not open file ") + outfile);
×
243
    }
244
    print_dot(prog, out);
×
245
}
×
246

247
void print_unreachable(std::ostream& os, const Program& prog, const AnalysisResult& result) {
×
248
    for (const auto& [label, notes] : result.find_unreachable(prog)) {
×
249
        for (const auto& msg : notes) {
×
250
            os << label << ": " << msg << "\n";
×
251
        }
252
    }
×
253
    os << "\n";
×
254
}
×
255

256
std::string to_string(const VerificationError& error) {
338✔
257
    std::stringstream ss;
338✔
258
    if (const auto& label = error.where) {
338✔
259
        ss << *label << ": ";
338✔
260
    }
261
    ss << error.message;
338✔
262
    return ss.str();
676✔
263
}
338✔
264

265
void print_error(std::ostream& os, const VerificationError& error, const Program& prog,
2✔
266
                 const VerbosityOptions& verbosity) {
267
    LineInfoPrinter printer{os, prog.info().line_info, verbosity.print_line_info};
2✔
268
    if (const auto& label = error.where) {
2✔
269
        printer.print_line_info(*label);
2✔
270
        os << *label << ": ";
2✔
271
    }
272
    os << error.message << "\n";
2✔
273
    os << "\n";
2✔
274
}
2✔
275

276
std::ostream& operator<<(std::ostream& os, const ArgSingle::Kind kind) {
74✔
277
    switch (kind) {
74✔
278
    case ArgSingle::Kind::ANYTHING: return os << "uint64_t";
14✔
279
    case ArgSingle::Kind::PTR_TO_CTX: return os << "ctx";
4✔
280
    case ArgSingle::Kind::PTR_TO_STACK: return os << "stack";
×
281
    case ArgSingle::Kind::PTR_TO_FUNC: return os << "func";
×
282
    case ArgSingle::Kind::MAP_FD: return os << "map_fd";
24✔
283
    case ArgSingle::Kind::MAP_FD_PROGRAMS: return os << "map_fd_programs";
×
284
    case ArgSingle::Kind::PTR_TO_MAP_KEY: return os << "map_key";
24✔
285
    case ArgSingle::Kind::PTR_TO_MAP_VALUE: return os << "map_value";
8✔
286
    case ArgSingle::Kind::PTR_TO_SOCKET: return os << "socket";
×
287
    case ArgSingle::Kind::PTR_TO_BTF_ID: return os << "btf_id";
×
288
    case ArgSingle::Kind::PTR_TO_ALLOC_MEM: return os << "alloc_mem";
×
289
    case ArgSingle::Kind::PTR_TO_SPIN_LOCK: return os << "spin_lock";
×
290
    case ArgSingle::Kind::PTR_TO_TIMER: return os << "timer";
×
291
    case ArgSingle::Kind::CONST_SIZE_OR_ZERO: return os << "size?";
×
292
    case ArgSingle::Kind::PTR_TO_WRITABLE_LONG: return os << "out_long";
×
293
    case ArgSingle::Kind::PTR_TO_WRITABLE_INT: return os << "out_int";
×
294
    }
295
    std::unreachable();
296
}
297

298
std::ostream& operator<<(std::ostream& os, const ArgPair::Kind kind) {
×
299
    switch (kind) {
×
300
    case ArgPair::Kind::PTR_TO_READABLE_MEM: return os << "mem";
×
301
    case ArgPair::Kind::PTR_TO_WRITABLE_MEM: return os << "out";
×
302
    }
303
    std::unreachable();
304
}
305

306
std::ostream& operator<<(std::ostream& os, const ArgSingle arg) {
74✔
307
    os << arg.kind;
74✔
308
    if (arg.or_null) {
74✔
309
        os << "?";
×
310
    }
311
    os << " " << arg.reg;
74✔
312
    return os;
74✔
313
}
314

315
std::ostream& operator<<(std::ostream& os, const ArgPair arg) {
×
316
    os << arg.kind;
×
317
    if (arg.or_null) {
×
318
        os << "?";
×
319
    }
320
    os << " " << arg.mem << "[" << arg.size;
×
321
    if (arg.can_be_zero) {
×
322
        os << "?";
×
323
    }
324
    os << "], uint64_t " << arg.size;
×
325
    return os;
×
326
}
327

328
std::ostream& operator<<(std::ostream& os, const Bin::Op op) {
280✔
329
    using Op = Bin::Op;
140✔
330
    switch (op) {
280✔
331
    case Op::MOV: return os;
81✔
332
    case Op::MOVSX8: return os << "s8";
×
333
    case Op::MOVSX16: return os << "s16";
×
334
    case Op::MOVSX32: return os << "s32";
×
335
    case Op::ADD: return os << "+";
72✔
336
    case Op::SUB: return os << "-";
×
337
    case Op::MUL: return os << "*";
×
338
    case Op::UDIV: return os << "/";
×
339
    case Op::SDIV: return os << "s/";
×
340
    case Op::UMOD: return os << "%";
×
341
    case Op::SMOD: return os << "s%";
×
342
    case Op::OR: return os << "|";
×
343
    case Op::AND: return os << "&";
30✔
344
    case Op::LSH: return os << "<<";
12✔
345
    case Op::RSH: return os << ">>";
×
346
    case Op::ARSH: return os << ">>>";
2✔
347
    case Op::XOR: return os << "^";
2✔
348
    }
349
    std::unreachable();
350
}
351

352
std::ostream& operator<<(std::ostream& os, const Condition::Op op) {
224✔
353
    using Op = Condition::Op;
112✔
354
    switch (op) {
224✔
355
    case Op::EQ: return os << "==";
38✔
356
    case Op::NE: return os << "!=";
10✔
357
    case Op::SET: return os << "&==";
×
358
    case Op::NSET: return os << "&!="; // not in ebpf
×
359
    case Op::LT: return os << "<";     // TODO: os << "u<";
124✔
360
    case Op::LE: return os << "<=";    // TODO: os << "u<=";
12✔
361
    case Op::GT: return os << ">";     // TODO: os << "u>";
22✔
362
    case Op::GE: return os << ">=";    // TODO: os << "u>=";
8✔
363
    case Op::SLT: return os << "s<";
4✔
364
    case Op::SLE: return os << "s<=";
×
365
    case Op::SGT: return os << "s>";
4✔
366
    case Op::SGE: return os << "s>=";
2✔
367
    }
368
    std::unreachable();
369
}
370

371
static string size(const int w, const bool is_signed = false) {
184✔
372
    return string(is_signed ? "s" : "u") + std::to_string(w * 8);
552✔
373
}
374

375
// ReSharper disable CppMemberFunctionMayBeConst
376
struct AssertionPrinterVisitor {
377
    std::ostream& _os;
378

379
    void operator()(ValidStore const& a) {
2✔
380
        _os << a.mem << ".type != stack -> " << TypeConstraint{a.val, TypeGroup::number};
2✔
381
    }
2✔
382

383
    void operator()(ValidAccess const& a) {
834✔
384
        if (a.or_null) {
834✔
385
            _os << "(" << TypeConstraint{a.reg, TypeGroup::number} << " and " << a.reg << ".value == 0) or ";
36✔
386
        }
387
        _os << "valid_access(" << a.reg << ".offset";
834✔
388
        if (a.offset > 0) {
834✔
389
            _os << "+" << a.offset;
100✔
390
        } else if (a.offset < 0) {
734✔
391
            _os << a.offset;
6✔
392
        }
393

394
        if (a.width == Value{Imm{0}}) {
834✔
395
            // a.width == 0, meaning we only care it's an in-bound pointer,
396
            // so it can be compared with another pointer to the same region.
397
            _os << ") for comparison/subtraction";
12✔
398
        } else {
399
            _os << ", width=" << a.width << ") for ";
822✔
400
            if (a.access_type == AccessType::read) {
822✔
401
                _os << "read";
726✔
402
            } else {
403
                _os << "write";
96✔
404
            }
405
        }
406
    }
834✔
407

408
    void operator()(const BoundedLoopCount& a) {
22✔
409
        _os << variable_registry.loop_counter(to_string(a.name)) << " < " << BoundedLoopCount::limit;
22✔
410
    }
22✔
411

412
    void operator()(ValidSize const& a) {
100✔
413
        const auto op = a.can_be_zero ? " >= " : " > ";
100✔
414
        _os << a.reg << ".value" << op << 0;
100✔
415
    }
100✔
416

417
    void operator()(ValidMapKeyValue const& a) {
58✔
418
        _os << "within(" << a.access_reg << ":" << (a.key ? "key_size" : "value_size") << "(" << a.map_fd_reg << "))";
67✔
419
    }
58✔
420

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

423
    void operator()(ValidMapType const& a) { _os << "valid_map_type(" << a.map_fd_reg << ", " << a.helper_name << ")"; }
×
424

425
    void operator()(ZeroCtxOffset const& a) { _os << variable_registry.reg(DataKind::ctx_offsets, a.reg.v) << " == 0"; }
2✔
426

427
    void operator()(Comparable const& a) {
10✔
428
        if (a.or_r2_is_number) {
10✔
429
            _os << TypeConstraint{a.r2, TypeGroup::number} << " or ";
15✔
430
        }
431
        _os << variable_registry.type_reg(a.r1.v) << " == " << variable_registry.type_reg(a.r2.v) << " in "
10✔
432
            << TypeGroup::singleton_ptr;
10✔
433
    }
10✔
434

435
    void operator()(Addable const& a) {
14✔
436
        _os << TypeConstraint{a.ptr, TypeGroup::pointer} << " -> " << TypeConstraint{a.num, TypeGroup::number};
28✔
437
    }
14✔
438

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

441
    void operator()(TypeConstraint const& tc) {
698✔
442
        const string cmp_op = is_singleton_type(tc.types) ? "==" : "in";
797✔
443
        _os << variable_registry.type_reg(tc.reg.v) << " " << cmp_op << " " << tc.types;
698✔
444
    }
698✔
445

446
    void operator()(FuncConstraint const& fc) { _os << variable_registry.type_reg(fc.reg.v) << " is helper"; }
6✔
447

448
    void operator()(ValidArgZero const& a) { _os << a.reg << " == 0"; }
×
449
};
450

451
// ReSharper disable CppMemberFunctionMayBeConst
452
struct CommandPrinterVisitor {
453
    std::ostream& os_;
454
    // Optional ProgramInfo: when present, Call instructions are resolved and
455
    // printed with helper name + argument list (e.g., "r0 = bpf_map_lookup_elem:
456
    // 1(map_fd r1, map_key r2)"). When absent, Call prints as the cheap
457
    // "r0 = call:<func>" form.
458
    const ProgramInfo* program_info_ = nullptr;
459

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

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

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

466
    void operator()(LoadPseudo const& b) {
×
467
        os_ << b.dst << " = ";
×
468
        switch (b.addr.kind) {
×
469
        case PseudoAddress::Kind::VARIABLE_ADDR: os_ << "variable_addr(" << b.addr.imm << ")"; break;
×
470
        case PseudoAddress::Kind::CODE_ADDR: os_ << "code_addr(" << b.addr.imm << ")"; break;
×
471
        case PseudoAddress::Kind::MAP_BY_IDX: os_ << "map_by_idx(" << b.addr.imm << ")"; break;
×
472
        case PseudoAddress::Kind::MAP_VALUE_BY_IDX:
×
473
            os_ << "mva(map_by_idx(" << b.addr.imm << ")) + " << b.addr.next_imm;
×
474
            break;
×
475
        }
476
    }
×
477

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

482
    void operator()(Bin const& b) {
280✔
483
        os_ << reg_name(b.dst, b.is64) << " " << b.op << "= " << b.v;
420✔
484
        if (b.lddw) {
280✔
485
            os_ << " ll";
2✔
486
        }
487
    }
280✔
488

489
    void operator()(Un const& b) {
12✔
490
        os_ << b.dst << " = ";
12✔
491
        switch (b.op) {
12✔
492
        case Un::Op::BE16: os_ << "be16 "; break;
2✔
493
        case Un::Op::BE32: os_ << "be32 "; break;
2✔
494
        case Un::Op::BE64: os_ << "be64 "; break;
2✔
495
        case Un::Op::LE16: os_ << "le16 "; break;
2✔
496
        case Un::Op::LE32: os_ << "le32 "; break;
2✔
497
        case Un::Op::LE64: os_ << "le64 "; break;
2✔
498
        case Un::Op::SWAP16: os_ << "swap16 "; break;
×
499
        case Un::Op::SWAP32: os_ << "swap32 "; break;
×
500
        case Un::Op::SWAP64: os_ << "swap64 "; break;
×
501
        case Un::Op::NEG: os_ << "-"; break;
×
502
        }
503
        os_ << b.dst;
12✔
504
    }
12✔
505

506
    void operator()(Call const& call) {
70✔
507
        if (program_info_ != nullptr) {
70✔
508
            // Rich print: resolve and emit helper name + arg list.
509
            const ResolvedCall r = resolve(call, *program_info_);
70✔
510
            os_ << "r0 = " << r.name << ":" << r.call.func << "(";
70✔
511
            for (uint8_t reg = 1; reg <= 5; reg++) {
144✔
512
                auto single =
72✔
513
                    std::ranges::find_if(r.contract.singles, [reg](const ArgSingle& a) { return a.reg.v == reg; });
372✔
514
                if (single != r.contract.singles.end()) {
144✔
515
                    if (reg > 1) {
74✔
516
                        os_ << ", ";
48✔
517
                    }
518
                    os_ << *single;
74✔
519
                    continue;
74✔
520
                }
521
                auto pair = std::ranges::find_if(r.contract.pairs, [reg](const ArgPair& a) { return a.mem.v == reg; });
70✔
522
                if (pair != r.contract.pairs.end()) {
70✔
523
                    if (reg > 1) {
×
524
                        os_ << ", ";
×
525
                    }
526
                    os_ << *pair;
×
527
                    reg++;
×
528
                    continue;
×
529
                }
530
                break;
70✔
531
            }
532
            os_ << ")";
70✔
533
            return;
70✔
534
        }
70✔
535
        // Cheap print: context-free, key only. Used when no ProgramInfo is
536
        // available (e.g., Instruction operator<< out of an analysis context).
537
        const char* kind_label = "call";
×
538
        switch (call.kind) {
×
539
        case CallKind::helper: kind_label = "call"; break;
540
        case CallKind::kfunc: kind_label = "call_kfunc"; break;
×
541
        case CallKind::builtin: kind_label = "call_builtin"; break;
×
542
        }
543
        os_ << "r0 = " << kind_label << ":" << call.func;
×
544
    }
545

546
    void operator()(CallLocal const& call) { os_ << "call <" << to_string(call.target) << ">"; }
×
547

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

550
    void operator()(CallBtf const& call) {
×
551
        os_ << "call_btf " << call.btf_id;
×
552
        if (call.module != 0) {
×
553
            os_ << " module " << call.module;
×
554
        }
555
    }
×
556

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

559
    void operator()(Jmp const& b) {
×
560
        // A "standalone" jump Instruction.
561
        // Print the label without offset calculations.
562
        if (b.cond) {
×
563
            os_ << "if ";
×
564
            print(*b.cond);
×
565
            os_ << " ";
×
566
        }
567
        os_ << "goto label <" << to_string(b.target) << ">";
×
568
    }
×
569

570
    void operator()(Jmp const& b, const int offset) {
54✔
571
        const string sign = offset > 0 ? "+" : "";
54✔
572
        const string target = sign + std::to_string(offset) + " <" + to_string(b.target) + ">";
108✔
573

574
        if (b.cond) {
54✔
575
            os_ << "if ";
40✔
576
            print(*b.cond);
40✔
577
            os_ << " ";
40✔
578
        }
579
        os_ << "goto " << target;
54✔
580
    }
54✔
581

582
    void operator()(Packet const& b) {
×
583
        /* Direct packet access, R0 = *(uint *) (skb->data + imm32) */
584
        /* Indirect packet access, R0 = *(uint *) (skb->data + src_reg + imm32) */
585
        const string s = size(b.width);
×
586
        os_ << "r0 = ";
×
587
        os_ << "*(" << s << " *)skb[";
×
588
        if (b.regoffset) {
×
589
            os_ << *b.regoffset;
×
590
        }
591
        if (b.offset != 0) {
×
592
            if (b.regoffset) {
×
593
                os_ << " + ";
×
594
            }
595
            os_ << b.offset;
×
596
        }
597
        os_ << "]";
×
598
    }
×
599

600
    void print(Deref const& access) {
184✔
601
        const string sign = access.offset < 0 ? " - " : " + ";
212✔
602
        const int offset = std::abs(access.offset); // what about INT_MIN?
184✔
603
        os_ << "*(" << size(access.width) << " *)";
276✔
604
        os_ << "(" << access.basereg << sign << offset << ")";
184✔
605
    }
184✔
606

607
    void print(Condition const& cond) {
224✔
608
        os_ << cond.left << " " << ((!cond.is64) ? "w" : "") << cond.op << " " << cond.right;
263✔
609
    }
224✔
610

611
    void operator()(Mem const& b) {
184✔
612
        if (b.is_load) {
184✔
613
            os_ << b.value << " = ";
44✔
614
        }
615
        if (b.is_load && b.is_signed) {
184✔
616
            const string sign = b.access.offset < 0 ? " - " : " + ";
×
617
            const int offset = std::abs(b.access.offset);
×
618
            os_ << "*(" << size(b.access.width, true) << " *)";
×
619
            os_ << "(" << b.access.basereg << sign << offset << ")";
×
620
        } else {
×
621
            print(b.access);
184✔
622
        }
623
        if (!b.is_load) {
184✔
624
            os_ << " = " << b.value;
140✔
625
        }
626
    }
184✔
627

628
    void operator()(Atomic const& b) {
×
629
        os_ << "lock ";
×
630
        print(b.access);
×
631
        os_ << " ";
×
632
        bool showfetch = true;
×
633
        switch (b.op) {
×
634
        case Atomic::Op::ADD: os_ << "+"; break;
×
635
        case Atomic::Op::OR: os_ << "|"; break;
×
636
        case Atomic::Op::AND: os_ << "&"; break;
×
637
        case Atomic::Op::XOR: os_ << "^"; break;
×
638
        case Atomic::Op::XCHG:
×
639
            os_ << "x";
×
640
            showfetch = false;
×
641
            break;
×
642
        case Atomic::Op::CMPXCHG:
×
643
            os_ << "cx";
×
644
            showfetch = false;
×
645
            break;
×
646
        }
647
        os_ << "= " << b.valreg;
×
648

649
        if (showfetch && b.fetch) {
×
650
            os_ << " fetch";
×
651
        }
652
    }
×
653

654
    void operator()(Assume const& b) {
184✔
655
        os_ << "assume ";
184✔
656
        print(b.cond);
184✔
657
    }
184✔
658

659
    void operator()(IncrementLoopCounter const& a) { os_ << variable_registry.loop_counter(to_string(a.name)) << "++"; }
×
660
};
661
// ReSharper restore CppMemberFunctionMayBeConst
662

663
std::ostream& operator<<(std::ostream& os, Instruction const& ins) {
190✔
664
    std::visit(CommandPrinterVisitor{os}, ins);
95✔
665
    return os;
190✔
666
}
667

668
static void print_instruction_rich(std::ostream& os, const Program& prog, const Instruction& ins) {
×
669
    CommandPrinterVisitor visitor{.os_ = os, .program_info_ = &prog.info()};
×
670
    std::visit(visitor, ins);
×
671
}
672

673
string to_string(Instruction const& ins) {
186✔
674
    std::stringstream str;
186✔
675
    str << ins;
186✔
676
    return str.str();
372✔
677
}
186✔
678

679
std::ostream& operator<<(std::ostream& os, const Assertion& a) {
1,826✔
680
    std::visit(AssertionPrinterVisitor{os}, a);
942✔
681
    return os;
942✔
682
}
683

684
string to_string(Assertion const& constraint) {
1,766✔
685
    std::stringstream str;
1,766✔
686
    str << constraint;
1,766✔
687
    return str.str();
3,532✔
688
}
1,766✔
689

690
auto get_labels(const InstructionSeq& insts) {
38✔
691
    Pc pc = 0;
38✔
692
    std::map<Label, Pc> pc_of_label;
38✔
693
    for (const auto& [label, inst, _] : insts) {
708✔
694
        pc_of_label[label] = pc;
670✔
695
        pc += size(inst);
670✔
696
    }
697
    return pc_of_label;
38✔
698
}
×
699

700
void print(const InstructionSeq& insts, std::ostream& out, const std::optional<const Label>& label_to_print,
38✔
701
           const bool print_line_info, const ProgramInfo* info) {
702
    const auto pc_of_label = get_labels(insts);
38✔
703
    Pc pc = 0;
38✔
704
    std::string previous_source;
38✔
705
    CommandPrinterVisitor visitor{.os_ = out, .program_info_ = info};
38✔
706
    for (const LabeledInstruction& labeled_inst : insts) {
708✔
707
        const auto& [label, ins, line_info] = labeled_inst;
670✔
708
        if (!label_to_print.has_value() || label == label_to_print) {
670✔
709
            if (line_info.has_value() && print_line_info) {
670✔
710
                auto& [file, source, line, column] = line_info.value();
×
711
                // Only decorate the first instruction associated with a source line.
712
                if (source != previous_source) {
×
713
                    out << line_info.value();
×
714
                    previous_source = source;
×
715
                }
716
            }
717
            if (label.isjump()) {
670✔
718
                out << "\n";
×
719
                out << label << ":\n";
×
720
            }
721
            if (label_to_print.has_value()) {
670✔
722
                out << pc << ": ";
×
723
            } else {
724
                out << std::setw(8) << pc << ":\t";
670✔
725
            }
726
            if (const auto jmp = std::get_if<Jmp>(&ins)) {
670✔
727
                if (!pc_of_label.contains(jmp->target)) {
54✔
728
                    CRAB_ERROR("Cannot find label ", to_string(jmp->target));
×
729
                }
730
                const Pc target_pc = pc_of_label.at(jmp->target);
54✔
731
                visitor(*jmp, gsl::narrow<int>(target_pc) - static_cast<int>(pc) - 1);
54✔
732
            } else {
733
                std::visit(visitor, ins);
616✔
734
            }
735
            out << "\n";
670✔
736
        }
737
        pc += size(ins);
670✔
738
    }
739
}
38✔
740

741
std::ostream& operator<<(std::ostream& o, const EbpfMapDescriptor& desc) {
28✔
742
    return o << "(" << "original_fd = " << desc.original_fd << ", " << "inner_map_fd = " << desc.inner_map_fd << ", "
28✔
743
             << "type = " << desc.type << ", " << "max_entries = " << desc.max_entries << ", "
28✔
744
             << "value_size = " << desc.value_size << ", " << "key_size = " << desc.key_size << ")";
28✔
745
}
746

747
void print_map_descriptors(const std::vector<EbpfMapDescriptor>& descriptors, std::ostream& o) {
38✔
748
    int i = 0;
38✔
749
    for (const auto& desc : descriptors) {
66✔
750
        o << "map " << i << ":" << desc << "\n";
28✔
751
        i++;
28✔
752
    }
753
}
38✔
754

755
std::ostream& operator<<(std::ostream& os, const btf_line_info_t& line_info) {
×
756
    os << "; " << line_info.file_name << ":" << line_info.line_number << "\n";
×
757
    os << "; " << line_info.source_line << "\n";
×
758
    return os;
×
759
}
760

761
void print_invariants_filtered(std::ostream& os, const Program& prog, const AnalysisResult& result,
2✔
762
                               const std::set<Label>& filter, const VerbosityOptions& verbosity,
763
                               const std::map<Label, RelevantState>* relevance) {
764
    DetailedPrinter printer{os, prog, verbosity.print_line_info};
2✔
765
    const bool compact = verbosity.compact_slice;
2✔
766
    const auto basic_blocks = BasicBlock::collect_basic_blocks(prog.cfg(), verbosity.simplify);
2✔
767

768
    // Build a mapping from each label in a basic block to the block's first label.
769
    // Needed to look up post-invariants for mid-block predecessor labels at join points.
770
    std::map<Label, Label> label_to_block_leader;
2✔
771
    for (const BasicBlock& bb : basic_blocks) {
28✔
772
        for (const Label& label : bb) {
52✔
773
            label_to_block_leader.insert({label, bb.first_label()});
52✔
774
        }
775
    }
776

777
    // Helper to look up the post-invariant for a predecessor label.
778
    // Mid-block labels don't have direct invariant entries, so we map
779
    // through the block leader to find the containing block's post-state.
780
    // Note: when simplify=true, the block leader's post represents the
781
    // entire collapsed block, which is correct for the last instruction
782
    // but approximate for mid-block predecessors. Failure slicing defaults
783
    // to simplify=false, so this approximation is rarely triggered.
784
    auto get_parent_post_invariant = [&](const Label& parent) -> const EbpfDomain* {
1✔
785
        const auto leader_it = label_to_block_leader.find(parent);
×
786
        const Label& lookup_label = (leader_it != label_to_block_leader.end()) ? leader_it->second : parent;
×
787
        const auto inv_it = result.invariants.find(lookup_label);
×
788
        if (inv_it != result.invariants.end() && !inv_it->second.post.is_bottom()) {
×
789
            return &inv_it->second.post;
790
        }
791
        return nullptr;
792
    };
2✔
793

794
    for (const BasicBlock& bb : basic_blocks) {
28✔
795
        // Check if any label in this basic block is in the filter
796
        bool bb_has_filtered_label = false;
26✔
797
        for (const Label& label : bb) {
48✔
798
            if (filter.contains(label)) {
26✔
799
                bb_has_filtered_label = true;
2✔
800
                break;
2✔
801
            }
802
        }
803
        if (!bb_has_filtered_label) {
26✔
804
            continue;
22✔
805
        }
806

807
        // Find the first filtered label in this block to use as the block header
808
        Label first_filtered_label = bb.first_label();
4✔
809
        for (const Label& label : bb) {
4✔
810
            if (filter.contains(label)) {
4✔
811
                first_filtered_label = label;
4✔
812
                break;
2✔
813
            }
814
        }
815

816
        // Use bb.first_label() for reachability check: if the block's entry is unreachable,
817
        // skip the entire block. The filtered label's pre-invariant is printed below.
818
        if (result.invariants.at(bb.first_label()).pre.is_bottom()) {
6✔
819
            continue;
×
820
        }
821

822
        // Print pre-invariant for first filtered label in block (unless compact)
823
        if (!compact) {
4✔
824
            const auto* label_relevance =
2✔
825
                relevance ? (relevance->contains(first_filtered_label) ? &relevance->at(first_filtered_label) : nullptr)
4✔
826
                          : nullptr;
4✔
827
            invariant_filter guard(os, label_relevance);
4✔
828
            os << "\nPre-invariant : " << result.invariants.at(first_filtered_label).pre << "\n";
4✔
829
        }
4✔
830

831
        // Print the jump and block header anchored to the basic block entry label
832
        // for correct CFG structure representation.
833
        printer.print_jump("from", bb.first_label());
10✔
834
        os << bb.first_label() << ":\n";
4✔
835

836
        // R3: Show per-predecessor invariants at join points.
837
        // When multiple predecessors exist and at least 2 are in the slice,
838
        // show what each incoming edge contributed to help diagnose lost correlations.
839
        if (!compact && relevance) {
4✔
840
            const auto parents = prog.cfg().parents_of(bb.first_label());
4✔
841
            std::vector<Label> in_slice_parents;
4✔
842
            for (const auto& parent : parents) {
8✔
843
                if (filter.contains(parent)) {
4✔
844
                    in_slice_parents.push_back(parent);
2✔
845
                }
846
            }
847

848
            if (in_slice_parents.size() >= 2) {
4✔
849
                // Union of relevance from first_filtered_label and all in-slice parents.
850
                // The first entry we find seeds the optional (supplying the filter context
851
                // it carries); subsequent entries merge into it.
852
                std::optional<RelevantState> join_relevance;
×
853
                auto try_merge = [&](const Label& lbl) {
×
854
                    if (const auto it = relevance->find(lbl); it != relevance->end()) {
×
855
                        if (join_relevance) {
×
856
                            join_relevance->merge(it->second);
×
857
                        } else {
858
                            join_relevance.emplace(it->second);
×
859
                        }
860
                    }
861
                };
×
862
                try_merge(first_filtered_label);
×
863
                for (const auto& parent : in_slice_parents) {
×
864
                    try_merge(parent);
×
865
                }
866

867
                if (join_relevance) {
×
868
                    os << "  --- join point: per-predecessor state ---\n";
×
869
                    invariant_filter guard(os, &*join_relevance);
×
870
                    for (const auto& parent : in_slice_parents) {
×
871
                        if (const auto* post = get_parent_post_invariant(parent)) {
×
872
                            os << "  from " << parent << ": " << *post << "\n";
×
873
                        }
874
                    }
875
                    os << "  --- end join point ---\n";
×
876
                }
×
877
            }
×
878
        }
4✔
879

880
        if (first_filtered_label != bb.first_label()) {
6✔
881
            // Indicate that some labels/instructions were skipped due to filtering.
882
            os << "  ... skipped ...\n";
×
883
        }
884

885
        Label last_label = bb.first_label();
4✔
886
        Label prev_filtered_label = bb.first_label();
4✔
887
        bool has_prev_filtered = false;
4✔
888
        for (const Label& label : bb) {
8✔
889
            if (!filter.contains(label)) {
4✔
890
                continue;
×
891
            }
892

893
            // If there was a gap since the previous filtered label in this block,
894
            // close the previous label's output and show a skip indicator.
895
            if (has_prev_filtered && prev_filtered_label != label) {
4✔
896
                // Print post-invariant and goto for the previous filtered label
897
                if (!compact) {
×
898
                    const auto& prev_current = result.invariants.at(prev_filtered_label);
×
899
                    if (!prev_current.post.is_bottom()) {
×
900
                        const auto* prev_label_relevance =
901
                            relevance ? (relevance->contains(prev_filtered_label) ? &relevance->at(prev_filtered_label)
×
902
                                                                                  : nullptr)
903
                                      : nullptr;
×
904
                        invariant_filter guard(os, prev_label_relevance);
×
905
                        printer.print_jump("goto", prev_filtered_label);
×
906
                        os << "\nPost-invariant : " << prev_current.post << "\n";
×
907
                    }
×
908
                }
909
                // Check if there are skipped labels between prev and current
910
                bool has_gap = false;
×
911
                for (const Label& mid : bb) {
×
912
                    if (mid <= prev_filtered_label) {
×
913
                        continue;
×
914
                    }
915
                    if (mid >= label) {
×
916
                        break;
917
                    }
918
                    has_gap = true;
×
919
                    break;
×
920
                }
921
                if (has_gap) {
×
922
                    os << "  ... skipped ...\n";
×
923
                }
924
                // Print pre-invariant for this label
925
                if (!compact) {
×
926
                    const auto* label_rel =
927
                        relevance ? (relevance->contains(label) ? &relevance->at(label) : nullptr) : nullptr;
×
928
                    invariant_filter guard(os, label_rel);
×
929
                    os << "\nPre-invariant : " << result.invariants.at(label).pre << "\n";
×
930
                    printer.print_jump("from", label);
×
931
                }
×
932
            }
933

934
            printer.print_line_info(label);
4✔
935

936
            // Print assertions, filtered by relevance if provided
937
            const auto* label_relevance =
2✔
938
                relevance ? (relevance->contains(label) ? &relevance->at(label) : nullptr) : nullptr;
4✔
939
            for (const auto& assertion : prog.assertions_at(label)) {
6✔
940
                // If we have relevance info, only print assertions involving relevant registers.
941
                // Assertions with no register deps (e.g., BoundedLoopCount) are always
942
                // printed to avoid hiding the failing assertion from the slice output.
943
                if (label_relevance) {
2✔
944
                    auto assertion_regs = extract_assertion_registers(assertion);
2✔
945
                    if (!assertion_regs.empty()) {
2✔
946
                        bool is_relevant = false;
2✔
947
                        for (const auto& reg : assertion_regs) {
2✔
948
                            if (label_relevance->registers.contains(reg)) {
2✔
949
                                is_relevant = true;
1✔
950
                                break;
1✔
951
                            }
952
                        }
953
                        if (!is_relevant) {
2✔
954
                            continue; // Skip this assertion
×
955
                        }
956
                    }
957
                }
2✔
958
                os << "  assert " << assertion << ";\n";
2✔
959
            }
4✔
960
            os << "  " << prog.instruction_at(label) << ";\n";
4✔
961

962
            last_label = label;
4✔
963
            prev_filtered_label = label;
4✔
964
            has_prev_filtered = true;
4✔
965

966
            const auto& current = result.invariants.at(label);
4✔
967
            if (current.error) {
4✔
968
                os << "\nVerification error:\n";
2✔
969
                print_error(os, *current.error, prog, verbosity);
2✔
970
                os << "\n";
2✔
971
            }
972
        }
973

974
        // Print post-invariant (unless compact)
975
        if (!compact) {
4✔
976
            const auto& current = result.invariants.at(last_label);
4✔
977
            if (!current.post.is_bottom()) {
4✔
978
                const auto* label_relevance =
1✔
979
                    relevance ? (relevance->contains(last_label) ? &relevance->at(last_label) : nullptr) : nullptr;
2✔
980
                invariant_filter guard(os, label_relevance);
2✔
981
                printer.print_jump("goto", last_label);
2✔
982
                os << "\nPost-invariant : " << current.post << "\n";
2✔
983
            }
2✔
984
        }
985
    }
8✔
986
    os << "\n";
2✔
987
}
2✔
988

989
void print_failure_slices(std::ostream& os, const Program& prog, const AnalysisResult& result,
2✔
990
                          const std::vector<FailureSlice>& slices, const VerbosityOptions& verbosity) {
991
    if (slices.empty()) {
2✔
992
        os << "No verification failures found.\n";
×
993
        return;
×
994
    }
995

996
    for (size_t i = 0; i < slices.size(); ++i) {
4✔
997
        const auto& slice = slices[i];
2✔
998

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

1001
        // Print error summary
1002
        os << "[ERROR] " << slice.error.message << "\n";
2✔
1003
        os << "[LOCATION] " << slice.failing_label << "\n";
2✔
1004

1005
        // Print relevant registers at failure point
1006
        const auto it = slice.relevance.find(slice.failing_label);
2✔
1007
        if (it != slice.relevance.end()) {
2✔
1008
            os << "[RELEVANT REGISTERS] ";
2✔
1009
            bool first = true;
2✔
1010
            for (const auto& reg : it->second.registers) {
4✔
1011
                if (!first) {
2✔
1012
                    os << ", ";
×
1013
                }
1014
                os << "r" << static_cast<int>(reg.v);
2✔
1015
                first = false;
2✔
1016
            }
1017
            if (!it->second.stack_offsets.empty()) {
2✔
1018
                for (const auto& offset : it->second.stack_offsets) {
×
1019
                    if (!first) {
×
1020
                        os << ", ";
×
1021
                    }
1022
                    os << "stack[" << offset << "]";
×
1023
                    first = false;
×
1024
                }
1025
            }
1026
            os << "\n";
2✔
1027
        }
1028

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

1031
        // Print a compact control-flow summary showing the branch-path skeleton
1032
        // through the slice. Lists labels in order with Assume/Jmp annotations.
1033
        // At join points (labels with ≥2 in-slice predecessors), the converging
1034
        // predecessors are grouped as {pred1 | pred2} → join_label.
1035
        {
1✔
1036
            os << "[CONTROL FLOW] ";
2✔
1037
            // Collect and sort impacted labels
1038
            auto labels = slice.impacted_labels();
2✔
1039

1040
            // Build a map: join_label → set of in-slice predecessors
1041
            // Also collect which labels are convergence predecessors (to skip them in linear output)
1042
            std::map<Label, std::vector<Label>> join_predecessors;
2✔
1043
            for (const auto& lbl : labels) {
6✔
1044
                const auto& parents = prog.cfg().parents_of(lbl);
4✔
1045
                std::vector<Label> in_slice_parents;
4✔
1046
                for (const auto& p : parents) {
8✔
1047
                    if (labels.contains(p)) {
4✔
1048
                        in_slice_parents.push_back(p);
2✔
1049
                    }
1050
                }
1051
                if (in_slice_parents.size() >= 2) {
4✔
1052
                    join_predecessors[lbl] = in_slice_parents;
×
1053
                }
1054
            }
4✔
1055
            // Labels consumed by a {..|..} group are skipped in linear output,
1056
            // unless they are themselves join points (nested joins).
1057
            std::set<Label> convergence_members;
2✔
1058
            for (const auto& preds : join_predecessors | std::views::values) {
2✔
1059
                for (const auto& p : preds) {
×
1060
                    if (!join_predecessors.contains(p)) {
×
1061
                        convergence_members.insert(p);
×
1062
                    }
1063
                }
1064
            }
1065

1066
            // Helper to annotate a label with its instruction type
1067
            auto annotate_label = [&](const Label& lbl) {
5✔
1068
                os << lbl;
4✔
1069
                const auto& ins = prog.instruction_at(lbl);
4✔
1070
                if (const auto* assume = std::get_if<Assume>(&ins)) {
4✔
1071
                    os << " (assume " << assume->cond.left << " " << assume->cond.op << " " << assume->cond.right
×
1072
                       << ")";
×
1073
                } else if (const auto* jmp = std::get_if<Jmp>(&ins)) {
4✔
1074
                    if (jmp->cond) {
×
1075
                        os << " (if " << jmp->cond->left << " " << jmp->cond->op << " " << jmp->cond->right << ")";
×
1076
                    }
1077
                }
1078
            };
6✔
1079

1080
            bool first_cf = true;
2✔
1081
            for (const auto& lbl : labels) {
6✔
1082
                // Skip labels that are part of a convergence group (printed with their join)
1083
                if (convergence_members.contains(lbl)) {
4✔
1084
                    continue;
×
1085
                }
1086

1087
                if (!first_cf) {
4✔
1088
                    os << ", ";
2✔
1089
                }
1090
                first_cf = false;
4✔
1091

1092
                // If this label is a join point, print {pred1 | pred2} → lbl
1093
                if (join_predecessors.contains(lbl)) {
4✔
1094
                    os << "{";
×
1095
                    bool first_pred = true;
×
1096
                    for (const auto& pred : join_predecessors.at(lbl)) {
×
1097
                        if (!first_pred) {
×
1098
                            os << " | ";
×
1099
                        }
1100
                        first_pred = false;
×
1101
                        annotate_label(pred);
×
1102
                    }
1103
                    os << "} -> ";
×
1104
                }
1105

1106
                annotate_label(lbl);
4✔
1107
            }
1108
            if (labels.contains(slice.failing_label)) {
2✔
1109
                os << " FAIL";
2✔
1110
            }
1111
            os << "\n\n";
2✔
1112
        }
2✔
1113

1114
        // Print the filtered CFG with assertion filtering based on relevance
1115
        os << "[CAUSAL TRACE]\n";
2✔
1116
        print_invariants_filtered(os, prog, result, slice.impacted_labels(), verbosity, &slice.relevance);
2✔
1117

1118
        if (i + 1 < slices.size()) {
2✔
1119
            os << "\n";
×
1120
        }
1121
    }
1122
}
1123

1124
} // 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