rust_verify_test coverage (7c1d655)

Coverage Report

Created: 2026-07-20 10:58

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
350k
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
79
96.1k
        match self {
80
254k
            TermX::Var(x) => write!(f, "{}", x),
81
448
            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
91.0k
            TermX::App(c @ (App::Call(_) | App::Ctor(_, _)), es) => {
86
91.0k
                match c {
87
90.9k
                    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
279k
                for i in 0..es.len() {
94
279k
                    write!(f, "{:?}", es[i])?;
95
279k
                    if i < es.len() - 1 {
96
188k
                        write!(f, ", ")?;
97
91.0k
                    }
98
                }
99
91.0k
                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
350k
    }
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
54.8k
    fn lex(&self) -> (u64, u64, u64, u64, u64) {
186
54.8k
        (self.num_operators, self.num_special, self.no_calls, self.depth, self.size)
187
54.8k
    }
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
175k
fn check_timeout(timer: &mut Timer) -> Result<(), VirErr> {
226
175k
    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
175k
        timer.timeout_countdown -= 1;
233
175k
        Ok(())
234
    }
235
175k
}
236
237
811k
fn trigger_vars_in_term(ctxt: &Ctxt, vars: &mut HashSet<VarIdent>, term: &Term) {
238
811k
    match &**term {
239
583k
        TermX::Var(x) if ctxt.trigger_vars.contains(x) => {
240
172k
            vars.insert(x.clone());
241
172k
        }
242
411k
        TermX::Var(..) => {}
243
227k
        TermX::App(_, args) => {
244
655k
            for arg in args.iter() {
245
655k
                trigger_vars_in_term(ctxt, vars, arg);
246
655k
            }
247
        }
248
    }
249
811k
}
250
251
400k
fn term_size(term: &Term) -> u64 {
252
400k
    match &**term {
253
287k
        TermX::Var(..) => 1,
254
113k
        TermX::App(_, args) => 1 + args.iter().map(term_size).sum::<u64>(),
255
    }
256
400k
}
257
258
766k
fn trigger_var_depth(ctxt: &Ctxt, term: &Term, depth: u64) -> Option<u64> {
259
766k
    match &**term {
260
470k
        TermX::Var(x) if ctxt.trigger_vars.contains(x) => Some(depth),
261
385k
        TermX::Var(..) => None,
262
296k
        TermX::App(_, args) => {
263
543k
            args.iter().filter_map(|t| trigger_var_depth(ctxt, t, depth + 1)).max()
264
        }
265
    }
266
766k
}
267
268
400k
fn count_bit_operators(term: &Term) -> u64 {
269
400k
    match &**term {
270
148
        TermX::App(App::BitOp(_), args) => 1 + args.iter().map(count_bit_operators).sum::<u64>(),
271
113k
        TermX::App(_, args) => args.iter().map(count_bit_operators).sum::<u64>(),
272
287k
        TermX::Var(..) => 0,
273
    }
274
400k
}
275
276
400k
fn count_special(term: &Term) -> u64 {
277
400k
    match &**term {
278
310
        TermX::App(App::ClosureSpec, args) => 1 + args.iter().map(count_special).sum::<u64>(),
279
112k
        TermX::App(_, args) => args.iter().map(count_special).sum::<u64>(),
280
287k
        TermX::Var(..) => 0,
281
    }
282
400k
}
283
284
400k
fn count_calls(term: &Term) -> u64 {
285
400k
    match &**term {
286
106k
        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
287k
        TermX::Var(..) => 0,
289
    }
290
400k
}
291
292
77.9k
fn make_score(term: &Term, depth: u64) -> Score {
293
77.9k
    let no_calls = if count_calls(term) == 0 { 1 } else { 0 };
294
77.9k
    Score {
295
77.9k
        num_operators: count_bit_operators(term),
296
77.9k
        num_special: count_special(term),
297
77.9k
        no_calls,
298
77.9k
        depth,
299
77.9k
        size: term_size(term),
300
77.9k
    }
301
77.9k
}
302
303
845k
fn gather_terms(ctxt: &mut Ctxt, ctx: &Ctx, exp: &Exp, depth: u64) -> (bool, Term) {
304
845k
    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
168k
    fn append_typ_params_as_terms(typ: &Typ, all_terms: &mut Vec<Term>) {
311
183k
        let ft = |all_terms: &mut Vec<Term>, t: &Typ| match &**t {
312
160k
            TypX::TypParam(x) => {
313
160k
                let x = crate::def::unique_bound(&crate::def::suffix_typ_param_id(x));
314
160k
                all_terms.push(Arc::new(TermX::Var(x)));
315
160k
                Ok(t.clone())
316
            }
317
22.7k
            _ => Ok(t.clone()),
318
183k
        };
319
168k
        crate::ast_visitor::map_typ_visitor_env(typ, all_terms, &ft).unwrap();
320
168k
    }
321
322
845k
    let (is_pure, term) = match &exp.x {
323
62.6k
        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
163k
        ExpX::Call(x, typs, args) => {
337
            use crate::sst::InternalFun;
338
163k
            let (is_pures, terms): (Vec<bool>, Vec<Term>) =
339
254k
                args.iter().map(|e| gather_terms(ctxt, ctx, e, depth + 1)).unzip();
340
163k
            let is_pure = is_pures.into_iter().all(|b| b);
341
163k
            let mut all_terms: Vec<Term> = Vec::new();
342
168k
            for typ in typs.iter() {
343
168k
                append_typ_params_as_terms(typ, &mut all_terms);
344
168k
            }
345
163k
            all_terms.extend(terms);
346
310
            match x {
347
163k
                CallFun::Fun(x, _) => match ctx.func_map.get(x) {
348
163k
                    Some(f) if f.x.attrs.no_auto_trigger => {
349
0
                        (false, Arc::new(TermX::App(ctxt.other(), Arc::new(all_terms))))
350
                    }
351
163k
                    _ => (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
9.54k
        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::InferSpecForLoopIter { .. } => 1,
417
0
                UnaryOp::StrLen => fail_on_strop(),
418
0
                UnaryOp::MutRefFinal(_) => 1,
419
0
                UnaryOp::MutRefCurrent | UnaryOp::MutRefFuture(_) => unreachable!(),
420
            };
421
32.4k
            let (is_pure1, term1) = gather_terms(ctxt, ctx, e1, depth);
422
32.4k
            match op {
423
0
                UnaryOp::BitNot(_) => (
424
0
                    is_pure1,
425
0
                    Arc::new(TermX::App(App::BitOp(BitOpName::BitNot), Arc::new(vec![term1]))),
426
0
                ),
427
32.4k
                _ => (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1])))),
428
            }
429
        }
430
0
        ExpX::UnaryOpr(UnaryOpr::Box(_), _) => panic!("unexpected box"),
431
0
        ExpX::UnaryOpr(UnaryOpr::Unbox(_), _) => panic!("unexpected box"),
432
        ExpX::UnaryOpr(
433
            UnaryOpr::CustomErr(_)
434
            | UnaryOpr::ProofNote(_)
435
            | UnaryOpr::AutoDecreases
436
            | UnaryOpr::AutoLoopEnsures,
437
0
            e1,
438
0
        ) => gather_terms(ctxt, ctx, e1, depth),
439
        ExpX::UnaryOpr(UnaryOpr::HasType(_), _) => {
440
0
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![]))))
441
        }
