rust_verify_test coverage (fbbbbcf)

Coverage Report

Created: 2026-08-23 08:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
vir/src/triggers_auto.rs
Line
Count
Source
1
use crate::ast::{
2
    BitwiseOp, Constant, Dt, FieldOpr, Fun, Ident, Typ, TypX, UnaryOp, UnaryOpr, VarAt, VarIdent,
3
    VirErr,
4
};
5
use crate::ast_util::{dt_as_friendly_rust_name, path_as_friendly_rust_name};
6
use crate::context::{ChosenTriggers, Ctx, FunctionCtx};
7
use crate::messages::{Span, error};
8
use crate::sst::{BinaryOp, CallFun, Exp, ExpX, Trig, Trigs, UniqueIdent};
9
use crate::util::vec_map;
10
use std::collections::{HashMap, HashSet};
11
use std::sync::Arc;
12
13
/*
14
This trigger selection algorithm is experimental and somewhat different from the usual
15
selection algorithms, such as the algorithm used by Z3 internally.
16
The goal is to be cautious and avoid triggers that lead to excessive quantifier
17
instantiations, which could lead to SMT timeouts.
18
19
To that end, the algorithm tries to choose only one trigger for any given quantifier,
20
because multiple triggers lead to more unintended instantiations.
21
The one "best" trigger is chosen using a rather arbitrary heuristic score.
22
The algorithm selects multiple triggers only if there is a tie for the first-place score
23
between multiple candidates.
24
25
If the chosen triggers are too conservative,
26
programmers can always override the decision with manual trigger annotations.
27
In fact, the hope is that the default triggers will err on the side of avoiding timeouts,
28
and then programmers can use manual triggers to make the quantifiers more liberal,
29
rather than the defaults causing timeouts,
30
and programmers having to use manual triggers to eliminate the timeouts.
31
*/
32
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
33
pub enum AutoType {
34
    Regular,
35
    All,
36
    None,
37
}
38
39
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
40
enum App {
41
    Const(Constant),
42
    Field(Dt, Ident, Ident),
43
    MutRefCurrent,
44
    MutRefFuture,
45
    Call(Fun),
46
    // datatype constructor: (Path, Variant)
47
    Ctor(Dt, Ident),
48
    Tuple,
49
    ClosureSpec,
50
    // u64 is an id, assigned via a simple counter
51
    Other(u64),
52
    VarAt(UniqueIdent, VarAt),
53
    BitOp(BitOpName),
54
    StaticVar(Fun),
55
    ExecFnByName(Fun),
56
    ExtEq,
57
}
58
59
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
60
pub enum BitOpName {
61
    BitXor,
62
    BitAnd,
63
    BitOr,
64
    Shr,
65
    Shl,
66
    BitNot,
67
}
68
69
type Term = Arc<TermX>;
70
type Terms = Arc<Vec<Term>>;
71
#[derive(PartialEq, Eq, Hash)]
72
enum TermX {
73
    Var(UniqueIdent),
74
    App(App, Terms),
75
}
76
77
impl std::fmt::Debug for TermX {
78
354k
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
79
97.1k
        match self {
80
257k
            TermX::Var(x) => write!(f, "{}", x),
81
440
            TermX::App(App::Const(c), _) => write!(f, "{:?}", c),
82
464
            TermX::App(App::Field(_, x, y), es) => write!(f, "{:?}.{}/{}", es[0], x, y),
83
179
            TermX::App(App::MutRefCurrent, es) => write!(f, "mut_ref_current({:?})", es[0]),
84
225
            TermX::App(App::MutRefFuture, es) => write!(f, "mut_ref_future({:?})", es[0]),
85
92.1k
            TermX::App(c @ (App::Call(_) | App::Ctor(_, _)), es) => {
86
92.1k
                match c {
87
92.0k
                    App::Call(x) => write!(f, "{}(", path_as_friendly_rust_name(&x.path))?,
88
84
                    App::Ctor(path, variant) => {
89
84
                        write!(f, "{}::{}(", dt_as_friendly_rust_name(path), variant)?
90
                    }
91
0
                    _ => unreachable!(),
92
                }
93
282k
                for i in 0..es.len() {
94
282k
                    write!(f, "{:?}", es[i])?;
95
282k
                    if i < es.len() - 1 {
96
190k
                        write!(f, ", ")?;
97
92.1k
                    }
98
                }
99
92.1k
                write!(f, ")")
100
            }
101
244
            TermX::App(App::ExtEq, es) => {
102
244
                write!(f, "ExtEq(")?;
103
813
                for i in 0..es.len() {
104
813
                    write!(f, "{:?}", es[i])?;
105
813
                    if i < es.len() - 1 {
106
569
                        write!(f, ", ")?;
107
244
                    }
108
                }
109
244
                write!(f, ")")
110
            }
111
            TermX::App(App::Tuple, _) => {
112
138
                write!(f, "Tuple")
113
            }
114
            TermX::App(App::ClosureSpec, _) => {
115
180
                write!(f, "ClosureSpec")
116
            }
117
            TermX::App(App::Other(_), _) => {
118
0
                write!(f, "_")
119
            }
120
3.12k
            TermX::App(App::VarAt(x, VarAt::Pre), _) => {
121
3.12k
                write!(f, "old({})", x)
122
            }
123
74
            TermX::App(App::BitOp(bop), _) => {
124
74
                write!(f, "BitOp: {:?}", bop)
125
            }
126
0
            TermX::App(App::StaticVar(fun), _) => {
127
0
                write!(f, "StaticVar: {:?}", fun)
128
            }
129
0
            TermX::App(App::ExecFnByName(fun), _) => {
130
0
                write!(f, "ExecFnByName: {:?}", fun)
131
            }
132
        }
133
354k
    }
