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

Alan-Jowett / ebpf-verifier / 22075299923

16 Feb 2026 04:55PM UTC coverage: 87.121% (+0.8%) from 86.313%
22075299923

push

github

web-flow
Replace Number backend: boost's cpp_int → __int128 (#1007)

Backport the i128 representation from the Rust port. Number is an
intermediate representation for eBPF verification where all inputs
and outputs are ≤64-bit; i128 provides headroom for widening
arithmetic without the overhead of arbitrary-precision integers.

This commit has been verified to produce the same invariants as its parent on the test data.

Key changes to src/arith/num_big.hpp:
- Use __int128 on GCC/Clang, boost::multiprecision::int128_t on MSVC
- All arithmetic operators use checked helpers (__builtin_*_overflow
  on GCC/Clang, manual pre-checks on MSVC) to prevent signed overflow UB
- Simplify sign_extend/zero_extend: direct bit arithmetic replaces
  template dispatch over fixed widths
- fill_ones computed in unsigned space to avoid shift UB
- Custom decimal parser accumulates in UInt128 to handle kInt128Min
- Width assertions tightened to eBPF domain bounds (≤64/65)
 Consequential simplifications:
- Weight = Number (was already the case), so convert_NtoW_overflow
  was a passthrough returning false — inlined at all ~18 call sites
  in zone_domain.cpp, removing dead overflow branches
- eval_expression_overflow → eval_expression, returns Weight directly
- Remove dead SafeI64 overload and num_safeint.hpp include
- Custom int128_to_string in printing.cpp (no std::to_string for __int128)
- Add missing transitive includes (<algorithm>, <cassert>) previously
  provided by boost headers

Also: remove unused stats infrastructure, rename_classes.py, and
miscellaneous cleanup (docs ASCII arrows, splitdbm simplifications).

Signed-off-by: Elazar Gershuni <elazarg@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

139 of 166 new or added lines in 5 files covered. (83.73%)

52 existing lines in 4 files now uncovered.

9389 of 10777 relevant lines covered (87.12%)

3125310.38 hits per line

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

91.67
/src/crab/zone_domain.cpp
1
// Copyright (c) Prevail Verifier contributors.
2
// SPDX-License-Identifier: Apache-2.0
3
#include <cassert>
4
#include <utility>
5

6
#include "crab/var_registry.hpp"
7
#include "crab/zone_domain.hpp"
8
#include "string_constraints.hpp"
9
#include "type_encoding.hpp"
10

11
using namespace splitdbm;
12