442
104
        ExpX::UnaryOpr(UnaryOpr::IntegerTypeBound(_, _), e1) => gather_terms(ctxt, ctx, e1, depth),
443
151
        ExpX::UnaryOpr(UnaryOpr::IsVariant { .. }, e1) => {
444
            // We currently don't auto-trigger on IsVariant
445
            // Even if we did, it might be best not to trigger on IsVariants generated from Match
446
151
            let (_, term1) = gather_terms(ctxt, ctx, e1, 1);
447
151
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1]))))
448
        }
449
0
        ExpX::UnaryOpr(UnaryOpr::HasResolved(_), e1) => {
450
0
            let (is_pure, term1) = gather_terms(ctxt, ctx, e1, depth + 1);
451
0
            (is_pure, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1]))))
452
        }
453
0
        ExpX::UnaryOpr(UnaryOpr::ToDyn(_), e1) => {
454
0
            let (_is_pure, term1) = gather_terms(ctxt, ctx, e1, 1);
455
0
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1]))))
456
        }
457
        ExpX::UnaryOpr(
458
1.16k
            UnaryOpr::Field(FieldOpr { datatype, variant, field, get_variant: _, check: _ }),
459
1.16k
            lhs,
460
        ) => {
461
1.16k
            let (is_pure, arg) = gather_terms(ctxt, ctx, lhs, depth + 1);
462
1.16k
            (
463
1.16k
                is_pure,
464
1.16k
                Arc::new(TermX::App(
465
1.16k
                    App::Field(datatype.clone(), variant.clone(), field.clone()),
466
1.16k
                    Arc::new(vec![arg]),
467
1.16k
                )),
468
1.16k
            )
469
        }
