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