13
namespace prevail {
14

15
ZoneDomain::ZoneDomain() : core_(std::make_unique<SplitDBM>()) { rev_map_.emplace_back(std::nullopt); }
3,568,446✔
16

17
ZoneDomain::~ZoneDomain() = default;
9,809,534✔
18

19
ZoneDomain::ZoneDomain(const ZoneDomain& o)
2,707,324✔
20
    : core_(std::make_unique<SplitDBM>(*o.core_)), vert_map_(o.vert_map_), rev_map_(o.rev_map_) {}
5,414,648✔
21

22
ZoneDomain::ZoneDomain(ZoneDomain&& o) noexcept = default;
6,935,400✔
23

24
ZoneDomain& ZoneDomain::operator=(const ZoneDomain& o) {
1,129,054✔
25
    if (this != &o) {
1,129,054✔
26
        core_ = std::make_unique<SplitDBM>(*o.core_);
1,129,054✔
27
        vert_map_ = o.vert_map_;
1,129,054✔
28
        rev_map_ = o.rev_map_;
1,129,054✔
29
    }
30
    return *this;
1,129,054✔
31
}
32

33
ZoneDomain& ZoneDomain::operator=(ZoneDomain&& o) noexcept = default;
198,992✔
34

35
ZoneDomain::ZoneDomain(VertMap&& vert_map, RevMap&& rev_map, std::unique_ptr<SplitDBM> core)
99,646✔
36
    : core_(std::move(core)), vert_map_(std::move(vert_map)), rev_map_(std::move(rev_map)) {
149,469✔
37
    normalize();
149,469✔
38
}
99,646✔
39

40
void ZoneDomain::set_to_top() {
×
NEW
41
    core_ = std::make_unique<SplitDBM>();
×
42
    vert_map_.clear();
×
43
    rev_map_.clear();
×
44
    rev_map_.emplace_back(std::nullopt);
×
45
}
×
46

47
bool ZoneDomain::is_top() const { return core_->is_top(); }
131,996✔
48

49
std::pair<std::size_t, std::size_t> ZoneDomain::size() const { return {core_->graph_size(), core_->num_edges()}; }
×
50

51
std::optional<VertId> ZoneDomain::get_vertid(const Variable x) const {
64,266,540✔
52
    const auto it = vert_map_.find(x);
64,266,540✔
53
    if (it == vert_map_.end()) {
96,401,379✔
54
        return {};
29,095,982✔
55
    }
56
    return it->second;
35,170,558✔
57
}
58

59
Bound ZoneDomain::get_lb(const std::optional<VertId>& v) const {
32,642,651✔
60
    return v ? core_->get_bound(*v, Side::LEFT) : MINUS_INFINITY;
32,642,651✔
61
}
62

63
Bound ZoneDomain::get_ub(const std::optional<VertId>& v) const {
31,623,889✔
64
    return v ? core_->get_bound(*v, Side::RIGHT) : PLUS_INFINITY;
31,623,889✔
65
}
66

67
Bound ZoneDomain::get_lb(const Variable x) const { return get_lb(get_vertid(x)); }
32,642,651✔
68

69
Bound ZoneDomain::get_ub(const Variable x) const {
31,862,603✔
70
    if (variable_registry->is_min_only(x)) {
31,862,603✔
71
        return PLUS_INFINITY;
238,714✔
72
    }
73
    return get_ub(get_vertid(x));
31,623,889✔
74
}
75

76
Interval ZoneDomain::get_interval(const Variable x) const { return {get_lb(x), get_ub(x)}; }
31,682,709✔
77

78
static std::optional<VertId> try_at(const ZoneDomain::VertMap& map, const Variable v) {
7,573,102✔
79
    const auto it = map.find(v);
3,786,547✔
80
    if (it == map.end()) {
7,573,102✔
81
        return std::nullopt;
2,826,342✔
82
    }
83
    return it->second;
4,746,760✔
84
}
85

86
VertId ZoneDomain::get_vert(Variable v) {
2,334,934✔
87
    if (const auto y = try_at(vert_map_, v)) {
2,334,934✔
88
        return *y;
1,271,190✔
89
    }
90

91
    VertId vert = core_->new_vertex();
1,063,744✔
92
    vert_map_.emplace(v, vert);
1,063,744✔
93
    // Initialize rev_map
94
    assert(vert <= rev_map_.size());
1,063,744✔
95
    if (vert < rev_map_.size()) {
1,063,744✔
96
        rev_map_[vert] = v;
456,138✔
97
    } else {
98
        rev_map_.push_back(v);
607,606✔
99
    }
100

101
    assert(vert != 0);
1,063,744✔
102

103
    return vert;
1,063,744✔
104
}
105

106
void ZoneDomain::diffcsts_of_assign(const LinearExpression& exp, std::vector<std::pair<Variable, Weight>>& lb,
176,756✔
107
                                    std::vector<std::pair<Variable, Weight>>& ub) const {
108
    diffcsts_of_assign(exp, true, ub);
176,756✔
109
    diffcsts_of_assign(exp, false, lb);
176,756✔
110
}
176,756✔
111

112
void ZoneDomain::diffcsts_of_assign(const LinearExpression& exp,
353,512✔
113
                                    /* if true then process the upper
114
                                       bounds, else the lower bounds */
115
                                    bool extract_upper_bounds,
116
                                    /* foreach {v, k} \in diff_csts we have
117
                                       the difference constraint v - k <= k */
118
                                    std::vector<std::pair<Variable, Weight>>& diff_csts) const {
119

120
    std::optional<Variable> unbounded_var;
353,512✔
121
    std::vector<std::pair<Variable, Weight>> terms;
353,512✔
122

123
    Weight residual = exp.constant_term();
353,512✔
124

125
    for (const auto& [y, n] : exp.variable_terms()) {
712,212✔
126
        const Weight coeff = n;
359,788✔
127

128
        if (coeff < Weight(0)) {
359,788✔
129
            // Can't do anything with negative coefficients.
130
            auto y_val = extract_upper_bounds ? get_lb(y) : get_ub(y);
1,424✔
131

132
            if (y_val.is_infinite()) {
1,424✔
133
                return;
168✔
134
            }
135
            const Weight ymax = *y_val.number();
1,256✔
136
            residual += ymax * coeff;
1,256✔
137
        } else {
138
            auto y_val = extract_upper_bounds ? get_ub(y) : get_lb(y);
358,364✔
139

140
            if (y_val.is_infinite()) {
358,364✔
141
                if (unbounded_var || coeff != Weight(1)) {
142,274✔
142
                    return;
920✔
143
                }
144
                unbounded_var = y;
141,354✔
145
            } else {
146
                const Weight ymax = *y_val.number();
216,090✔
147
                residual += ymax * coeff;
216,090✔
148
                terms.emplace_back(y, ymax);
216,090✔
149
            }
150
        }
151
    }
152

153
    if (unbounded_var) {
352,424✔
154
        // There is exactly one unbounded variable with unit
155
        // coefficient
156
        diff_csts.emplace_back(*unbounded_var, residual);
140,374✔
157
    } else {
158
        for (const auto& [v, n] : terms) {
427,398✔
159
            diff_csts.emplace_back(v, residual - n);
215,348✔
160
        }
161
    }
162
}
353,512✔
163

164
void ZoneDomain::diffcsts_of_lin_leq(const LinearExpression& exp,
1,541,052✔
165
                                     /* difference contraints */
166
                                     std::vector<diffcst_t>& csts,
167
                                     /* x >= lb for each {x,lb} in lbs */
168
                                     std::vector<std::pair<Variable, Weight>>& lbs,
169
                                     /* x <= ub for each {x,ub} in ubs */
170
                                     std::vector<std::pair<Variable, Weight>>& ubs) const {
171
    Weight exp_ub = -Weight{exp.constant_term()};
1,541,052✔
172

173
    Weight unbounded_lbcoeff;
1,541,052✔
174
    Weight unbounded_ubcoeff;
1,541,052✔
175
    std::optional<Variable> unbounded_lbvar;
1,541,052✔
176
    std::optional<Variable> unbounded_ubvar;
1,541,052✔
177

178
    std::vector<std::pair<std::pair<Weight, Variable>, Weight>> pos_terms, neg_terms;
1,541,052✔
179
    for (const auto& [y, n] : exp.variable_terms()) {
3,146,010✔
180
        const Weight coeff = n;
1,608,828✔
181
        if (coeff > Weight(0)) {
1,608,828✔
182
            auto y_lb = get_lb(y);
780,048✔
183
            if (y_lb.is_infinite()) {
780,048✔
184
                if (unbounded_lbvar) {
634,000✔
185
                    return;
×
186
                }
187
                unbounded_lbvar = y;
634,000✔
188
                unbounded_lbcoeff = coeff;
634,000✔
189
            } else {
190
                const Weight ymin = *y_lb.number();
146,048✔
191
                exp_ub -= ymin * coeff;
146,048✔
192
                pos_terms.push_back({{coeff, y}, ymin});
146,048✔
193
            }
194
        } else {
195
            auto y_ub = get_interval(y).ub();
828,780✔
196
            if (y_ub.is_infinite()) {
828,780✔
197
                if (unbounded_ubvar) {
62,100✔
198
                    return;
3,870✔
199
                }
200
                unbounded_ubvar = y;
58,230✔
201
                unbounded_ubcoeff = -coeff;
441,568✔
202
            } else {
203
                const Weight ymax = *y_ub.number();
766,680✔
204
                exp_ub -= ymax * coeff;
766,680✔
205
                neg_terms.push_back({{-coeff, y}, ymax});
766,680✔
206
            }
207
        }
208
    }
209

210
    if (unbounded_lbvar) {
1,537,182✔
211
        Variable x{*unbounded_lbvar};
634,000✔
212
        if (unbounded_ubvar) {
634,000✔
213
            if (unbounded_lbcoeff == Weight(1) && unbounded_ubcoeff == Weight(1)) {
1,300✔
214
                csts.push_back({{x, *unbounded_ubvar}, exp_ub});
1,300✔
215
            }
216
        } else {
217
            if (unbounded_lbcoeff == Weight(1)) {
632,700✔
218
                for (const auto& [nv, k] : neg_terms) {
633,424✔
219
                    csts.push_back({{x, nv.second}, exp_ub - k});
724✔
220
                }
221
            }
222
            // Add bounds for x
223
            ubs.emplace_back(x, exp_ub / unbounded_lbcoeff);
632,700✔
224
        }
225
    } else {
226
        if (unbounded_ubvar) {
903,182✔
227
            Variable y{*unbounded_ubvar};
53,060✔
228
            if (unbounded_ubcoeff == Weight(1)) {
53,060✔
229
                for (const auto& [nv, k] : pos_terms) {
55,398✔
230
                    csts.push_back({{nv.second, y}, exp_ub + k});
2,338✔
231
                }
232
            }
233
            // Add bounds for y
234
            lbs.emplace_back(y, -exp_ub / unbounded_ubcoeff);
53,060✔
235
        } else {
236
            for (const auto& [neg_nv, neg_k] : neg_terms) {
1,589,680✔
237
                for (const auto& [pos_nv, pos_k] : pos_terms) {
772,702✔
238
                    csts.push_back({{pos_nv.second, neg_nv.second}, exp_ub - neg_k + pos_k});
33,144✔
239
                }
240
            }
241
            for (const auto& [neg_nv, neg_k] : neg_terms) {
1,589,680✔
242
                lbs.emplace_back(neg_nv.second, -exp_ub / neg_nv.first + neg_k);
739,558✔
243
            }
244
            for (const auto& [pos_nv, pos_k] : pos_terms) {
993,832✔
245
                ubs.emplace_back(pos_nv.second, exp_ub / pos_nv.first + pos_k);
143,710✔
246
            }
247
        }
248
    }
249
}
1,542,987✔
250

251
bool ZoneDomain::add_linear_leq(const LinearExpression& exp) {
1,541,052✔
252
    std::vector<std::pair<Variable, Weight>> lbs, ubs;
1,541,052✔
253
    std::vector<diffcst_t> csts;
1,541,052✔
254
    diffcsts_of_lin_leq(exp, csts, lbs, ubs);
1,541,052✔
255

256
    // Apply lower bounds
257
    for (const auto& [var, n] : lbs) {
2,329,158✔
258
        const VertId vert = get_vert(var);
792,618✔
259
        if (!core_->update_bound_if_tighter(vert, Side::LEFT, n)) {
792,618✔
260
            return false;
25,884✔
261
        }
262
    }
263

264
    // Apply upper bounds
265
    for (const auto& [var, n] : ubs) {
2,287,266✔
266
        if (variable_registry->is_min_only(var)) {
775,998✔
267
            continue;
14,096✔
268
        }
269
        const VertId vert = get_vert(var);
761,902✔
270
        if (!core_->update_bound_if_tighter(vert, Side::RIGHT, n)) {
761,902✔
271
            return false;
36,266✔
272
        }
273
    }
274

275
    // Apply difference constraints
276
    for (const auto& [diff, k] : csts) {
1,529,578✔
277
        const VertId src = get_vert(diff.second);
35,786✔
278
        const VertId dest = get_vert(diff.first);
35,786✔
279
        if (!core_->add_difference_constraint(src, dest, k)) {
35,786✔
280
            return false;
32,366✔
281
        }
282
    }
283

284
    core_->close_after_bound_updates();
1,493,792✔
285
    normalize();
2,264,316✔
286
    return true;
746,896✔
287
}
1,541,052✔
288

289
static Interval trim_interval(const Interval& i, const Number& n) {
6,782✔
290
    if (i.lb() == n) {
6,782✔
291
        return Interval{n + 1, i.ub()};
4,356✔
292
    }
293
    if (i.ub() == n) {
2,426✔
294
        return Interval{i.lb(), n - 1};
4✔
295
    }
296
    if (i.is_top() && n == 0) {
2,422✔
297
        return Interval{1, std::numeric_limits<uint64_t>::max()};
×
298
    }
299
    return i;
2,422✔
300
}
301

302
bool ZoneDomain::add_univar_disequation(Variable x, const Number& n) {
6,782✔
303
    const Interval i = get_interval(x);
6,782✔
304
    const Interval new_i = trim_interval(i, n);
6,782✔
305
    if (new_i.is_bottom()) {
6,782✔
306
        return false;
2,163✔
307
    }
308
    if (new_i.is_top() || !(new_i <= i)) {
2,456✔
309
        return true;
61✔
310
    }
311

312
    const VertId v = get_vert(x);
2,334✔
313
    if (new_i.lb().is_finite()) {
2,334✔
314
        if (!core_->strengthen_bound(v, Side::LEFT, Weight{*new_i.lb().number()})) {
2,334✔
315
            return false;
316
        }
317
    }
318
    if (new_i.ub().is_finite() && !variable_registry->is_min_only(x)) {
2,334✔
319
        if (!core_->strengthen_bound(v, Side::RIGHT, Weight{*new_i.ub().number()})) {
2,334✔
320
            return false;
321
        }
322
    }
323
    normalize();
5,725✔
324
    return true;
1,167✔
325
}
326

327
bool ZoneDomain::operator<=(const ZoneDomain& o) const {
1,064✔
328
    // cover all trivial cases to avoid allocating a dbm matrix
329
    if (o.is_top()) {
1,064✔
330
        return true;
68✔
331
    }
332
    if (is_top()) {
928✔
333
        return false;
4✔
334
    }
335

336
    if (vert_map_.size() < o.vert_map_.size()) {
1,840✔
337
        return false;
26✔
338
    }
339

340
    // Build permutation mapping from o's vertices to this's vertices
341
    constexpr VertId INVALID_VERT = std::numeric_limits<VertId>::max();
868✔
342
    std::vector perm(o.core_->graph_size(), INVALID_VERT);
868✔
343
    perm[0] = 0;
868✔
344
    for (const auto& [v, n] : o.vert_map_) {
4,067✔
345
        if (!o.core_->vertex_has_edges(n)) {
1,554✔
346
            continue;
×
347
        }
348

349
        // We can't have this <= o if we're missing some vertex.
350
        if (auto y = try_at(vert_map_, v)) {
1,554✔
351
            perm[n] = *y;
1,554✔
352
        } else {
353
            return false;
×
354
        }
355
    }
356

357
    return SplitDBM::is_subsumed_by(*core_, *o.core_, perm);
868✔
358
}
868✔
359

360
using RevMap = std::vector<std::optional<Variable>>;
361
using VertMap = boost::container::flat_map<Variable, VertId>;
362

363
[[nodiscard]]
364
std::pair<VertMap, RevMap> result_mappings(const RevMap& aligned_vars) {
99,646✔
365
    VertMap vmap;
49,823✔
366
    RevMap revmap = aligned_vars;
99,646✔
367

368
    for (size_t i = 1; i < aligned_vars.size(); ++i) {
2,962,008✔
369
        if (aligned_vars[i]) {
2,862,362✔
370
            vmap.emplace(*aligned_vars[i], gsl::narrow<VertId>(i));
5,724,724✔
371
        }
372
    }
373
    return {std::move(vmap), std::move(revmap)};
149,469✔
374
}
99,646✔
375

376
// Build alignment from intersection of variables (for join/widen)
377
std::tuple<AlignedPair, RevMap> ZoneDomain::make_intersection_alignment(const ZoneDomain& left,
99,592✔
378
                                                                        const ZoneDomain& right) {
379
    std::vector<VertId> perm_left = {0};
149,388✔
380
    std::vector<VertId> perm_right = {0};
149,388✔
381
    RevMap aligned_vars = {std::nullopt};
149,388✔
382

383
    for (const auto& [var, left_vert] : left.vert_map_) {
4,555,790✔
384
        if (auto right_vert = try_at(right.vert_map_, var)) {
2,904,404✔
385
            perm_left.push_back(left_vert);
2,859,476✔
386
            perm_right.push_back(*right_vert);
2,859,476✔
387
            aligned_vars.push_back(var);
2,859,476✔
388
        }
389
    }
390

391
    return std::make_tuple(AlignedPair{*left.core_, *right.core_, std::move(perm_left), std::move(perm_right)},
149,388✔
392
                           std::move(aligned_vars));
248,980✔
393
}
99,592✔
394

395
// Build alignment from union of variables (for meet)
396
std::tuple<AlignedPair, RevMap> ZoneDomain::make_union_alignment(const ZoneDomain& left, const ZoneDomain& right) {
54✔
397
    constexpr VertId NOT_PRESENT = static_cast<VertId>(-1);
54✔
398

399
    std::vector<VertId> perm_left = {0};
81✔
400
    std::vector<VertId> perm_right = {0};
81✔
401
    RevMap aligned_vars = {std::nullopt};
81✔
402
    std::vector initial_potentials = {Weight(0)};
108✔
403

404
    boost::container::flat_map<Variable, size_t> var_to_index;
27✔
405
    for (const auto& [var, left_vert] : left.vert_map_) {
384✔
406
        var_to_index.emplace(var, perm_left.size());
184✔
407
        perm_left.push_back(left_vert);
184✔
408
        perm_right.push_back(NOT_PRESENT);
184✔
409
        aligned_vars.push_back(var);
184✔
410
        initial_potentials.push_back(left.core_->potential_at(left_vert) - left.core_->potential_at_zero());
276✔
411
    }
412

413
    for (const auto& [var, right_vert] : right.vert_map_) {
4,401✔
414
        if (const auto it = var_to_index.find(var); it != var_to_index.end()) {
2,862✔
415
            perm_right[it->second] = right_vert;
240✔
416
        } else {
417
            perm_left.push_back(NOT_PRESENT);
2,702✔
418
            perm_right.push_back(right_vert);
2,702✔
419
            aligned_vars.push_back(var);
2,702✔
420
            initial_potentials.push_back(right.core_->potential_at(right_vert) - right.core_->potential_at_zero());
4,053✔
421
        }
422
    }
423

424
    return std::make_tuple(AlignedPair{*left.core_, *right.core_, std::move(perm_left), std::move(perm_right),
135✔
425
                                       std::move(initial_potentials)},
27✔
426
                           std::move(aligned_vars));
135✔
427
}
54✔
428

429
ZoneDomain do_join(const ZoneDomain& left, const ZoneDomain& right) {
99,496✔
430
    if (right.is_top()) {
99,496✔
431
        return right;
36✔
432
    }
433
    if (left.is_top()) {
99,460✔
434
        return left;
72✔
435
    }
436

437
    // 1. Build alignment (intersection of variables)
438
    auto [aligned_pair, aligned_vars] = ZoneDomain::make_intersection_alignment(left, right);
99,388✔
439

440
    // 2. Execute graph join
441
    SplitDBM joined = SplitDBM::join(aligned_pair);
99,388✔
442

443
    // 3. Build result mappings and garbage collect
444
    auto [out_vmap, out_revmap] = result_mappings(std::move(aligned_vars));
99,388✔
445
    for (const VertId v : joined.get_disconnected_vertices()) {
138,946✔
446
        joined.forget(v);
39,558✔
447
        if (out_revmap[v]) {
39,558✔
448
            out_vmap.erase(*out_revmap[v]);
39,558✔
449
            out_revmap[v] = std::nullopt;
59,337✔
450
        }
451
    }
99,388✔
452

453
    return ZoneDomain(std::move(out_vmap), std::move(out_revmap), std::make_unique<SplitDBM>(std::move(joined)));
99,388✔
454
}
149,082✔
455

456
void ZoneDomain::operator|=(const ZoneDomain& right) { *this = do_join(*this, right); }
×
457

458
ZoneDomain ZoneDomain::operator|(const ZoneDomain& right) const { return do_join(*this, right); }
99,496✔
459

460
ZoneDomain ZoneDomain::widen(const ZoneDomain& o) const {
204✔
461
    // 1. Build alignment (intersection of variables)
462
    auto [aligned_pair, aligned_vars] = make_intersection_alignment(*this, o);
204✔
463

464
    // 2. Execute graph widen
465
    auto result_core = std::make_unique<SplitDBM>(SplitDBM::widen(aligned_pair));
204✔
466

467
    // 3. Build result mappings
468
    auto [out_vmap, out_revmap] = result_mappings(aligned_vars);
204✔
469

470
    return ZoneDomain(std::move(out_vmap), std::move(out_revmap), std::move(result_core));
306✔
471
}
306✔
472

473
std::optional<ZoneDomain> ZoneDomain::meet(const ZoneDomain& o) const {
104✔
474
    if (is_top()) {
104✔
475
        return o;
12✔
476
    }
477
    if (o.is_top()) {
92✔
478
        return *this;
38✔
479
    }
480

481
    // 1. Build alignment (union of variables, with initial potentials)
482
    auto [aligned_pair, aligned_vars] = make_union_alignment(*this, o);
54✔
483

484
    // 2. Execute graph meet
485
    auto meet_result = SplitDBM::meet(aligned_pair);
54✔
486
    if (!meet_result) {
54✔
487
        return std::nullopt; // Infeasible
×
488
    }
489

490
    // 3. Build result mappings
491
    auto [out_vmap, out_revmap] = result_mappings(aligned_vars);
54✔
492

493
    return ZoneDomain(std::move(out_vmap), std::move(out_revmap), std::make_unique<SplitDBM>(std::move(*meet_result)));
81✔
494
}
108✔
495

496
void ZoneDomain::havoc(const Variable v) {
2,153,360✔
497
    if (const auto y = try_at(vert_map_, v)) {
2,153,360✔
498
        core_->forget(*y);
485,012✔
499
        rev_map_[*y] = std::nullopt;
485,012✔
500
        vert_map_.erase(v);
485,012✔
501
        normalize();
1,561,692✔
502
    }
503
}
2,153,360✔
504

505
// return false if it becomes bottom
506
bool ZoneDomain::add_constraint(const LinearConstraint& cst) {
885,104✔
507
    if (cst.is_tautology()) {
885,104✔
508
        return true;
89✔
509
    }
510

511
    // g.check_adjs();
512

513
    if (cst.is_contradiction()) {
884,926✔
514
        return false;
515✔
515
    }
516

517
    switch (cst.kind()) {
883,896✔
518
    case ConstraintKind::LESS_THAN_OR_EQUALS_ZERO: {
149,858✔
519
        if (!add_linear_leq(cst.expression())) {
149,858✔
520
            return false;
13,990✔
521
        }
522
        break;
60,939✔
523
    }
524
    case ConstraintKind::LESS_THAN_ZERO: {
62,240✔
525
        // We try to convert a strict to non-strict.
526
        // e < 0 --> e <= -1
527
        const auto nc = LinearConstraint(cst.expression().plus(1), ConstraintKind::LESS_THAN_OR_EQUALS_ZERO);
93,358✔
528
        if (!add_linear_leq(nc.expression())) {
62,240✔
529
            return false;
19,188✔
530
        }
531
        break;
43,052✔
532
    }
62,240✔
533
    case ConstraintKind::EQUALS_ZERO: {
664,496✔
534
        const LinearExpression& exp = cst.expression();
664,496✔
535
        if (!add_linear_leq(exp) || !add_linear_leq(exp.negate())) {
1,328,954✔
536
            return false;
46✔
537
        }
538
        break;
332,202✔
539
    }
540
    case ConstraintKind::NOT_ZERO: {
7,302✔
541
        // XXX: similar precision as the interval domain
542
        const LinearExpression& e = cst.expression();
7,302✔
543
        for (const auto& [variable, coefficient] : e.variable_terms()) {
11,214✔
544
            Interval i = compute_residual(e, variable) / Interval(coefficient);
8,238✔
545
            if (auto k = i.singleton()) {
8,238✔
546
                if (!add_univar_disequation(variable, *k)) {
6,782✔
547
                    return false;
4,326✔
548
                }
549
            }
550
        }
551
    } break;
1,488✔
552
    }
553
    return true;
416,155✔
554
}
555

556
void ZoneDomain::assign(Variable lhs, const LinearExpression& e) {
374,710✔
557
    const Interval value_interval = eval_interval(e);
374,710✔
558

559
    std::optional<Weight> lb_w, ub_w;
374,710✔
560
    if (value_interval.lb().is_finite()) {
374,710✔
561
        lb_w = Weight{-*value_interval.lb().number()};
458,715✔
562
    }
563
    if (value_interval.ub().is_finite()) {
374,710✔
564
        ub_w = Weight{*value_interval.ub().number()};
302,148✔
565
    }
566

567
    // JN: it seems that we can only do this if
568
    // close_bounds_inline is disabled (which in eBPF is always the case).
569
    // Otherwise, the meet operator misses some non-redundant edges.
570
    if (value_interval.is_singleton()) {
374,710✔
571
        set(lhs, value_interval);
197,954✔
572
        normalize();
197,954✔
573
        return;
297,475✔
574
    }
575

576
    std::vector<std::pair<Variable, Weight>> diffs_lb, diffs_ub;
176,756✔
577
    // Construct difference constraints from the assignment
578
    diffcsts_of_assign(e, diffs_lb, diffs_ub);
176,756✔
579
    if (diffs_lb.empty() && diffs_ub.empty()) {
176,756✔
580
        set(lhs, value_interval);
544✔
581
        normalize();
816✔
582
        return;
544✔
583
    }
584

585
    const Weight e_val = eval_expression(e);
176,212✔
586

587
    std::vector<std::pair<VertId, Weight>> diffs_from, diffs_to;
176,212✔
588
    for (const auto& [var, n] : diffs_lb) {
354,074✔
589
        diffs_from.emplace_back(get_vert(var), -n);
177,862✔
590
    }
591
    for (const auto& [var, n] : diffs_ub) {
354,072✔
592
        diffs_to.emplace_back(get_vert(var), n);
177,860✔
593
    }
594

595
    VertId vert = core_->assign_vertex(core_->potential_at_zero() + e_val, diffs_from, diffs_to, lb_w,
440,530✔
596
                                       (ub_w && !variable_registry->is_min_only(lhs)) ? ub_w : std::nullopt);
176,212✔
597

598
    assert(vert <= rev_map_.size());
176,212✔
599
    if (vert == rev_map_.size()) {
176,212✔
600
        rev_map_.push_back(lhs);
64,958✔
601
    } else {
602
        rev_map_[vert] = lhs;
111,254✔
603
    }
604
    // Clear the old x vertex
605
    havoc(lhs);
176,212✔
606
    vert_map_.emplace(lhs, vert);
176,212✔
607

608
    normalize();
264,318✔
609
}
177,028✔
610

611
ZoneDomain ZoneDomain::narrow(const ZoneDomain& o) const {
×
612
    if (is_top()) {
×
613
        return o;
×
614
    }
615
    // FIXME: Implement properly
616
    // Narrowing as a no-op should be sound.
617
    return {*this};
×
618
}
619

620
void ZoneDomain::clear_thread_local_state() { SplitDBM::clear_thread_local_state(); }
1,430✔
621

622
void ZoneDomain::normalize() { core_->normalize(); }
2,630,887✔
623

624
void ZoneDomain::set(const Variable x, const Interval& intv) {
354,254✔
625
    assert(!intv.is_bottom());
354,254✔
626

627
    havoc(x);
354,254✔
628

629
    if (intv.is_top()) {
354,254✔
630
        return;
1,734✔
631
    }
632

633
    const VertId v = get_vert(x);
350,786✔
634
    if (intv.ub().is_finite() && !variable_registry->is_min_only(x)) {
350,786✔
635
        core_->set_bound(v, Side::RIGHT, Weight{*intv.ub().number()});
502,896✔
636
    }
637
    if (intv.lb().is_finite()) {
350,786✔
638
        core_->set_bound(v, Side::LEFT, Weight{*intv.lb().number()});
350,630✔
639
    }
640
    normalize();
350,786✔
641
}
642

643
void ZoneDomain::apply(const ArithBinOp op, const Variable x, const Variable y, const Variable z) {
×
644
    switch (op) {
×
645
    case ArithBinOp::ADD: assign(x, LinearExpression(y).plus(z)); break;
×
646
    case ArithBinOp::SUB: assign(x, LinearExpression(y).subtract(z)); break;
×
647
    // For the rest of operations, we fall back on intervals.
648
    case ArithBinOp::MUL: set(x, get_interval(y) * get_interval(z)); break;
×
649
    default: CRAB_ERROR("DBM: unreachable");
×
650
    }
651
    normalize();
×
652
}
×
653

654
void ZoneDomain::apply(const ArithBinOp op, const Variable x, const Variable y, const Number& k) {
×
655
    switch (op) {
×
656
    case ArithBinOp::ADD: assign(x, LinearExpression(y).plus(k)); break;
×
657
    case ArithBinOp::SUB: assign(x, LinearExpression(y).subtract(k)); break;
×
658
    case ArithBinOp::MUL: assign(x, LinearExpression(k, y)); break;
×
659
    }
660
    normalize();
×
661
}
×
662

663
void ZoneDomain::forget(const VariableVector& variables) {
×
664
    if (is_top()) {
×
665
        return;
666
    }
667

668
    for (const auto v : variables) {
×
669
        if (vert_map_.contains(v)) {
×
670
            havoc(v);
×
671
        }
672
    }
673
    normalize();
×
674
}
675

676
static std::string to_string(const Variable vd, const Variable vs, const Weight& w, const bool eq) {
224✔
677
    std::stringstream elem;
224✔
678
    if (eq) {
224✔
679
        if (w.operator>(0)) {
14✔
680
            elem << vd << "=" << vs << "+" << w;
14✔
681
        } else if (w.operator<(0)) {
×
682
            elem << vs << "=" << vd << "+" << -w;
×
683
        } else {
684
            const auto [left, right] = std::minmax(vs, vd, variable_registry->printing_order);
×
685
            elem << left << "=" << right;
×
686
        }
687
    } else {
688
        elem << vd << "-" << vs << "<=" << w;
210✔
689
    }
690
    return elem.str();
448✔
691
}
224✔
692

693
StringInvariant ZoneDomain::to_set() const {
1,968✔
694
    if (this->is_top()) {
1,968✔
695
        return StringInvariant::top();
36✔
696
    }
697

698
    const Graph& g = core_->graph();
1,932✔
699

700
    // Extract all the edges
701
    SubGraph g_excl{g, 0};
1,932✔
702

703
    std::map<Variable, Variable> equivalence_classes;
1,932✔
704
    std::set<std::tuple<Variable, Variable, Weight>> diff_csts;
1,932✔
705
    for (const VertId s : g_excl.verts()) {
7,150✔
706
        const Variable vs = *rev_map_.at(s);
5,218✔
707
        Variable least = vs;
5,218✔
708
        for (const VertId d : g_excl.succs(s)) {
9,737✔
709
            const Variable vd = *rev_map_.at(d);
1,648✔
710
            const Weight w = g_excl.edge_val(s, d);
1,648✔
711
            if (w == 0) {
1,648✔
712
                least = std::min(least, vd, variable_registry->printing_order);
2,088✔
713
            } else {
714
                diff_csts.emplace(vd, vs, w);
256✔
715
            }
716
        }
717
        equivalence_classes.insert_or_assign(vs, least);
5,218✔
718
    }
719

720
    std::set<Variable> representatives;
1,932✔
721
    std::set<std::string> result;
1,932✔
722
    for (const auto [vs, least] : equivalence_classes) {
7,150✔
723
        if (vs == least) {
5,218✔
724
            representatives.insert(least);
4,668✔
725
        } else {
726
            result.insert(variable_registry->name(vs) + "=" + variable_registry->name(least));
1,100✔
727
        }
728
    }
729

730
    // simplify: x - y <= k && y - x <= -k
731
    //        -> x <= y + k <= x
732
    //        -> x = y + k
733
    for (const auto& [vd, vs, w] : diff_csts) {
2,188✔
734
        if (!representatives.contains(vd) || !representatives.contains(vs)) {
256✔
735
            continue;
144✔
736
        }
737
        auto dual = to_string(vs, vd, -w, false);
112✔
738
        if (result.contains(dual)) {
112✔
739
            assert(w != 0);
14✔
740
            result.erase(dual);
14✔
741
            result.insert(to_string(vd, vs, w, true));
21✔
742
        } else {
743
            result.insert(to_string(vd, vs, w, false));
147✔
744
        }
745
    }
112✔
746

747
    // Intervals
748
    for (VertId v : g_excl.verts()) {
7,150✔
749
        const auto pvar = this->rev_map_[v];
5,218✔
750
        if (!pvar || !representatives.contains(*pvar)) {
5,218✔
751
            continue;
698✔
752
        }
753
        const bool has_lb = g.elem(v, 0);
4,668✔
754
        const bool has_ub = g.elem(0, v) && !variable_registry->is_min_only(*pvar);
4,668✔
755
        if (!has_lb && !has_ub) {
4,668✔
756
            continue;
76✔
757
        }
758
        Interval v_out{has_lb ? -Number(g.edge_val(v, 0)) : MINUS_INFINITY,
4,592✔
759
                       has_ub ? Number(g.edge_val(0, v)) : PLUS_INFINITY};
4,592✔
760
        assert(!v_out.is_bottom());
4,592✔
761

762
        Variable variable = *pvar;
4,592✔
763

764
        std::stringstream elem;
4,592✔
765
        elem << variable;
4,592✔
766
        if (variable_registry->is_type(variable)) {
4,592✔
767
            auto [lb, ub] = v_out.bound(T_UNINIT, T_MAX);
1,734✔
768
            if (lb == ub) {
1,734✔
769
                if (variable_registry->is_in_stack(variable) && lb == T_NUM) {
1,714✔
770
                    // no need to show this
771
                    continue;
72✔
772
                }
773
                elem << "=" << lb;
1,642✔
774
            } else {
775
                elem << " in " << typeset_to_string(iterate_types(lb, ub));
30✔
776
            }
777
        } else if (variable_registry->is_min_only(variable)) {
2,858✔
778
            // One-sided variables: display just the lower bound
779
            elem << "=" << v_out.lb();
124✔
780
        } else {
781
            elem << "=";
2,734✔
782
            if (v_out.is_singleton()) {
2,734✔
783
                elem << v_out.lb();
2,032✔
784
            } else {
785
                elem << v_out;
702✔
786
            }
787
        }
788
        result.insert(elem.str());
4,520✔
789
    }
4,592✔
790

791
    return StringInvariant{std::move(result)};
1,932✔
792
}
1,932✔
793

794
std::ostream& operator<<(std::ostream& o, const ZoneDomain& dom) { return o << dom.to_set(); }
×
795

796
Weight ZoneDomain::eval_expression(const LinearExpression& e) const {
176,212✔
797
    Weight res = e.constant_term();
176,212✔
798
    for (const auto& [variable, coefficient] : e.variable_terms()) {
355,062✔
799
        res += (pot_value(variable) - core_->potential_at_zero()) * Weight{coefficient};
268,275✔
800
    }
801
    return res;
176,212✔
802
}
803

804
Interval ZoneDomain::compute_residual(const LinearExpression& e, const Variable pivot) const {
8,238✔
805
    Interval residual(-e.constant_term());
8,238✔
806
    for (const auto& [variable, coefficient] : e.variable_terms()) {
18,452✔
807
        if (variable != pivot) {
10,214✔
808
            residual = residual - Interval(coefficient) * get_interval(variable);
1,976✔
809
        }
810
    }
811
    return residual;
8,238✔
812
}
813

814
Weight ZoneDomain::pot_value(const Variable v) const {
178,850✔
815
    if (const auto y = try_at(vert_map_, v)) {
178,850✔
816
        return core_->potential_at(*y);
129,528✔
817
    }
818
    return {0};
49,322✔
819
}
820

821
Interval ZoneDomain::eval_interval(const LinearExpression& e) const {
30,631,363✔
822
    using namespace prevail::interval_operators;
15,317,148✔
823
    Interval r{e.constant_term()};
30,631,363✔
824
    for (const auto& [variable, coefficient] : e.variable_terms()) {
61,171,442✔
825
        r += coefficient * get_interval(variable);
30,540,079✔
826
    }
827
    return r;
30,631,363✔
828
}
829

830
bool ZoneDomain::intersect(const LinearConstraint& cst) const {
60,880✔
831
    if (cst.is_contradiction()) {
60,880✔
832
        return false;
833
    }
834
    if (is_top() || cst.is_tautology()) {
60,880✔
835
        return true;
162✔
836
    }
837
    return intersect_aux(cst);
60,718✔
838
}
839

840
bool ZoneDomain::entail(const LinearConstraint& rhs) const {
1,844,700✔
841
    if (rhs.is_tautology()) {
1,844,700✔
842
        return true;
123✔
843
    }
844
    if (rhs.is_contradiction()) {
1,844,500✔
845
        return false;
10✔
846
    }
847
    const Interval interval = eval_interval(rhs.expression());
1,844,480✔
848
    switch (rhs.kind()) {
1,844,480✔
849
    case ConstraintKind::EQUALS_ZERO:
169,464✔
850
        if (interval.singleton() == std::optional(Number(0))) {
170,329✔
851
            return true;
154,636✔
852
        }
853
        break;
7,415✔
854
    case ConstraintKind::LESS_THAN_OR_EQUALS_ZERO:
131,494✔
855
        if (interval.ub() <= Number(0)) {
197,241✔
856
            return true;
59,460✔
857
        }
858
        break;
6,287✔
859
    case ConstraintKind::LESS_THAN_ZERO:
13,934✔
860
        if (interval.ub() < Number(0)) {
13,934✔
861
            return true;
6,959✔
862
        }
863
        break;
8✔
864
    case ConstraintKind::NOT_ZERO:
1,529,588✔
865
        if (interval.ub() < Number(0) || interval.lb() > Number(0)) {
1,529,588✔
866
            return true;
923,706✔
867
        }
868
        break;
302,941✔
869
    }
870
    // TODO: copy the implementation from crab
871
    //       https://github.com/seahorn/crab/blob/master/include/crab/domains/split_dbm.hpp
872
    if (rhs.kind() == ConstraintKind::EQUALS_ZERO) {
633,300✔
873
        // try to convert the equality into inequalities so when it's
874
        // negated we do not have disequalities.
875
        return entail_aux(LinearConstraint(rhs.expression(), ConstraintKind::LESS_THAN_OR_EQUALS_ZERO)) &&
41,722✔
876
               entail_aux(LinearConstraint(rhs.expression().negate(), ConstraintKind::LESS_THAN_OR_EQUALS_ZERO));
28,783✔
877
    } else {
878
        return entail_aux(rhs);
618,472✔
879
    }
880

881
    // Note: we cannot convert rhs into ZoneDomain and then use the <=
882
    //       operator. The problem is that we cannot know for sure
883
    //       whether ZoneDomain can represent precisely rhs. It is not
884
    //       enough to do something like
885
    //
886
    //       ZoneDomain dom = rhs;
887
    //       if (dom.is_top()) { ... }
888
}
889

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