470
222k
        ExpX::Binary(op, e1, e2) => {
471
            use BinaryOp::*;
472
222k
            let depth = match op {
473
113k
                And | Or | Xor | Implies | Eq => 0,
474
0
                HeightCompare { .. } => 1,
475
103k
                Ne | Inequality(_) | Arith(..) | RealArith(..) | IeeeFloat(..) => 1,
476
5.19k
                Bitwise(..) => 1,
477
0
                StrGetChar => fail_on_strop(),
478
0
                Index(..) => 1,
479
            };
480
222k
            let (is_pure1, term1) = gather_terms(ctxt, ctx, e1, depth);
481
222k
            let (is_pure2, term2) = gather_terms(ctxt, ctx, e2, depth);
482
222k
            match op {
483
5.19k
                Bitwise(bp) => {
484
5.19k
                    let bop = match bp {
485
0
                        BitwiseOp::BitXor => BitOpName::BitXor,
486
2.73k
                        BitwiseOp::BitAnd => BitOpName::BitAnd,
487
1.64k
                        BitwiseOp::Shr => BitOpName::Shr,
488
814
                        BitwiseOp::Shl(..) => BitOpName::Shl,
489
0
                        BitwiseOp::BitOr => BitOpName::BitOr,
490
                    };
491
5.19k
                    let is_pure = is_pure1 && is_pure2;
492
5.19k
                    (is_pure, Arc::new(TermX::App(App::BitOp(bop), Arc::new(vec![term1, term2]))))
493
                }
494
216k
                _ => (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1, term2])))),
495
            }
496
        }
497
4.90k
        ExpX::BinaryOpr(crate::ast::BinaryOpr::ExtEq(_, typ), e1, e2) => {
498
4.90k
            let (is_pure1, term1) = gather_terms(ctxt, ctx, e1, 0);
499
4.90k
            let (is_pure2, term2) = gather_terms(ctxt, ctx, e2, 0);
500
4.90k
            let mut terms = vec![term1, term2];
501
4.90k
            if ctxt.gather_for_all_triggers {
502
190
                append_typ_params_as_terms(typ, &mut terms);
503
4.71k
            }
504
4.90k
            if !ctxt.gather_for_all_triggers {
505
4.71k
                (false, Arc::new(TermX::App(ctxt.other(), Arc::new(terms))))
506
            } else {
507
190
                (is_pure1 && is_pure2, Arc::new(TermX::App(App::ExtEq, Arc::new(terms))))
508
            }
509
        }
510
55
        ExpX::If(e1, e2, e3) => {
511
55
            let depth = 1;
512
55
            let (_, term1) = gather_terms(ctxt, ctx, e1, depth);
513
55
            let (_, term2) = gather_terms(ctxt, ctx, e2, depth);
514
55
            let (_, term3) = gather_terms(ctxt, ctx, e3, depth);
515
55
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![term1, term2, term3]))))
516
        }
517
        ExpX::WithTriggers(..) => {
518
0
            panic!("shouldn't be inferring triggers for WithTriggers expression")
519
        }
520
        ExpX::Bind(_, _) => {
521
            // REVIEW: we could at least look for matching loops here
522
12.2k
            (false, Arc::new(TermX::App(ctxt.other(), Arc::new(vec![]))))
523
        }
524
8
        ExpX::ArrayLiteral(es) => {
525
8
            let (is_pures, terms): (Vec<bool>, Vec<Term>) =
526
16
                es.iter().map(|e| gather_terms(ctxt, ctx, e, depth + 1)).unzip();
527
8
            let is_pure = is_pures.into_iter().all(|b| b);
528
8
            (is_pure, Arc::new(TermX::App(ctxt.other(), Arc::new(terms))))
529
        }