134
}
135
136
/*
137
First, we prefer triggers containing the fewest number of terms:
138
- {f(x, y)} (1 term) is better (safer) than {g(x), h(y)} (2 terms)
139
We choose this because a smaller number of terms leads to fewer quantifier instantiations,
140
meaning less chance of an SMT timeout.
141
142
Second, for triggers that are tied for number of terms, we compute a heuristic score:
143
- the depth measures how deeply buried the term is inside other terms
144
  - lower depth is better
145
  - prefer terms next to logical operators or == rather than arithmetic
146
  - we actually measure the max depth to the trigger variables in the term
147
    rather than to the term itself -- otherwise, in f(g(x)),
148
    the term g(x) would be considered higher depth than f(g(x)),
149
    and this would bias the decision towards large terms,
150
    while we actually prefer small terms
151
- the size measures how large a term is
152
  - smaller size is better
153
- terms that contain a function call are better than terms with just constructors and fields
154
  - (avoid choosing something like Option::Some(x) as the trigger)
155
We choose these because they are likely to identify relevant terms
156
such as function definitions f(x, y) == ... or implication f(x, y) ==> ...
157
rather than terms used incidentally inside other terms.
158
159
Obviously, these are fairly arbitrary criteria, but the goal is to make *some* choice,
160
rather than just selecting all the candidate triggers.
161
162
REVIEW: these heuristics are experimental -- are they useful in practice?  Can they be improved?
163
*/
164
165
// Score for a single term in a trigger.
166
// Can be summed to compute a total score for all terms in a trigger
167
// (lower scores are better)
168
#[derive(Debug)]
169
struct Score {
170
    // number of bitwise operators
171
    num_operators: u64,
172
    // number of special operations (currently, InternalFun::ClosureReq/ClosureEns)
173
    num_special: u64,
174
    // 0 means term has function calls
175
    // 1 means term has no function calls (only constructors, fields, operators)
176
    no_calls: u64,
177
    // 1 or more, or 0 for next to ==
178
    depth: u64,
179
    // total size of term
180
    size: u64,
181
}
182
183
impl Score {
184
    // lower score is better (lexicographically ordered)
185
55.4k
    fn lex(&self) -> (u64, u64, u64, u64, u64) {
186
55.4k
        (self.num_operators, self.num_special, self.no_calls, self.depth, self.size)
187
55.4k
    }
188
}
189
190
struct Ctxt {
191
    // variables the triggers must cover
192
    trigger_vars: HashSet<VarIdent>,
193
    // terms with App
194
    all_terms: HashMap<Term, Span>,
195
    // terms with App and without Other
196
    // (note: tuple terms are excluded from pure_terms, but a pure_term may have tuples inside)
197
    // The usize is used to sort the terms in the triggers for better stability
198
    pure_terms: HashMap<Term, (Exp, usize)>,
199
    // all_terms, indexed by head App
200
    all_terms_by_app: HashMap<App, HashMap<Term, Span>>,
201
    // pure_terms, indexed by trigger_vars
202
    pure_terms_by_var: HashMap<VarIdent, HashMap<Term, Span>>,
203
    // best score for this term
204
    pure_best_scores: HashMap<Term, Score>,
205
    // used for Other
206
    next_id: u64,
207
    // gather for all_triggers (include ExtEq)
208
    gather_for_all_triggers: bool,
209
}
210
211
impl Ctxt {
212
275k
    fn other(&mut self) -> App {
213
275k
        self.next_id += 1;
214
275k
        App::Other(self.next_id)
215
275k
    }
216
}
217
218
struct Timer {
219
    // span of entire quantifier
220
    span: Span,
221
    // algorithms are exponential, so give up rather than taking too long
222
    timeout_countdown: u64,
223
}
224
225
177k
fn check_timeout(timer: &mut Timer) -> Result<(), VirErr> {
226
177k
    if timer.timeout_countdown == 0 {
227
0
        Err(error(
228
0
            &timer.span,
229
0
            "could not infer triggers, because quantifier is too large (use manual #[trigger] instead)",
230
0
        ))
231
    } else {
232
177k
        timer.timeout_countdown -= 1;
233
177k
        Ok(())
234
    }
235
177k
}
236
237
819k
fn trigger_vars_in_term(ctxt: &Ctxt, vars: &mut HashSet<VarIdent>, term: &Term) {
238
819k
    match &**term {
239
589k
        TermX::Var(x) if ctxt.trigger_vars.contains(x) => {
240
173k
            vars.insert(x.clone());
241
173k
        }
242
415k
        TermX::Var(..) => {}
243
229k
        TermX::App(_, args) => {
244
661k
            for arg in args.iter() {
245
661k
                trigger_vars_in_term(ctxt, vars, arg);
246
661k
            }
247
        }
248
    }
249
819k
}
250
251
404k
fn term_size(term: &Term) -> u64 {
252
404k
    match &**term {
253
289k
        TermX::Var(..) => 1,
254
114k
        TermX::App(_, args) => 1 + args.iter().map(term_size).sum::<u64>(),
255
    }
256
404k
}
257
258
772k
fn trigger_var_depth(ctxt: &Ctxt, term: &Term, depth: u64) -> Option<u64> {
259
772k
    match &**term {
260
473k
        TermX::Var(x) if ctxt.trigger_vars.contains(x) => Some(depth),
261
387k
        TermX::Var(..) => None,
262
298k
        TermX::App(_, args) => {
263
546k
            args.iter().filter_map(|t| trigger_var_depth(ctxt, t, depth + 1)).max()
264
        }
265
    }
266
772k
}
267
268
404k
fn count_bit_operators(term: &Term) -> u64 {
269
404k
    match &**term {
270
148
        TermX::App(App::BitOp(_), args) => 1 + args.iter().map(count_bit_operators).sum::<u64>(),
271
114k
        TermX::App(_, args) => args.iter().map(count_bit_operators).sum::<u64>(),
272
289k
        TermX::Var(..) => 0,
273
    }
274
404k
}
275
276
404k
fn count_special(term: &Term) -> u64 {
277
404k
    match &**term {
278
310
        TermX::App(App::ClosureSpec, args) => 1 + args.iter().map(count_special).sum::<u64>(),
279
114k
        TermX::App(_, args) => args.iter().map(count_special).sum::<u64>(),
280
289k
        TermX::Var(..) => 0,
281
    }
282
404k
}
283
284
404k
fn count_calls(term: &Term) -> u64 {
285
404k
    match &**term {
286
107k
        TermX::App(App::Call(_), args) => 1 + args.iter().map(count_calls).sum::<u64>(),
287
6.67k
        TermX::App(_, args) => args.iter().map(count_calls).sum::<u64>(),
288
289k
        TermX::Var(..) => 0,
289
    }
290
404k
}
291
292
78.7k
fn make_score(term: &Term, depth: u64) -> Score {
293
78.7k
    let no_calls = if count_calls(term) == 0 { 1 } else { 0 };
294
78.7k
    Score {
295
78.7k
        num_operators: count_bit_operators(term),
296
78.7k
        num_special: count_special(term),
297
78.7k
        no_calls,
298
78.7k
        depth,
299
78.7k
        size: term_size(term),
300
78.7k
    }
301
78.7k
}
302
303
847k
fn gather_terms(ctxt: &mut Ctxt, ctx: &Ctx, exp: &Exp, depth: u64) -> (bool, Term) {
304
847k
    let fail_on_strop = || {
305
0
        unreachable!(
306
            "internal error: doesn't make sense to reach `gather_terms` for string operations defined for verus_builtin, these are only used to tie verus_builtin and vstd together and do not make sense in user programs"
307
        )
308
    };
309
310
170k
    fn append_typ_params_as_terms(typ: &Typ, all_terms: &mut Vec<Term>) {
311
185k
        let ft = |all_terms: &mut Vec<Term>, t: &Typ| match &**t {
312
161k
            TypX::TypParam(x) => {
313
161k
                let x = crate::def::unique_bound(&crate::def::suffix_typ_param_id(x));
314
161k
                all_terms.push(Arc::new(TermX::Var(x)));
315
161k
                Ok(t.clone())
316
            }
317
23.5k
            _ => Ok(t.clone()),
318
185k
        };
319
170k
        crate::ast_visitor::map_typ_visitor_env(typ, all_terms, &ft).unwrap();
320
170k
    }
321
322
847k
    let (is_pure, term) = match &exp.x {
323
62.9k
        ExpX::Const(c) => (true, Arc::new(TermX::App(App::Const(c.clone()), Arc::new(vec![])))),
324
318k
        ExpX::Var(x) => (true, Arc::new(TermX::Var(x.clone()))),
325
0
        ExpX::VarLoc(..) | ExpX::Loc(..) => panic!("unexpected Loc/VarLoc in quantifier"),
326
4.80k
        ExpX::VarAt(x, _) => {
327
4.80k
            (true, Arc::new(TermX::App(App::VarAt(x.clone(), VarAt::Pre), Arc::new(vec![]))))
328
        }
329
0
        ExpX::StaticVar(x) => {
330
0
            (true, Arc::new(TermX::App(App::StaticVar(x.clone()), Arc::new(vec![]))))
331
        }
332
185
        ExpX::ExecFnByName(fun) => {
333
185
            (true, Arc::new(TermX::App(App::ExecFnByName(fun.clone()), Arc::new(vec![]))))
334
        }
335
0
        ExpX::Old(_, _) => panic!("internal error: Old"),
336
165k
        ExpX::Call(x, typs, args) => {
337
            use crate::sst::InternalFun;
338
165k
            let (is_pures, terms): (Vec<bool>, Vec<Term>) =
339
256k
                args.iter().map(|e| gather_terms(ctxt, ctx, e, depth + 1)).unzip();
340
165k
            let is_pure = is_pures.into_iter().all(|b| b);
341
165k
            let mut all_terms: Vec<Term> = Vec::new();
342
170k
            for typ in typs.iter() {
343
170k
                append_typ_params_as_terms(typ, &mut all_terms);
344
170k
            }
345
165k
            all_terms.extend(terms);
346
310
            match x {
347
165k
                CallFun::Fun(x, _) => match ctx.func_map.get(x) {
348
165k
                    Some(f) if f.x.attrs.no_auto_trigger => {
349
0
                        (false, Arc::new(TermX::App(ctxt.other(), Arc::new(all_terms))))
350
                    }
351
165k
                    _ => (is_pure, Arc::new(TermX::App(App::Call(x.clone()), Arc::new(all_terms)))),
352
                },
353
0
                CallFun::Recursive(_) => panic!("internal error: CheckTermination"),
354
                CallFun::InternalFun(
355
                    InternalFun::ClosureReq | InternalFun::ClosureEns | InternalFun::DefaultEns,
356
310
                ) => (is_pure, Arc::new(TermX::App(App::ClosureSpec, Arc::new(all_terms)))),
357
                CallFun::InternalFun(
358
                    InternalFun::CheckDecreaseHeight | InternalFun::OpenInvariantMask(..),
359
0
                ) => (is_pure, Arc::new(TermX::App(ctxt.other(), Arc::new(all_terms)))),
360
            }
361
        }
362
9.10k
        ExpX::CallLambda(e0, es) => {
363
            // REVIEW: maybe we should include CallLambdas in the auto-triggers
364
9.10k
            let depth = 1;
365
9.10k
            let (_, term0) = gather_terms(ctxt, ctx, e0, depth);
366
9.10k
            let mut terms: Vec<Term> =
367
9.83k
                es.iter().map(|e| gather_terms(ctxt, ctx, e, depth).1).collect();
368
9.10k
            terms.insert(0, term0);
369
9.10k
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(terms))))
370
        }
371
1.86k
        ExpX::Ctor(path, variant, fields) => {
372
1.86k
            let (variant, args) = crate::sst_to_air::ctor_to_apply(ctx, path, variant, fields);
373
1.86k
            let (is_pures, terms): (Vec<bool>, Vec<Term>) =
374
1.86k
                args.map(|e| gather_terms(ctxt, ctx, &e.a, depth + 1)).unzip();
375
1.86k
            let is_pure = is_pures.into_iter().all(|b| b);
376
1.86k
            match path {
377
1.52k
                Dt::Path(_) => (
378
1.52k
                    is_pure,
379
1.52k
                    Arc::new(TermX::App(App::Ctor(path.clone(), variant), Arc::new(terms))),
380
1.52k
                ),
381
342
                Dt::Tuple(_) => (is_pure, Arc::new(TermX::App(App::Tuple, Arc::new(terms)))),
382
            }
383
        }
384
7
        ExpX::NullaryOpr(_) => (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![])))),
385
10.0k
        ExpX::Unary(UnaryOp::Trigger(_), e1) => gather_terms(ctxt, ctx, e1, depth),
386
0
        ExpX::Unary(UnaryOp::CoerceMode { .. }, e1) => gather_terms(ctxt, ctx, e1, depth),
387
0
        ExpX::Unary(UnaryOp::MustBeFinalized | UnaryOp::MustBeElaborated, e1) => {
388
0
            gather_terms(ctxt, ctx, e1, depth)
389
        }