530
        ExpX::Interp(_) => {
531
0
            panic!("Found an interpreter expression {:?} outside the interpreter", exp)
532
        }
533
        ExpX::FuelConst(_) => {
534
0
            panic!("Found a FuelConst expression in trigger selection")
535
        }
536
    };
537
845k
    if let TermX::Var(..) = *term {
538
318k
        return (is_pure, term);
539
526k
    }
540
526k
    if let TermX::App(App::VarAt(..), _) = *term {
541
4.80k
        return (is_pure, term);
542
522k
    }
543
522k
    if let TermX::App(App::Tuple, _) = *term {
544
342
        return (is_pure, term);
545
521k
    }
546
521k
    if !ctxt.all_terms.contains_key(&term) {
547
487k
        ctxt.all_terms.insert(term.clone(), exp.span.clone());
548
487k
        if let TermX::App(app, _) = &*term {
549
487k
            if !ctxt.all_terms_by_app.contains_key(app) {
550
438k
                ctxt.all_terms_by_app.insert(app.clone(), HashMap::new());
551
438k
            }
552
487k
            ctxt.all_terms_by_app.get_mut(app).unwrap().insert(term.clone(), exp.span.clone());
553
0
        }
554
34.4k
    }
555
521k
    if is_pure {
556
223k
        if let Some(var_depth) = trigger_var_depth(ctxt, &term, depth) {
557
77.9k
            if !ctxt.pure_terms.contains_key(&term) {
558
76.7k
                ctxt.pure_terms.insert(term.clone(), (exp.clone(), ctxt.pure_terms.len()));
559
76.7k
            }
560
77.9k
            let score = make_score(&term, var_depth);
561
77.9k
            if !ctxt.pure_best_scores.contains_key(&term)
562
1.16k
                || score.lex() < ctxt.pure_best_scores[&term].lex()
563
76.7k
            {
564
76.7k
                ctxt.pure_best_scores.insert(term.clone(), score);
565
76.7k
            }
566
145k
        }
567
298k
    }
568
521k
    (is_pure, term)