390
        ExpX::Unary(UnaryOp::CastToInteger, _) => {
391
0
            panic!("internal error: CastToInteger should have been removed before here")
392
        }
393
1.82k
        ExpX::Unary(op @ (UnaryOp::MutRefCurrent | UnaryOp::MutRefFuture(_)), e1) => {
394
1.82k
            let (is_pure, arg) = gather_terms(ctxt, ctx, e1, depth + 1);
395
1.82k
            let app = match op {
396
1.53k
                UnaryOp::MutRefCurrent => App::MutRefCurrent,
397
299
                UnaryOp::MutRefFuture(_) => App::MutRefFuture,
398
0
                _ => unreachable!(),
399
            };
400
1.82k
            (is_pure, Arc::new(TermX::App(app, Arc::new(vec![arg]))))
401
        }
402
32.4k
        ExpX::Unary(op, e1) => {
403
32.4k
            let depth = match op {
404
                UnaryOp::Not
405
                | UnaryOp::CoerceMode { .. }
406
                | UnaryOp::MustBeFinalized
407
                | UnaryOp::MustBeElaborated
408
                | UnaryOp::CastToInteger
409
4.57k
                | UnaryOp::Length(_) => 0,
410
0
                UnaryOp::HeightTrigger => 1,
411
27.8k
                UnaryOp::Trigger(_) | UnaryOp::Clip { .. } | UnaryOp::BitNot(_) => 1,
412
0
                UnaryOp::IntToReal => 1,
413
0
                UnaryOp::RealToInt => 1,
414
0
                UnaryOp::FloatToBits => 1,
415
0
                UnaryOp::IeeeFloat(_) => 1,
416
0
                UnaryOp::StrLen => fail_on_strop(),
417
0
                UnaryOp::MutRefFinal(_) => 1,
418
0
                UnaryOp::MutRefCurrent | UnaryOp::MutRefFuture(_) => unreachable!(),
419
            };
420
32.4k
            let (is_pure1, term1) = gather_terms(ctxt, ctx, e1, depth);
421
32.4k
            match op {
422
0
                UnaryOp::BitNot(_) => (
423
0
                    is_pure1,
424
0
                    Arc::new(TermX::App(App::BitOp(BitOpName::BitNot), Arc::new(vec![term1]))),
425
0
                ),
426
32.4k
                _ => (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1])))),
427
            }
428
        }
429
0
        ExpX::UnaryOpr(UnaryOpr::Box(_), _) => panic!("unexpected box"),
430
0
        ExpX::UnaryOpr(UnaryOpr::Unbox(_), _) => panic!("unexpected box"),
431
        ExpX::UnaryOpr(
432
            UnaryOpr::CustomErr(_)
433
            | UnaryOpr::ProofNote(_)
434
            | UnaryOpr::AutoDecreases
435
            | UnaryOpr::AutoLoopEnsures,
436
0
            e1,
437
0
        ) => gather_terms(ctxt, ctx, e1, depth),
438
        ExpX::UnaryOpr(UnaryOpr::HasType(_), _) => {
439
0
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![]))))
440
        }
441
104
        ExpX::UnaryOpr(UnaryOpr::IntegerTypeBound(_, _), e1) => gather_terms(ctxt, ctx, e1, depth),
442
151
        ExpX::UnaryOpr(UnaryOpr::IsVariant { .. }, e1) => {
443
            // We currently don't auto-trigger on IsVariant
444
            // Even if we did, it might be best not to trigger on IsVariants generated from Match
445
151
            let (_, term1) = gather_terms(ctxt, ctx, e1, 1);
446
151
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1]))))
447
        }
448
0
        ExpX::UnaryOpr(UnaryOpr::HasResolved(_), e1) => {
449
0
            let (is_pure, term1) = gather_terms(ctxt, ctx, e1, depth + 1);
450
0
            (is_pure, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1]))))
451
        }
452
0
        ExpX::UnaryOpr(UnaryOpr::ToDyn(_), e1) => {
453
0
            let (_is_pure, term1) = gather_terms(ctxt, ctx, e1, 1);
454
0
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1]))))
455
        }
456
        ExpX::UnaryOpr(
457
1.16k
            UnaryOpr::Field(FieldOpr { datatype, variant, field, get_variant: _, check: _ }),
458
1.16k
            lhs,
459
        ) => {
460
1.16k
            let (is_pure, arg) = gather_terms(ctxt, ctx, lhs, depth + 1);
461
1.16k
            (
462
1.16k
                is_pure,
463
1.16k
                Arc::new(TermX::App(
464
1.16k
                    App::Field(datatype.clone(), variant.clone(), field.clone()),
465
1.16k
                    Arc::new(vec![arg]),
466
1.16k
                )),
467
1.16k
            )
468
        }
469
0
        ExpX::UnaryOpr(UnaryOpr::LoopIsolationBoundary(_), _e1) => {
470
0
            panic!("unexpected LoopIsolationBoundary");
471
        }
472
221k
        ExpX::Binary(op, e1, e2) => {
473
            use BinaryOp::*;
474
221k
            let depth = match op {
475
114k
                And | Or | Xor | Implies | Eq => 0,
476
0
                HeightCompare { .. } => 1,
477
101k
                Ne | Inequality(_) | Arith(..) | RealArith(..) | IeeeFloat(..) => 1,
478
5.19k
                Bitwise(..) => 1,
479
0
                StrGetChar => fail_on_strop(),
480
0
                Index(..) => 1,
481
            };
482
221k
            let (is_pure1, term1) = gather_terms(ctxt, ctx, e1, depth);
483
221k
            let (is_pure2, term2) = gather_terms(ctxt, ctx, e2, depth);
484
221k
            match op {
485
5.19k
                Bitwise(bp) => {
486
5.19k
                    let bop = match bp {
487
0
                        BitwiseOp::BitXor => BitOpName::BitXor,
488
2.73k
                        BitwiseOp::BitAnd => BitOpName::BitAnd,
489
1.64k
                        BitwiseOp::Shr => BitOpName::Shr,
490
814
                        BitwiseOp::Shl(..) => BitOpName::Shl,
491
0
                        BitwiseOp::BitOr => BitOpName::BitOr,
492
                    };
493
5.19k
                    let is_pure = is_pure1 && is_pure2;
494
5.19k
                    (is_pure, Arc::new(TermX::App(App::BitOp(bop), Arc::new(vec![term1, term2]))))
495
                }
496
216k
                _ => (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1, term2])))),
497
            }
498
        }
499
4.99k
        ExpX::BinaryOpr(crate::ast::BinaryOpr::ExtEq(_, typ), e1, e2) => {
500
4.99k
            let (is_pure1, term1) = gather_terms(ctxt, ctx, e1, 0);
501
4.99k
            let (is_pure2, term2) = gather_terms(ctxt, ctx, e2, 0);
502
4.99k
            let mut terms = vec![term1, term2];
503
4.99k
            if ctxt.gather_for_all_triggers {
504
190
                append_typ_params_as_terms(typ, &mut terms);
505
4.80k
            }
506
4.99k
            if !ctxt.gather_for_all_triggers {
507
4.80k
                (false, Arc::new(TermX::App(ctxt.other(), Arc::new(terms))))
508
            } else {
509
190
                (is_pure1 && is_pure2, Arc::new(TermX::App(App::ExtEq, Arc::new(terms))))
510
            }
511
        }
512
55
        ExpX::If(e1, e2, e3) => {
513
55
            let depth = 1;
514
55
            let (_, term1) = gather_terms(ctxt, ctx, e1, depth);
515
55
            let (_, term2) = gather_terms(ctxt, ctx, e2, depth);
516
55
            let (_, term3) = gather_terms(ctxt, ctx, e3, depth);
517
55
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1, term2, term3]))))
518
        }
519
        ExpX::WithTriggers(..) => {
520
0
            panic!("shouldn't be inferring triggers for WithTriggers expression")
521
        }
522
        ExpX::Bind(_, _) => {
523
            // REVIEW: we could at least look for matching loops here
524
12.5k
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![]))))
525
        }
526
8
        ExpX::ArrayLiteral(es) => {
527
8
            let (is_pures, terms): (Vec<bool>, Vec<Term>) =
528
16
                es.iter().map(|e| gather_terms(ctxt, ctx, e, depth + 1)).unzip();
529
8
            let is_pure = is_pures.into_iter().all(|b| b);
530
8
            (is_pure, Arc::new(TermX::App(ctxt.other(), Arc::new(terms))))
531
        }
532
        ExpX::Interp(_) => {
533
0
            panic!("Found an interpreter expression {:?} outside the interpreter", exp)
534
        }