569
845k
}
570
571
// First bool: is term equal to template for some instantiation of trigger_vars?
572
// Second bool: is the instantiation potentially bigger than the original template?
573
635k
fn structure_matches(ctxt: &Ctxt, template: &Term, term: &Term) -> (bool, bool) {
574
635k
    match (&**template, &**term) {
575
4.21k
        (TermX::Var(x1), TermX::App(app, _))
576
4.21k
            if ctxt.trigger_vars.contains(x1) && !matches!(app, App::VarAt(..)) =>
577
        {
578
3.30k
            (true, true)
579
        }
580
453k
        (TermX::Var(x1), _) if ctxt.trigger_vars.contains(x1) => (true, false),
581
318k
        (TermX::Var(x1), TermX::Var(x2)) => (x1 == x2, false),
582
177k
        (TermX::App(a1, args1), TermX::App(a2, args2))
583
177k
            if a1 == a2 && args1.len() == args2.len() =>
584
        {
585
176k
            let (eq, bigger): (Vec<bool>, Vec<bool>) = args1
586
176k
                .iter()
587
176k
                .zip(args2.iter())
588
506k
                .map(|(a1, a2)| structure_matches(ctxt, a1, a2))
589
176k
                .unzip();
590
176k
            (eq.into_iter().all(|b| b), bigger.into_iter().any(|b| b))
591
        }
592
3.52k
        _ => (false, false),
593
    }
594
635k
}
595
596
71.0k
fn remove_obvious_potential_loops(ctxt: &mut Ctxt, timer: &mut Timer) -> Result<(), VirErr> {
597
    // Very basic filtering of potential matching loops:
598
    //   eliminate f(...x...) if there's a different term f(...e...)
599
    //   that matches f(...x...) in structure
600
    // REVIEW: we could attempt more sophisticated cycle detection
601
71.0k
    let mut remove: Vec<Term> = Vec::new();
602
76.7k
    for pure in ctxt.pure_terms.keys() {
603
76.7k
        if let TermX::App(app, _) = &**pure
604
76.7k
            && !matches!(app, App::VarAt(..))
605
        {
606
76.7k
            if ctxt.all_terms_by_app.contains_key(app) {
607
129k
                for term in ctxt.all_terms_by_app[app].keys() {
608
129k
                    check_timeout(timer)?;
609
129k
                    let (eq, bigger) = structure_matches(ctxt, pure, term);
610
129k
                    if eq && bigger {
611
70
                        remove.push(pure.clone());
612
70
                        break;
613
128k
                    }
614
                }
615
0
            }
616
0
        }
617
    }
618
71.0k
    for pure in remove {
619
70
        ctxt.pure_terms.remove(&pure);
620
70
    }
621
71.0k
    Ok(())
622
71.0k
}
623
624
type Trigger = Vec<(Term, Span)>;
625
626
struct State {
627
    remaining_vars: HashSet<VarIdent>,
628
    accumulated_terms: HashMap<Term, Span>,
629
    // if AutoType::All, chosen_triggers will contain all minimal covers of the variable set
630
    // if AutoType::Auto, chosen_triggers will contain a single minimal cover chosen by Score heuristic
631
    chosen_triggers: Vec<Trigger>,
632
    // If we relied on Score to break a tie, we consider this a low-confidence trigger
633
    // and we emit a report to the user.
634
    low_confidence: bool,
635
}
636
637
52.5k
fn trigger_score(ctxt: &Ctxt, trigger: &Trigger) -> Score {
638
52.5k
    let mut total = Score { num_operators: 0, num_special: 0, no_calls: 0, depth: 0, size: 0 };
639
55.6k
    for (t, _) in trigger.iter() {
640
55.6k
        let score = &ctxt.pure_best_scores[t];
641
55.6k
        total.num_operators += score.num_operators;
642
55.6k
        total.num_special += score.num_special;
643
55.6k
        total.no_calls += score.no_calls;
644
55.6k
        total.depth += score.depth;
645
55.6k
        total.size += score.size;
646
55.6k
    }
647
52.5k
    total
648
52.5k
}
649
650
/// Compute a set of covering triggers
651
/// If all_triggers is false, Find the best trigger that covers all the trigger variables.
652
/// If all_triggers is true, Return all minimal covering triggers
653
/// This is a variant of minimum-set-cover, which is NP-complete.
654
150k
fn compute_triggers(
655
150k
    ctxt: &Ctxt,
656
150k
    state: &mut State,
657
150k
    timer: &mut Timer,
658
150k
    all_triggers: bool,
659
150k
) -> Result<(), VirErr> {
660
150k
    if state.remaining_vars.len() == 0 {
661
102k
        let trigger: Vec<(Term, Span)> =
662
102k
            state.accumulated_terms.iter().map(|(t, s)| (t.clone(), s.clone())).collect();
663
        // println!("found: {:?} {:?}", trigger, trigger_score(ctxt, &trigger));
664
102k
        if all_triggers {
665
            // when trying to compute all minimal triggers, we need only concern
666
            // ourselves with ensuring
667
            // 1) the new trigger isn't a (proper) subset of an existing one
668
            //    in which case we remove the existing one
669
            // 2) there isn't an existing trigger that is a subset of the new one
670
            //    in which case we don't add the new one
671
            // claim: it is impossible for both to be true
672
            // proof:
673
            // inductive invariant -- all triggers in state.computed_triggers incomparable
674
            // preserved as if we have subset in either direction, exactly one is removed
675
            // assume now we have a new trigger t that is proper subset of to1 and such that to2 is a subset of t.
676
            // we have that to2 is a subset of t1, a contradiction of inductive invariant
677
            // maybe we can formalize this someday :)
678
6.46k
            let mut old_sub_new = false;
679
6.46k
            let trig_exp_set: HashSet<Arc<TermX>> =
680
9.62k
                trigger.iter().map(|(term, _)| term.clone()).collect();
681
17.8k
            state.chosen_triggers.retain(|old_trig| {
682
17.8k
                let old_trig_exp_set: HashSet<Arc<TermX>> =
683
32.3k
                    old_trig.iter().map(|(term, _)| term.clone()).collect();
684
17.8k
                old_sub_new = old_sub_new || old_trig_exp_set.is_subset(&trig_exp_set);
685
17.8k
                !(trig_exp_set.is_subset(&old_trig_exp_set)
686
619
                    && trig_exp_set.len() < old_trig_exp_set.len())
687
17.8k
            });
688
6.46k
            if !old_sub_new {
689
5.84k
                state.chosen_triggers.push(trigger);
690
5.84k
            }
691
6.46k
            return Ok(());
692
96.3k
        }
693
96.3k
        if state.chosen_triggers.len() > 0 {
694
            // If we're better than what came before, drop what came before
695
27.1k
            if state.chosen_triggers[0].len() > trigger.len() {
696
851
                state.chosen_triggers.clear();
697
851
                state.low_confidence = false;
698
851
            } else {
699
26.2k
                let prev_score = trigger_score(ctxt, &state.chosen_triggers[0]).lex();
700
26.2k
                let new_score = trigger_score(ctxt, &trigger).lex();
701
26.2k
                if prev_score > new_score {
702
2.23k
                    state.low_confidence = true;
703
2.23k
                    state.chosen_triggers.clear();
704
24.0k
                } else if prev_score < new_score {
705
3.78k
                    state.low_confidence = true;
706
                    // If we're worse, return
707
3.78k
                    return Ok(());
708
20.2k
                }
709
            }
710
69.2k
        }
711
92.5k
        state.chosen_triggers.push(trigger);
712
92.5k
        return Ok(());
713
47.3k
    }
714
47.3k
    if state.chosen_triggers.len() > 0
715
1.70k
        && !all_triggers
716
1.11k
        && state.chosen_triggers[0].len() <= state.accumulated_terms.len()
717
    {
718
        // We've already found something better
719
        // this early exit optimization only necessary when not computing full set
720
881
        return Ok(());
721
46.4k
    }
722
46.4k
    check_timeout(timer)?;
723
    // pick one variable x from remaining_vars
724
46.4k
    let x = state.remaining_vars.iter().next().unwrap().clone();
725
79.0k
    for (term, span) in &ctxt.pure_terms_by_var[&x] {
726
79.0k
        if !state.accumulated_terms.contains_key(term) {
727
79.0k
            state.accumulated_terms.insert(term.clone(), span.clone());
728
79.0k
            let mut vars: HashSet<VarIdent> = HashSet::new();
729
79.0k
            let mut removed: Vec<VarIdent> = Vec::new();
730
79.0k
            trigger_vars_in_term(ctxt, &mut vars, &term);
731
            // remove term's vars
732
87.8k
            for y in vars {
733
87.8k
                if state.remaining_vars.contains(&y) {
734
85.5k
                    state.remaining_vars.remove(&y);
735
85.5k
                    removed.push(y.clone());
736
85.5k
                }
737
            }
738
79.0k
            compute_triggers(ctxt, state, timer, all_triggers)?;
739
            // restore vars
740
85.5k
            for y in removed {
741
85.5k
                state.remaining_vars.insert(y);
742
85.5k
            }
743
79.0k
            state.accumulated_terms.remove(term);
744
0
        }
745
    }
746
46.4k
    Ok(())
747
150k
}
748
749
71.0k
pub(crate) fn build_triggers(
750
71.0k
    ctx: &Ctx,
751
71.0k
    span: &Span,
752
71.0k
    vars: &Vec<VarIdent>,
753
71.0k
    exp: &Exp,
754
71.0k
    auto_trigger: AutoType,
755
71.0k
) -> Result<Trigs, VirErr> {
756
71.0k
    let mut ctxt = Ctxt {
757
71.0k
        trigger_vars: vars.iter().cloned().collect(),
758
71.0k
        all_terms: HashMap::new(),
759
71.0k
        pure_terms: HashMap::new(),
760
71.0k
        all_terms_by_app: HashMap::new(),
761
71.0k
        pure_terms_by_var: HashMap::new(),
762
71.0k
        pure_best_scores: HashMap::new(),
763
71.0k
        next_id: 0,
764
71.0k
        gather_for_all_triggers: auto_trigger == AutoType::All,
765
71.0k
    };
766
71.0k
    for x in vars {
767
50.2k
        ctxt.pure_terms_by_var.insert(x.clone(), HashMap::new());
768
50.2k
    }
769
71.0k
    let mut timer = Timer { span: span.clone(), timeout_countdown: 10000 };
770
71.0k
    gather_terms(&mut ctxt, ctx, exp, 0);
771
    /*
772
    println!();
773
    println!("all:");
774
    for t in ctxt.all_terms.keys() {
775
        println!("  {:?}", t);
776
    }
777
    println!("pure:");
778
    for t in ctxt.pure_terms.keys() {
779
        println!("  {:?} {:?}", t, ctxt.pure_best_scores[t].lex());
780
    }
781
    */
782
71.0k
    remove_obvious_potential_loops(&mut ctxt, &mut timer)?;
783
    // println!("pure after loop removal:");
784
76.6k
    for (term, (e, _)) in ctxt.pure_terms.iter() {
785
76.6k
        let mut vars: HashSet<VarIdent> = HashSet::new();
786
76.6k
        trigger_vars_in_term(&ctxt, &mut vars, &term);
787
83.1k
        for x in &vars {
788
83.1k
            ctxt.pure_terms_by_var.get_mut(x).unwrap().insert(term.clone(), e.span.clone());
789
83.1k
        }
790
        // println!("  {:?}", term);
791
    }
792
    /*
793
    println!("by var:");
794
    for (x, map) in &ctxt.pure_terms_by_var {
795
        println!("  {:?} {:?}", x, map.keys());
796
    }
797
    */
798
71.0k
    let mut state = State {
799
71.0k
        remaining_vars: ctxt.trigger_vars.iter().cloned().collect(),
800
71.0k
        accumulated_terms: HashMap::new(),
801
71.0k
        chosen_triggers: Vec::new(),
802
71.0k
        low_confidence: false,
803
71.0k
    };
804
71.0k
    compute_triggers(&ctxt, &mut state, &mut timer, auto_trigger == AutoType::All)?;
805
806
    // To stabilize the order of the chosen triggers,
807
    // sort the triggers by the position of their terms in exp
808
94.6k
    for trigger in &mut state.chosen_triggers {
809
94.6k
        trigger.sort_by_key(|(term, _)| ctxt.pure_terms[term].1);
810
    }
811
71.0k
    state.chosen_triggers.sort_by_key(|trig| vec_map(trig, |(term, _)| ctxt.pure_terms[term].1));
812
813
    /*
814
    for found in &state.best_so_far {
815
        let score: u64 = trigger_score(&ctxt, &found);
816
        println!("FOUND: {} {:?}", score, found.iter().map(|(t, _)| t).collect::<Vec<_>>());
817
    }
818
    */
819
71.0k
    let mut chosen_triggers_vec = ctx.global.chosen_triggers.borrow_mut();
820
94.6k
    let found_triggers: Vec<Vec<(Span, String)>> = vec_map(&state.chosen_triggers, |trig| {
821
94.6k
        vec_map(&trig, |(term, span)| (span.clone(), format!("{:?}", term)))
822
94.6k
    });
823
71.0k
    let module = match &ctx.fun {
824
0
        Some(FunctionCtx { module_for_chosen_triggers: Some(m), .. }) => m.clone(),
825
71.0k
        _ => ctx.module.x.path.clone(),
826
    };
827
71.0k
    let chosen_triggers = ChosenTriggers {
828
71.0k
        module,
829
71.0k
        span: span.clone(),
830
71.0k
        triggers: found_triggers,
831
71.0k
        low_confidence: state.low_confidence && (auto_trigger == AutoType::None),
832
        manual: false,
833
    };
834
71.0k
    chosen_triggers_vec.push(chosen_triggers);
835
71.0k
    if state.chosen_triggers.len() >= 1 {
836
94.6k
        let trigs: Vec<Trig> = vec_map(&state.chosen_triggers, |trig| {
837
94.6k
            Arc::new(vec_map(&trig, |(term, _)| ctxt.pure_terms[term].0.clone()))
838
94.6k
        });
839
71.0k
        Ok(Arc::new(trigs))
840
    } else {
841
4
        Err(error(
842
4
            span,
843
4
            "Could not automatically infer triggers for this quantifier.  Use #[trigger] annotations to manually mark trigger terms instead.",
844
4
        ))
845
    }
846
71.0k
}