535
        ExpX::FuelConst(_) => {
536
0
            panic!("Found a FuelConst expression in trigger selection")
537
        }
538
    };
539
847k
    if let TermX::Var(..) = *term {
540
318k
        return (is_pure, term);
541
529k
    }
542
529k
    if let TermX::App(App::VarAt(..), _) = *term {
543
4.80k
        return (is_pure, term);
544
524k
    }
545
524k
    if let TermX::App(App::Tuple, _) = *term {
546
342
        return (is_pure, term);
547
524k
    }
548
524k
    if !ctxt.all_terms.contains_key(&term) {
549
489k
        ctxt.all_terms.insert(term.clone(), exp.span.clone());
550
489k
        if let TermX::App(app, _) = &*term {
551
489k
            if !ctxt.all_terms_by_app.contains_key(app) {
552
439k
                ctxt.all_terms_by_app.insert(app.clone(), HashMap::new());
553
439k
            }
554
489k
            ctxt.all_terms_by_app.get_mut(app).unwrap().insert(term.clone(), exp.span.clone());
555
0
        }
556
34.9k
    }
557
524k
    if is_pure {
558
225k
        if let Some(var_depth) = trigger_var_depth(ctxt, &term, depth) {
559
78.7k
            if !ctxt.pure_terms.contains_key(&term) {
560
77.5k
                ctxt.pure_terms.insert(term.clone(), (exp.clone(), ctxt.pure_terms.len()));
561
77.5k
            }
562
78.7k
            let score = make_score(&term, var_depth);
563
78.7k
            if !ctxt.pure_best_scores.contains_key(&term)
564
1.16k
                || score.lex() < ctxt.pure_best_scores[&term].lex()
565
77.6k
            {
566
77.6k
                ctxt.pure_best_scores.insert(term.clone(), score);
567
77.6k
            }
568
147k
        }
569
298k
    }
570
524k
    (is_pure, term)
571
847k
}
572
573
// First bool: is term equal to template for some instantiation of trigger_vars?
574
// Second bool: is the instantiation potentially bigger than the original template?
575
642k
fn structure_matches(ctxt: &Ctxt, template: &Term, term: &Term) -> (bool, bool) {
576
642k
    match (&**template, &**term) {
577
4.21k
        (TermX::Var(x1), TermX::App(app, _))
578
4.21k
            if ctxt.trigger_vars.contains(x1) && !matches!(app, App::VarAt(..)) =>
579
        {
580
3.30k
            (true, true)
581
        }
582
458k
        (TermX::Var(x1), _) if ctxt.trigger_vars.contains(x1) => (true, false),
583
322k
        (TermX::Var(x1), TermX::Var(x2)) => (x1 == x2, false),
584
179k
        (TermX::App(a1, args1), TermX::App(a2, args2))
585
179k
            if a1 == a2 && args1.len() == args2.len() =>
586
        {
587
178k
            let (eq, bigger): (Vec<bool>, Vec<bool>) = args1
588
178k
                .iter()
589
178k
                .zip(args2.iter())
590
512k
                .map(|(a1, a2)| structure_matches(ctxt, a1, a2))
591
178k
                .unzip();
592
178k
            (eq.into_iter().all(|b| b), bigger.into_iter().any(|b| b))
593
        }
594
3.52k
        _ => (false, false),
595
    }
596
642k
}
597
598
72.0k
fn remove_obvious_potential_loops(ctxt: &mut Ctxt, timer: &mut Timer) -> Result<(), VirErr> {
599
    // Very basic filtering of potential matching loops:
600
    //   eliminate f(...x...) if there's a different term f(...e...)
601
    //   that matches f(...x...) in structure
602
    // REVIEW: we could attempt more sophisticated cycle detection
603
72.0k
    let mut remove: Vec<Term> = Vec::new();
604
77.5k
    for pure in ctxt.pure_terms.keys() {
605
77.5k
        if let TermX::App(app, _) = &**pure
606
77.5k
            && !matches!(app, App::VarAt(..))
607
        {
608
77.5k
            if ctxt.all_terms_by_app.contains_key(app) {
609
130k
                for term in ctxt.all_terms_by_app[app].keys() {
610
130k
                    check_timeout(timer)?;
611
130k
                    let (eq, bigger) = structure_matches(ctxt, pure, term);
612
130k
                    if eq && bigger {
613
70
                        remove.push(pure.clone());
614
70
                        break;
615
130k
                    }
616
                }
617
0
            }
618
0
        }
619
    }
620
72.0k
    for pure in remove {
621
70
        ctxt.pure_terms.remove(&pure);
622
70
    }
623
72.0k
    Ok(())
624
72.0k
}
625
626
type Trigger = Vec<(Term, Span)>;
627
628
struct State {
629
    remaining_vars: HashSet<VarIdent>,
630
    accumulated_terms: HashMap<Term, Span>,
631
    // if AutoType::All, chosen_triggers will contain all minimal covers of the variable set
632
    // if AutoType::Auto, chosen_triggers will contain a single minimal cover chosen by Score heuristic
633
    chosen_triggers: Vec<Trigger>,
634
    // If we relied on Score to break a tie, we consider this a low-confidence trigger
635
    // and we emit a report to the user.
636
    low_confidence: bool,
637
}
638
639
53.1k
fn trigger_score(ctxt: &Ctxt, trigger: &Trigger) -> Score {
640
53.1k
    let mut total = Score { num_operators: 0, num_special: 0, no_calls: 0, depth: 0, size: 0 };
641
56.2k
    for (t, _) in trigger.iter() {
642
56.2k
        let score = &ctxt.pure_best_scores[t];
643
56.2k
        total.num_operators += score.num_operators;
644
56.2k
        total.num_special += score.num_special;
645
56.2k
        total.no_calls += score.no_calls;
646
56.2k
        total.depth += score.depth;
647
56.2k
        total.size += score.size;
648
56.2k
    }
649
53.1k
    total
650
53.1k
}
651
652
/// Compute a set of covering triggers
653
/// If all_triggers is false, Find the best trigger that covers all the trigger variables.
654
/// If all_triggers is true, Return all minimal covering triggers
655
/// This is a variant of minimum-set-cover, which is NP-complete.
656
151k
fn compute_triggers(
657
151k
    ctxt: &Ctxt,
658
151k
    state: &mut State,
659
151k
    timer: &mut Timer,
660
151k
    all_triggers: bool,
661
151k
) -> Result<(), VirErr> {
662
151k
    if state.remaining_vars.len() == 0 {
663
104k
        let trigger: Vec<(Term, Span)> =
664
104k
            state.accumulated_terms.iter().map(|(t, s)| (t.clone(), s.clone())).collect();
665
        // println!("found: {:?} {:?}", trigger, trigger_score(ctxt, &trigger));
666
104k
        if all_triggers {
667
            // when trying to compute all minimal triggers, we need only concern
668
            // ourselves with ensuring
669
            // 1) the new trigger isn't a (proper) subset of an existing one
670
            //    in which case we remove the existing one
671
            // 2) there isn't an existing trigger that is a subset of the new one
672
            //    in which case we don't add the new one
673
            // claim: it is impossible for both to be true
674
            // proof:
675
            // inductive invariant -- all triggers in state.computed_triggers incomparable
676
            // preserved as if we have subset in either direction, exactly one is removed
677
            // assume now we have a new trigger t that is proper subset of to1 and such that to2 is a subset of t.
678
            // we have that to2 is a subset of t1, a contradiction of inductive invariant
679
            // maybe we can formalize this someday :)
680
6.47k
            let mut old_sub_new = false;
681
6.47k
            let trig_exp_set: HashSet<Arc<TermX>> =
682
9.63k
                trigger.iter().map(|(term, _)| term.clone()).collect();
683
17.9k
            state.chosen_triggers.retain(|old_trig| {
684
17.9k
                let old_trig_exp_set: HashSet<Arc<TermX>> =
685
32.3k
                    old_trig.iter().map(|(term, _)| term.clone()).collect();
686
17.9k
                old_sub_new = old_sub_new || old_trig_exp_set.is_subset(&trig_exp_set);
687
17.9k
                !(trig_exp_set.is_subset(&old_trig_exp_set)
688
595
                    && trig_exp_set.len() < old_trig_exp_set.len())
689
17.9k
            });
690
6.47k
            if !old_sub_new {
691
5.82k
                state.chosen_triggers.push(trigger);
692
5.82k
            }
693
6.47k
            return Ok(());
694
97.6k
        }
695
97.6k
        if state.chosen_triggers.len() > 0 {
696
            // If we're better than what came before, drop what came before
697
27.4k
            if state.chosen_triggers[0].len() > trigger.len() {
698
875
                state.chosen_triggers.clear();
699
875
                state.low_confidence = false;
700
875
            } else {
701
26.5k
                let prev_score = trigger_score(ctxt, &state.chosen_triggers[0]).lex();
702
26.5k
                let new_score = trigger_score(ctxt, &trigger).lex();
703
26.5k
                if prev_score > new_score {
704
2.26k
                    state.low_confidence = true;
705
2.26k
                    state.chosen_triggers.clear();
706
24.2k
                } else if prev_score < new_score {
707
3.78k
                    state.low_confidence = true;
708
                    // If we're worse, return
709
3.78k
                    return Ok(());
710
20.5k
                }
711
            }
712
70.2k
        }
713
93.8k
        state.chosen_triggers.push(trigger);
714
93.8k
        return Ok(());
715
47.8k
    }
716
47.8k
    if state.chosen_triggers.len() > 0
717
1.68k
        && !all_triggers
718
1.08k
        && state.chosen_triggers[0].len() <= state.accumulated_terms.len()
719
    {
720
        // We've already found something better
721
        // this early exit optimization only necessary when not computing full set
722
858
        return Ok(());
723
47.0k
    }
724
47.0k
    check_timeout(timer)?;
725
    // pick one variable x from remaining_vars
726
47.0k
    let x = state.remaining_vars.iter().next().unwrap().clone();
727
79.9k
    for (term, span) in &ctxt.pure_terms_by_var[&x] {
728
79.9k
        if !state.accumulated_terms.contains_key(term) {
729
79.9k
            state.accumulated_terms.insert(term.clone(), span.clone());
730
79.9k
            let mut vars: HashSet<VarIdent> = HashSet::new();
731
79.9k
            let mut removed: Vec<VarIdent> = Vec::new();
732
79.9k
            trigger_vars_in_term(ctxt, &mut vars, &term);
733
            // remove term's vars
734
88.7k
            for y in vars {
735
88.7k
                if state.remaining_vars.contains(&y) {
736
86.4k
                    state.remaining_vars.remove(&y);
737
86.4k
                    removed.push(y.clone());
738
86.4k
                }
739
            }
740
79.9k
            compute_triggers(ctxt, state, timer, all_triggers)?;
741
            // restore vars
742
86.4k
            for y in removed {
743
86.4k
                state.remaining_vars.insert(y);
744
86.4k
            }
745
79.9k
            state.accumulated_terms.remove(term);
746
0
        }
747
    }
748
47.0k
    Ok(())
749
151k
}
750
751
72.0k
pub(crate) fn build_triggers(
752
72.0k
    ctx: &Ctx,
753
72.0k
    span: &Span,
754
72.0k
    vars: &Vec<VarIdent>,
755
72.0k
    exp: &Exp,
756
72.0k
    auto_trigger: AutoType,
757
72.0k
) -> Result<Trigs, VirErr> {
758
72.0k
    let mut ctxt = Ctxt {
759
72.0k
        trigger_vars: vars.iter().cloned().collect(),
760
72.0k
        all_terms: HashMap::new(),
761
72.0k
        pure_terms: HashMap::new(),
762
72.0k
        all_terms_by_app: HashMap::new(),
763
72.0k
        pure_terms_by_var: HashMap::new(),
764
72.0k
        pure_best_scores: HashMap::new(),
765
72.0k
        next_id: 0,
766
72.0k
        gather_for_all_triggers: auto_trigger == AutoType::All,
767
72.0k
    };
768
72.0k
    for x in vars {
769
50.8k
        ctxt.pure_terms_by_var.insert(x.clone(), HashMap::new());
770
50.8k
    }
771
72.0k
    let mut timer = Timer { span: span.clone(), timeout_countdown: 10000 };
772
72.0k
    gather_terms(&mut ctxt, ctx, exp, 0);
773
    /*
774
    println!();
775
    println!("all:");
776
    for t in ctxt.all_terms.keys() {
777
        println!("  {:?}", t);
778
    }
779
    println!("pure:");
780
    for t in ctxt.pure_terms.keys() {
781
        println!("  {:?} {:?}", t, ctxt.pure_best_scores[t].lex());
782
    }
783
    */
784
72.0k
    remove_obvious_potential_loops(&mut ctxt, &mut timer)?;
785
    // println!("pure after loop removal:");
786
77.5k
    for (term, (e, _)) in ctxt.pure_terms.iter() {
787
77.5k
        let mut vars: HashSet<VarIdent> = HashSet::new();
788
77.5k
        trigger_vars_in_term(&ctxt, &mut vars, &term);
789
83.9k
        for x in &vars {
790
83.9k
            ctxt.pure_terms_by_var.get_mut(x).unwrap().insert(term.clone(), e.span.clone());
791
83.9k
        }
792
        // println!("  {:?}", term);
793
    }
794
    /*
795
    println!("by var:");
796
    for (x, map) in &ctxt.pure_terms_by_var {
797
        println!("  {:?} {:?}", x, map.keys());
798
    }
799
    */
800
72.0k
    let mut state = State {
801
72.0k
        remaining_vars: ctxt.trigger_vars.iter().cloned().collect(),
802
72.0k
        accumulated_terms: HashMap::new(),
803
72.0k
        chosen_triggers: Vec::new(),
804
72.0k
        low_confidence: false,
805
72.0k
    };
806
72.0k
    compute_triggers(&ctxt, &mut state, &mut timer, auto_trigger == AutoType::All)?;
807
808
    // To stabilize the order of the chosen triggers,
809
    // sort the triggers by the position of their terms in exp
810
95.9k
    for trigger in &mut state.chosen_triggers {
811
95.9k
        trigger.sort_by_key(|(term, _)| ctxt.pure_terms[term].1);
812
    }
813
72.0k
    state.chosen_triggers.sort_by_key(|trig| vec_map(trig, |(term, _)| ctxt.pure_terms[term].1));
814
815
    /*
816
    for found in &state.best_so_far {
817
        let score: u64 = trigger_score(&ctxt, &found);
818
        println!("FOUND: {} {:?}", score, found.iter().map(|(t, _)| t).collect::<Vec<_>>());
819
    }
820
    */
821
72.0k
    let mut chosen_triggers_vec = ctx.global.chosen_triggers.borrow_mut();
822
95.9k
    let found_triggers: Vec<Vec<(Span, String)>> = vec_map(&state.chosen_triggers, |trig| {
823
95.9k
        vec_map(&trig, |(term, span)| (span.clone(), format!("{:?}", term)))
824
95.9k
    });
825
72.0k
    let module = match &ctx.fun {
826
0
        Some(FunctionCtx { module_for_chosen_triggers: Some(m), .. }) => m.clone(),
827
72.0k
        _ => ctx.module.x.path.clone(),
828
    };
829
72.0k
    let chosen_triggers = ChosenTriggers {
830
72.0k
        module,
831
72.0k
        span: span.clone(),
832
72.0k
        triggers: found_triggers,
833
72.0k
        low_confidence: state.low_confidence && (auto_trigger == AutoType::None),
834
        manual: false,
835
    };
836
72.0k
    chosen_triggers_vec.push(chosen_triggers);
837
72.0k
    if state.chosen_triggers.len() >= 1 {
838
95.9k
        let trigs: Vec<Trig> = vec_map(&state.chosen_triggers, |trig| {
839
95.9k
            Arc::new(vec_map(&trig, |(term, _)| ctxt.pure_terms[term].0.clone()))
840
95.9k
        });
841
72.0k
        Ok(Arc::new(trigs))
842
    } else {
843
4
        Err(error(
844
4
            span,
845
4
            "Could not automatically infer triggers for this quantifier.  Use #[trigger] annotations to manually mark trigger terms instead.",
846
4
        ))
847
    }
848
72.0k
}