rucc_opt/discharge.rs
1//! Taking out a safety check whose answer is already known.
2//!
3//! Design: `spec/safe-memory/07-check-elimination.md` section 7.3, which is the first half of the
4//! Tier E budget. `rucc-safety` puts a bounds check and a lifetime check in front of every access
5//! and does not try to be clever about it, on purpose: a walk that inserts everything is a walk
6//! anybody can read, and every check that is not needed is meant to be taken out here instead.
7//! This pass takes them out, and it does the case document 07 expects to be worth the most and to
8//! be the easiest to get right, which is a second access to bytes an earlier access already had
9//! checked. Both checks in front of that access are the pass's business, because `rucc-safety`
10//! emits the pair and taking out one of a pair is half a saving.
11//!
12//! # The two halves
13//!
14//! Section 7.7 asks for the pass and the condition to be separate things, and they are. What is in
15//! this file is a walk: which check runs before which, which pointer was computed from which, and
16//! how far apart two addresses are. Nothing here decides whether that is enough. The condition
17//! under which a check may go is a rule in `rules/safety.rules`, a solver has to agree with it
18//! before this crate finishes building, and `crate::rules::safety` is the table it compiles into.
19//!
20//! The split is worth the trouble because the two halves fail differently. A walk that gets the
21//! context wrong is a bug of the ordinary kind, and section 14.3's differential check accounting,
22//! which runs the instrumented program with every check and again with the discharged ones gone,
23//! is what looks for it. A removal condition that is wrong is arithmetic that is off at the ends of
24//! the type. It gives the right answer on every test anybody writes and lets one access through in
25//! the one case nobody thought of, and nothing observes that until somebody exploits it.
26//!
27//! # What it establishes and what it asks
28//!
29//! Walking the dominator tree from the entry, the pass carries a set of facts. A `check_bounds`
30//! that stays is a fact, because a check that passes says the bytes it was about lie inside one
31//! storage instance, and a check that fails does not return. A fact is remembered as the pointer's
32//! base and the constant offset from it, which is what a chain of `ptr_add` over constants comes
33//! to, plus how many bytes the access covers.
34//!
35//! At the next `check_bounds`, the pointer is normalized the same way. When a fact shares its base,
36//! the distance between the two accesses is the difference of the two offsets, and that is a number
37//! this pass has rather than a claim it makes: both addresses are the same value plus a constant.
38//! The question of whether the later bytes are inside the earlier ones is then handed to the table,
39//! which answers it in sixty four bit arithmetic rather than in the offsets, and the check goes
40//! only if the answer is yes.
41//!
42//! The capability operand has to be the `cap_of` of the check's own pointer, which is the shape
43//! `rucc-safety` emits and the shape the argument needs. The check being removed asks whether its
44//! bytes are inside the instance that owns its own pointer, its pointer is inside the range the
45//! earlier check established, and that range is inside one instance, so the answer is yes. A check
46//! whose capability came from somewhere else is asking about a different instance and is left
47//! alone. Nothing is required of the earlier check's capability, because all that is used of it is
48//! that the check passed, and a check that passed put its bytes inside one instance whatever
49//! capability it named.
50//!
51//! # The lifetime half, and what it borrows from the other one
52//!
53//! A `check_live` that stays is a fact too, and a smaller one than it looks: it says the storage
54//! instance holding its own address is alive, and it says nothing about the address four bytes
55//! along, because that address might be in a different instance. On its own that fact discharges
56//! only a second lifetime check of the very same address, and the shape `rucc-safety` emits is a
57//! lifetime check per field rather than per object, so on its own it would almost never fire.
58//!
59//! What makes it fire is the bounds fact sitting next to it. A `check_bounds` that passed put its
60//! whole range inside one instance, so if the lifetime check's address is in that range, the
61//! instance that was found alive is the instance the whole range is in, and the whole range is
62//! alive. So a lifetime fact is recorded as the widest checked range containing its address, and a
63//! later lifetime check is asked about as a single byte. The question of whether that byte is in
64//! that range is the same question the bounds half asks, put to the same rule.
65//!
66//! The order the two arrive in is what makes this work rather than a coincidence to be careful
67//! about: `rucc-safety` emits the bounds check first and the lifetime check second, so the range is
68//! established by the time there is a lifetime fact to widen. A lifetime check that arrives with no
69//! range around it keeps the narrow fact, which is correct and worth little.
70//!
71//! # Why a call throws the facts away, and which calls do not
72//!
73//! Section 7.3 says nothing kills a bounds fact except a redefinition of the capability, which in
74//! SSA is never, and this pass is stricter than that: a call, or anything else this pass cannot see
75//! through, drops every fact it is carrying.
76//!
77//! The case is a `free` and then an allocation of something smaller at the same address. The range
78//! established before the call is no longer inside one instance after it, and what document 07
79//! leaves that to is the lifetime judgement rather than this one. Today's lifetime check is about
80//! the address rather than about the version the capability was taken at, so it would not refuse
81//! the access either, and a rate this pass reports is worth less than a hole it opens. The strict
82//! version is what is written first.
83//!
84//! A `meta_end` and a `meta_transfer` drop the facts as well. Nothing emits either one yet, so
85//! this costs nothing today and is the difference between conservative and wrong on the day the
86//! instrumentation starts ending lifetimes. `crate::nofree` treats them the same way.
87//!
88//! A call that says it reaches nothing which can free is the exception, and it is not this pass
89//! being trusting. `crate::nofree` works the answer out over the whole module before the pipeline
90//! starts and writes it onto the call site as [`Flags::NOFREE`], because the fact belongs to the
91//! callee and a pass is given one function. Reading it here is reading what the IR says, the same
92//! way the pass reads an opcode. Nothing else about a call is believed: the facts still go across
93//! an unmarked call, a call through an address, and inline assembly.
94//!
95//! What the strictness still costs is measured rather than guessed. A check that a fact would have
96//! covered if a call had not intervened is counted, so `-fopt-info-missed` says per function what
97//! is left to win.
98
99use rucc_ir::{Def, Extra, Flags, Func, Inst, Opcode, Value};
100
101use crate::rules::{Piece, Subject, Table, safety};
102use crate::{Analyses, Fuel, Pass, Preserved, Stats};
103
104/// Recorded once for each bounds check taken out.
105const REMOVED: &str = "bounds check removed, a dominating check covers the same bytes";
106
107/// Recorded once for each lifetime check taken out.
108const REMOVED_LIVE: &str = "lifetime check removed, a dominating check covers the same storage";
109
110/// Recorded for a bounds check that would have gone if there had been fuel for it.
111const NO_FUEL: &str = "bounds check kept, the pass ran out of fuel";
112
113/// Recorded for a lifetime check that would have gone if there had been fuel for it.
114const NO_FUEL_LIVE: &str = "lifetime check kept, the pass ran out of fuel";
115
116/// Recorded for a bounds check a call cost, which is the honest price of the paragraph above.
117///
118/// This one is worth reading rather than skipping. It is the number of checks that are still being
119/// paid for because `crate::nofree` could not vouch for a call, so it says per function what the
120/// rest of section 7.5's summary work would be worth before anybody writes it.
121const PAST_A_CALL: &str =
122 "bounds check kept, a call between it and the check that covers it might free";
123
124/// The same, for a lifetime check. Section 8.8 is about this number rather than the one above.
125const PAST_A_CALL_LIVE: &str =
126 "lifetime check kept, a call between it and the check that covers it might free";
127
128/// Recorded for a bounds check whose operands this pass cannot read.
129const UNKNOWN_SHAPE: &str = "bounds check left alone, its pointer is not a base and a constant";
130
131/// Recorded for a lifetime check whose operands this pass cannot read.
132const UNKNOWN_SHAPE_LIVE: &str =
133 "lifetime check left alone, its pointer is not a base and a constant";
134
135/// The pass. It holds nothing, because everything it works out is about one function.
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct Discharge;
138
139impl Pass for Discharge {
140 fn name(&self) -> &'static str {
141 "discharge"
142 }
143
144 fn describe(&self) -> &'static str {
145 "a bounds or lifetime check a dominating check already covered is removed"
146 }
147
148 fn preserves(&self) -> Preserved {
149 // Instructions go and blocks do not. A check is not a terminator and removing one leaves
150 // every edge where it was.
151 Preserved::ALL
152 }
153
154 fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
155 let mut stats = Stats::new();
156 let Some(entry) = func.entry() else { return stats };
157 let dom = an.dominators(func).clone();
158
159 // The walk is a stack rather than recursion because the dominator tree of a long chain of
160 // blocks is as deep as the function is long, and a pass is not a place to find that out.
161 // Each block carries its own copy of what holds at its start, which is what makes a fact a
162 // call killed in one arm of a branch still hold in the other.
163 let mut going: Vec<(Inst, &'static str)> = Vec::new();
164 let mut work = vec![(entry, Scope::default())];
165 while let Some((block, mut scope)) = work.pop() {
166 for inst in func.insts(block).collect::<Vec<Inst>>() {
167 if opaque(func, inst) {
168 scope.forget();
169 continue;
170 }
171 match func[inst].opcode {
172 Opcode::CheckBounds => {
173 let Some(asked) = about(func, inst) else {
174 stats.missed(UNKNOWN_SHAPE);
175 continue;
176 };
177 if !scope.bounds.covers(&asked) {
178 if scope.bounds.covered_before(&asked) {
179 stats.missed(PAST_A_CALL);
180 }
181 // A check that stays is a check that runs, and a check that runs
182 // establishes what it was about. One that was removed establishes
183 // nothing new: whatever covered it covers everything it would have.
184 scope.bounds.held.push(asked);
185 continue;
186 }
187 if !fuel.take() {
188 stats.missed(NO_FUEL);
189 scope.bounds.held.push(asked);
190 continue;
191 }
192 going.push((inst, REMOVED));
193 }
194 Opcode::CheckLive => {
195 let Some(asked) = alive(func, inst) else {
196 stats.missed(UNKNOWN_SHAPE_LIVE);
197 continue;
198 };
199 if !scope.alive.covers(&asked) {
200 if scope.alive.covered_before(&asked) {
201 stats.missed(PAST_A_CALL_LIVE);
202 }
203 scope.alive.held.push(widened(&scope.bounds, asked));
204 continue;
205 }
206 if !fuel.take() {
207 stats.missed(NO_FUEL_LIVE);
208 scope.alive.held.push(widened(&scope.bounds, asked));
209 continue;
210 }
211 going.push((inst, REMOVED_LIVE));
212 }
213 _ => continue,
214 }
215 }
216 for child in dom.children(block) {
217 work.push((child, scope.clone()));
218 }
219 }
220
221 for (inst, why) in going {
222 func.remove_inst(inst);
223 stats.optimized(why);
224 }
225 stats
226 }
227}
228
229/// A range of bytes some check has already been passed on, or is being asked about.
230///
231/// The address is kept as the value it was computed from and the constant distance from it, rather
232/// than as the pointer itself, because that is what makes two of these comparable: the whole of
233/// what this pass knows about two addresses is that they are one value plus two constants.
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235struct Fact {
236 /// The value the address was computed from.
237 base: Value,
238 /// How far past it the access starts.
239 offset: i128,
240 /// How many bytes it covers.
241 size: i128,
242}
243
244/// One kind of fact, and what has become of it.
245#[derive(Debug, Clone, Default)]
246struct Known {
247 /// The ranges a check has been passed on and nothing has cast doubt on since.
248 held: Vec<Fact>,
249 /// The ones a call threw away, kept only so that the cost of throwing them away is a number
250 /// somebody can read rather than a paragraph somebody has to believe.
251 lost: Vec<Fact>,
252}
253
254impl Known {
255 /// Whether something still standing answers this.
256 fn covers(&self, asked: &Fact) -> bool {
257 self.held.iter().any(|fact| covers(fact, asked))
258 }
259
260 /// Whether something would have answered it before a call came along.
261 fn covered_before(&self, asked: &Fact) -> bool {
262 self.lost.iter().any(|fact| covers(fact, asked))
263 }
264
265 /// Gives up everything, because something happened that this pass cannot see through.
266 fn forget(&mut self) {
267 self.lost.append(&mut self.held);
268 }
269}
270
271/// What holds where the walk has got to.
272///
273/// The two kinds are apart because they are killed together and answered separately: a range being
274/// inside one instance and that instance being alive are different claims, and reporting them as
275/// one number would hide which of the two a check is still being paid for.
276#[derive(Debug, Clone, Default)]
277struct Scope {
278 /// Ranges a `check_bounds` established are inside one storage instance.
279 bounds: Known,
280 /// Ranges a `check_live` established are in an instance that is alive.
281 alive: Known,
282}
283
284impl Scope {
285 /// Gives up every fact of either kind.
286 fn forget(&mut self) {
287 self.bounds.forget();
288 self.alive.forget();
289 }
290}
291
292/// Whether this instruction could do something to memory that this pass cannot account for.
293///
294/// A call is the whole of it, in every spelling, and inline assembly with it. A `tail_call` ends
295/// the block and there is nothing after it to protect, and it is here anyway so that the reason a
296/// fact survives is never that the walk did not think of something.
297///
298/// A call carrying [`Flags::NOFREE`] reaches nothing that ends a lifetime, so there is nothing for
299/// it to have done to the bytes an earlier check was passed on. `crate::nofree` is what put the
300/// flag there and what argues for it.
301///
302/// A `meta_end` and a `meta_transfer` end a lifetime by saying so, which is the plainest way for a
303/// fact to stop being true, and neither is emitted today.
304fn opaque(func: &Func, inst: Inst) -> bool {
305 match func[inst].opcode {
306 Opcode::Call | Opcode::CallIndirect | Opcode::TailCall => {
307 !func[inst].flags.contains(Flags::NOFREE)
308 }
309 Opcode::InlineAsm | Opcode::MetaEnd | Opcode::MetaTransfer => true,
310 _ => false,
311 }
312}
313
314/// What a `check_bounds` is about, when it is one this pass can read.
315fn about(func: &Func, check: Inst) -> Option<Fact> {
316 let (base, offset) = addressed(func, check)?;
317 let Extra::Mem(info) = func[check].extra else { return None };
318 Some(Fact { base, offset, size: i128::from(func[info].size) })
319}
320
321/// What a `check_live` is about, when it is one this pass can read.
322///
323/// One byte, because that is the whole of what the check says: the instance holding this address
324/// is alive, and nothing about the address next door. The widening to a range that makes the fact
325/// useful is [`widened`], and it needs a bounds fact to do it.
326fn alive(func: &Func, check: Inst) -> Option<Fact> {
327 let (base, offset) = addressed(func, check)?;
328 Some(Fact { base, offset, size: 1 })
329}
330
331/// The address a check is about, as a base and a constant.
332///
333/// The capability has to be the `cap_of` of the check's own pointer. That is the shape
334/// `rucc-safety` emits and it is what the removal argument in the module comment needs, so a check
335/// that does not have it is not a check this pass has anything to say about.
336fn addressed(func: &Func, check: Inst) -> Option<(Value, i128)> {
337 let args = &func[func[check].args];
338 let &capability = args.first()?;
339 let &pointer = args.get(1)?;
340 if operand_of(func, capability, Opcode::CapOf, 0) != Some(pointer) {
341 return None;
342 }
343 Some(normal(func, pointer))
344}
345
346/// A lifetime fact grown from one address to the checked range it sits in.
347///
348/// The argument is in the module comment: a `check_bounds` that passed put its whole range inside
349/// one instance, so the instance this lifetime check found alive is the instance that range is in.
350/// With no range around the address the fact stays as it came, which is correct and answers only a
351/// repeat of the very same check.
352fn widened(bounds: &Known, asked: Fact) -> Fact {
353 bounds.held.iter().find(|fact| covers(fact, &asked)).copied().unwrap_or(asked)
354}
355
356/// The value an address was computed from, and how far past it the address is.
357///
358/// A `ptr_add` over a constant is walked through, and anything else is where the answer stops. The
359/// arithmetic here is exact because it is done in `i128` over offsets that came out of the IR as
360/// sixty four bit constants, and whether it is small enough to mean anything at sixty four bits is
361/// the rule's question rather than this function's.
362fn normal(func: &Func, value: Value) -> (Value, i128) {
363 let mut base = value;
364 let mut offset: i128 = 0;
365 while let Some((from, step)) = walked(func, base) {
366 let Some(sum) = offset.checked_add(step) else { break };
367 base = from;
368 offset = sum;
369 }
370 (base, offset)
371}
372
373/// The pointer one `ptr_add` over a constant was computed from, and by how much.
374fn walked(func: &Func, value: Value) -> Option<(Value, i128)> {
375 let from = operand_of(func, value, Opcode::PtrAdd, 0)?;
376 let by = operand_of(func, value, Opcode::PtrAdd, 1)?;
377 Some((from, constant(func, by)?))
378}
379
380/// Operand `index` of the instruction that produced `value`, when that instruction is `opcode`.
381fn operand_of(func: &Func, value: Value, opcode: Opcode, index: usize) -> Option<Value> {
382 let Def::Result { inst, .. } = func[value].def else { return None };
383 if func[inst].opcode != opcode {
384 return None;
385 }
386 func[func[inst].args].get(index).copied()
387}
388
389/// The value of an integer constant, read with its own sign.
390fn constant(func: &Func, value: Value) -> Option<i128> {
391 let Def::Result { inst, .. } = func[value].def else { return None };
392 if func[inst].opcode != Opcode::IConst {
393 return None;
394 }
395 let Extra::Imm(imm) = func[inst].extra else { return None };
396 let ty = func[value].ty;
397 ty.is_int().then(|| func[imm].signed(ty))
398}
399
400/// Whether an established fact answers the check being asked about.
401///
402/// This function decides nothing. It puts the two together into the term the rule file is written
403/// about and asks the table, which is the whole of section 7.7's split: the paragraph above worked
404/// out that the two addresses are one value a constant apart, and whether that is enough is
405/// somebody's proof rather than this file's opinion.
406fn covers(fact: &Fact, asked: &Fact) -> bool {
407 if fact.base != asked.base {
408 return false;
409 }
410 let Some(delta) = asked.offset.checked_sub(fact.offset) else { return false };
411 let mut question = Question::default();
412 let at = question.opaque();
413 let at = question.app("value.i64", &[at]);
414 let span = question.number(fact.size);
415 let span = question.app("iconst.i64", &[span]);
416 let far = question.number(delta);
417 let far = question.app("iconst.i64", &[far]);
418 let reach = question.number(asked.size);
419 let reach = question.app("iconst.i64", &[reach]);
420 let term = question.app("covered.i64", &[at, span, far, reach]);
421 match safety::TABLE.find(&question, term) {
422 Some(found) => yes(&safety::TABLE, found.rule),
423 None => false,
424 }
425}
426
427/// Whether the rule that fired answers yes.
428///
429/// A discharge rule replaces the question with a constant, and one is yes. Every rule in the file
430/// answers that today, and reading it off the rule rather than assuming it is what keeps this
431/// honest on the day one of them answers something else.
432fn yes(table: &Table, rule: usize) -> bool {
433 matches!(table.rules[rule].replacement, [Piece::App { .. }, Piece::Int(1)])
434}
435
436/// A term built to be asked about, and nothing else.
437///
438/// The rules are matched against this rather than against the function, because what is being asked
439/// about is not in the function: it is what the walk worked out about two of its instructions. So
440/// the subject is a small arena of exactly the term being asked, built fresh for each question and
441/// thrown away with the answer.
442#[derive(Debug, Default)]
443struct Question {
444 held: Vec<Held>,
445}
446
447/// One node of that term.
448#[derive(Debug)]
449enum Held {
450 /// A number the pattern can read and a guard can be about.
451 Int(i128),
452 /// A head and its arguments.
453 App(&'static str, Vec<usize>),
454 /// Something with no structure, which is how an address the rule only names is written.
455 Opaque,
456}
457
458impl Question {
459 /// Adds a constant and gives back where it went.
460 ///
461 /// Named for what it adds rather than for what it holds, because the arena also answers
462 /// [`Subject::int`] and one name for the two would read as though building a term and asking
463 /// about one were the same act.
464 fn number(&mut self, value: i128) -> usize {
465 self.held.push(Held::Int(value));
466 self.held.len() - 1
467 }
468
469 /// Adds an application of `head` to what is already in the arena.
470 fn app(&mut self, head: &'static str, args: &[usize]) -> usize {
471 self.held.push(Held::App(head, args.to_vec()));
472 self.held.len() - 1
473 }
474
475 /// Adds something the rule can bind and cannot look inside.
476 fn opaque(&mut self) -> usize {
477 self.held.push(Held::Opaque);
478 self.held.len() - 1
479 }
480}
481
482impl Subject for Question {
483 type Node = usize;
484
485 fn head(&self, node: usize) -> Option<(&str, usize)> {
486 match &self.held[node] {
487 Held::App(head, args) => Some((head, args.len())),
488 Held::Int(_) | Held::Opaque => None,
489 }
490 }
491
492 fn arg(&self, node: usize, index: usize) -> usize {
493 match &self.held[node] {
494 Held::App(_, args) => args[index],
495 // The walk only asks for an argument `head` said was there, so this is unreachable
496 // rather than a case with an answer.
497 Held::Int(_) | Held::Opaque => unreachable!("only an application has arguments"),
498 }
499 }
500
501 fn int(&self, node: usize) -> Option<i128> {
502 match self.held[node] {
503 Held::Int(value) => Some(value),
504 Held::App(..) | Held::Opaque => None,
505 }
506 }
507
508 fn same(&self, a: usize, b: usize) -> bool {
509 // Every node of a question is written once, so two places holding one thing are one place.
510 a == b
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use rucc_base::Interner;
517 use rucc_ir::{
518 AsmInfo, Block, BlockCallList, Builder, Extra, Flags, Func, InstData, MemInfo, MemOrder,
519 Opcode, Restrict, Signature, Type, Value,
520 };
521
522 use super::{Discharge, Fact};
523 use crate::stats::Kind;
524 use crate::{Analyses, Fuel, Pass};
525
526 /// A function taking a pointer, with one block, ready to have accesses put in it.
527 fn blank() -> (Interner, Func, Block, Value) {
528 let mut names = Interner::new();
529 let name = names.intern("f");
530 let mut func = Func::new(name, Signature::new().with_params(&[Type::PTR]));
531 let block = func.create_block();
532 let pointer = func.append_param(block, Type::PTR);
533 (names, func, block, pointer)
534 }
535
536 /// Puts `cap_of` and a `check_bounds` over `size` bytes at `pointer` into a block.
537 ///
538 /// The same shape `rucc-safety` emits, written out here rather than reached for, because
539 /// `rucc-opt` is rank 9 alongside `rucc-safety` and cannot depend on it.
540 fn check(build: &mut Builder<'_>, pointer: Value, size: u64) {
541 let args = build.func().push_values(&[pointer]);
542 let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
543 let info = MemInfo {
544 size,
545 align: 1,
546 order: MemOrder::NotAtomic,
547 tbaa: None,
548 restrict: Restrict::NONE,
549 };
550 let args = build.func().push_values(&[capability, pointer]);
551 let extra = Extra::Mem(build.func().add_mem(info));
552 build.inst(InstData { args, extra, ..InstData::new(Opcode::CheckBounds) }, &[]);
553 }
554
555 /// Puts `cap_of` and a `check_live` at `pointer` into a block.
556 ///
557 /// `rucc-safety` emits this straight after the bounds check for the same access and shares the
558 /// one `cap_of` between the two. Sharing it is not what the pass reads, so the tests build a
559 /// second one, which is the harder shape for it to accept.
560 fn live(build: &mut Builder<'_>, pointer: Value) {
561 let args = build.func().push_values(&[pointer]);
562 let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
563 let args = build.func().push_values(&[capability, pointer]);
564 build.inst(InstData { args, ..InstData::new(Opcode::CheckLive) }, &[]);
565 }
566
567 /// Both checks in front of one access, in the order `rucc-safety` writes them.
568 fn access(build: &mut Builder<'_>, pointer: Value, size: u64) {
569 check(build, pointer, size);
570 live(build, pointer);
571 }
572
573 /// A pointer `bytes` past another one.
574 fn past(build: &mut Builder<'_>, pointer: Value, bytes: i128) -> Value {
575 let offset = build.iconst(Type::int(64), bytes);
576 let args = build.func().push_values(&[pointer, offset]);
577 build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR)
578 }
579
580 /// How many checks are left in a function.
581 fn checks(func: &Func) -> usize {
582 func.blocks()
583 .flat_map(|block| func.insts(block).collect::<Vec<_>>())
584 .filter(|&inst| func[inst].opcode == Opcode::CheckBounds)
585 .count()
586 }
587
588 /// How many lifetime checks are left in a function.
589 fn lives(func: &Func) -> usize {
590 func.blocks()
591 .flat_map(|block| func.insts(block).collect::<Vec<_>>())
592 .filter(|&inst| func[inst].opcode == Opcode::CheckLive)
593 .count()
594 }
595
596 fn run(func: &mut Func) -> crate::Stats {
597 Discharge.run(func, &mut Analyses::new(), &mut Fuel::unlimited())
598 }
599
600 #[test]
601 fn a_second_check_of_the_same_bytes_goes() {
602 let (_, mut func, block, pointer) = blank();
603 let mut build = Builder::new(&mut func, block);
604 check(&mut build, pointer, 4);
605 check(&mut build, pointer, 4);
606 build.ret(&[]);
607 let stats = run(&mut func);
608 assert_eq!(checks(&func), 1);
609 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
610 }
611
612 #[test]
613 fn a_check_of_bytes_inside_a_checked_range_goes() {
614 // Four bytes at offset four, inside sixteen bytes at offset zero. This is the shape the
615 // whole pass is for: a struct whose fields are read one after another through one pointer.
616 let (_, mut func, block, pointer) = blank();
617 let mut build = Builder::new(&mut func, block);
618 check(&mut build, pointer, 16);
619 let field = past(&mut build, pointer, 4);
620 check(&mut build, field, 4);
621 build.ret(&[]);
622 run(&mut func);
623 assert_eq!(checks(&func), 1);
624 }
625
626 #[test]
627 fn a_check_of_bytes_past_the_end_of_a_checked_range_stays() {
628 // Four bytes at offset fourteen is two bytes past the end of the sixteen that were
629 // checked, and those two bytes are what the check is for.
630 let (_, mut func, block, pointer) = blank();
631 let mut build = Builder::new(&mut func, block);
632 check(&mut build, pointer, 16);
633 let over = past(&mut build, pointer, 14);
634 check(&mut build, over, 4);
635 build.ret(&[]);
636 assert!(!run(&mut func).changed());
637 assert_eq!(checks(&func), 2);
638 }
639
640 #[test]
641 fn a_check_of_bytes_before_a_checked_range_stays() {
642 // The guard's `delta` is not negative, and this is why. A read four bytes below what was
643 // checked is a read of somebody else's memory, and it is the bug the check exists for.
644 let (_, mut func, block, pointer) = blank();
645 let mut build = Builder::new(&mut func, block);
646 check(&mut build, pointer, 16);
647 let under = past(&mut build, pointer, -4);
648 check(&mut build, under, 4);
649 build.ret(&[]);
650 assert!(!run(&mut func).changed());
651 assert_eq!(checks(&func), 2);
652 }
653
654 #[test]
655 fn a_check_through_a_pointer_nothing_relates_to_the_first_stays() {
656 let mut names = Interner::new();
657 let name = names.intern("two");
658 let mut func = Func::new(name, Signature::new().with_params(&[Type::PTR, Type::PTR]));
659 let block = func.create_block();
660 let one = func.append_param(block, Type::PTR);
661 let other = func.append_param(block, Type::PTR);
662 let mut build = Builder::new(&mut func, block);
663 check(&mut build, one, 16);
664 check(&mut build, other, 4);
665 build.ret(&[]);
666 assert!(!run(&mut func).changed());
667 assert_eq!(checks(&func), 2);
668 }
669
670 #[test]
671 fn a_check_a_call_stands_between_stays_and_is_counted() {
672 // The conservatism the module comment argues for, and the number that says what it costs.
673 let (mut names, mut func, block, pointer) = blank();
674 let mut build = Builder::new(&mut func, block);
675 check(&mut build, pointer, 16);
676 let callee = names.intern("might_free");
677 let signature = build.func().add_signature(Signature::new());
678 build.call(callee, signature, &[]);
679 check(&mut build, pointer, 4);
680 build.ret(&[]);
681 let stats = run(&mut func);
682 assert!(!stats.changed());
683 assert_eq!(checks(&func), 2);
684 assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL), 1);
685 }
686
687 #[test]
688 fn a_check_a_call_that_cannot_free_stands_between_goes() {
689 // The other side of the paragraph above. The summary said this call reaches nothing that
690 // ends a lifetime, so the range the first check established is still one range.
691 let (mut names, mut func, block, pointer) = blank();
692 let mut build = Builder::new(&mut func, block);
693 check(&mut build, pointer, 16);
694 let callee = names.intern("counts_them");
695 let signature = build.func().add_signature(Signature::new());
696 let call = build.call(callee, signature, &[]);
697 check(&mut build, pointer, 4);
698 build.ret(&[]);
699 func[call].flags |= Flags::NOFREE;
700 let stats = run(&mut func);
701 assert_eq!(checks(&func), 1);
702 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
703 assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL), 0);
704 }
705
706 #[test]
707 fn inline_assembly_throws_the_facts_away_whatever_it_is_flagged() {
708 // There is no flag that would make this safe. The template is text the compiler does not
709 // read, so nothing worked anything out about what it reaches.
710 let (mut names, mut func, block, pointer) = blank();
711 let mut build = Builder::new(&mut func, block);
712 check(&mut build, pointer, 16);
713 build.inline_asm(
714 AsmInfo {
715 template: names.intern("nop"),
716 constraints: names.intern(""),
717 clobbers: names.intern(""),
718 targets: BlockCallList::EMPTY,
719 },
720 &[],
721 &[],
722 Flags::NONE,
723 );
724 check(&mut build, pointer, 4);
725 build.ret(&[]);
726 let stats = run(&mut func);
727 assert!(!stats.changed());
728 assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL), 1);
729 }
730
731 #[test]
732 fn a_check_that_only_one_path_covers_stays() {
733 // The dominator tree is what makes this right. The check in the arm covers the one in the
734 // join on one path and not on the other, and a check that goes has to be one that ran.
735 let (_, mut func, block, pointer) = blank();
736 let arm = func.create_block();
737 let join = func.create_block();
738 let mut build = Builder::new(&mut func, block);
739 let condition = build.iconst(Type::int(32), 1);
740 build.br_if(condition, arm, &[], join, &[]);
741 let mut build = Builder::new(&mut func, arm);
742 check(&mut build, pointer, 16);
743 build.jump(join, &[]);
744 let mut build = Builder::new(&mut func, join);
745 check(&mut build, pointer, 4);
746 build.ret(&[]);
747 assert!(!run(&mut func).changed());
748 assert_eq!(checks(&func), 2);
749 }
750
751 #[test]
752 fn a_check_a_dominating_block_covers_goes() {
753 let (_, mut func, block, pointer) = blank();
754 let after = func.create_block();
755 let mut build = Builder::new(&mut func, block);
756 check(&mut build, pointer, 16);
757 build.jump(after, &[]);
758 let mut build = Builder::new(&mut func, after);
759 let field = past(&mut build, pointer, 8);
760 check(&mut build, field, 8);
761 build.ret(&[]);
762 run(&mut func);
763 assert_eq!(checks(&func), 1);
764 }
765
766 #[test]
767 fn fuel_stops_the_removing_and_not_the_looking() {
768 let (_, mut func, block, pointer) = blank();
769 let mut build = Builder::new(&mut func, block);
770 check(&mut build, pointer, 4);
771 check(&mut build, pointer, 4);
772 check(&mut build, pointer, 4);
773 build.ret(&[]);
774 let mut fuel = Fuel::of(1);
775 let stats = Discharge.run(&mut func, &mut Analyses::new(), &mut fuel);
776 assert_eq!(checks(&func), 2);
777 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
778 assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
779 }
780
781 #[test]
782 fn a_second_lifetime_check_of_the_same_address_goes() {
783 // The narrow fact on its own, with no range around it to widen into.
784 let (_, mut func, block, pointer) = blank();
785 let mut build = Builder::new(&mut func, block);
786 live(&mut build, pointer);
787 live(&mut build, pointer);
788 build.ret(&[]);
789 let stats = run(&mut func);
790 assert_eq!(lives(&func), 1);
791 assert_eq!(stats.count(Kind::Optimized, super::REMOVED_LIVE), 1);
792 }
793
794 #[test]
795 fn a_lifetime_check_inside_a_checked_range_goes() {
796 // The shape the pass is for, with both halves of it. Sixteen bytes are checked and found
797 // alive, then a field four bytes in is read, and neither check in front of it survives.
798 let (_, mut func, block, pointer) = blank();
799 let mut build = Builder::new(&mut func, block);
800 access(&mut build, pointer, 16);
801 let field = past(&mut build, pointer, 4);
802 access(&mut build, field, 4);
803 build.ret(&[]);
804 let stats = run(&mut func);
805 assert_eq!(checks(&func), 1);
806 assert_eq!(lives(&func), 1);
807 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
808 assert_eq!(stats.count(Kind::Optimized, super::REMOVED_LIVE), 1);
809 }
810
811 #[test]
812 fn a_lifetime_check_outside_every_checked_range_stays() {
813 // Four bytes at offset twenty are past the sixteen that were checked, so nothing says the
814 // address is in the instance that was found alive, and it might be in no instance at all.
815 let (_, mut func, block, pointer) = blank();
816 let mut build = Builder::new(&mut func, block);
817 access(&mut build, pointer, 16);
818 let over = past(&mut build, pointer, 20);
819 live(&mut build, over);
820 build.ret(&[]);
821 assert!(!run(&mut func).changed());
822 assert_eq!(lives(&func), 2);
823 }
824
825 #[test]
826 fn a_lifetime_check_with_no_range_around_it_does_not_widen() {
827 // Without the bounds check the first lifetime check speaks only for its own address, so
828 // the one four bytes along is a different question and stays.
829 let (_, mut func, block, pointer) = blank();
830 let mut build = Builder::new(&mut func, block);
831 live(&mut build, pointer);
832 let field = past(&mut build, pointer, 4);
833 live(&mut build, field);
834 build.ret(&[]);
835 assert!(!run(&mut func).changed());
836 assert_eq!(lives(&func), 2);
837 }
838
839 #[test]
840 fn a_lifetime_check_a_call_stands_between_stays_and_is_counted() {
841 // Section 8.8's number. This is the one the summaries were written for.
842 let (mut names, mut func, block, pointer) = blank();
843 let mut build = Builder::new(&mut func, block);
844 access(&mut build, pointer, 16);
845 let callee = names.intern("might_free");
846 let signature = build.func().add_signature(Signature::new());
847 build.call(callee, signature, &[]);
848 let field = past(&mut build, pointer, 4);
849 live(&mut build, field);
850 build.ret(&[]);
851 let stats = run(&mut func);
852 assert!(!stats.changed());
853 assert_eq!(lives(&func), 2);
854 assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL_LIVE), 1);
855 }
856
857 #[test]
858 fn a_lifetime_check_a_call_that_cannot_free_stands_between_goes() {
859 let (mut names, mut func, block, pointer) = blank();
860 let mut build = Builder::new(&mut func, block);
861 access(&mut build, pointer, 16);
862 let callee = names.intern("counts_them");
863 let signature = build.func().add_signature(Signature::new());
864 let call = build.call(callee, signature, &[]);
865 let field = past(&mut build, pointer, 4);
866 live(&mut build, field);
867 build.ret(&[]);
868 func[call].flags |= Flags::NOFREE;
869 let stats = run(&mut func);
870 assert_eq!(lives(&func), 1);
871 assert_eq!(stats.count(Kind::Optimized, super::REMOVED_LIVE), 1);
872 }
873
874 #[test]
875 fn ending_a_lifetime_throws_the_facts_away() {
876 // Nothing emits `meta_end` yet, so this is the test that says what will happen when
877 // something does, rather than a test of anything the compiler does today.
878 let (_, mut func, block, pointer) = blank();
879 let mut build = Builder::new(&mut func, block);
880 access(&mut build, pointer, 16);
881 let size = build.iconst(Type::int(64), 16);
882 let args = build.func().push_values(&[pointer, size]);
883 build.inst(InstData { args, ..InstData::new(Opcode::MetaEnd) }, &[]);
884 access(&mut build, pointer, 16);
885 build.ret(&[]);
886 let stats = run(&mut func);
887 assert!(!stats.changed());
888 assert_eq!(checks(&func), 2);
889 assert_eq!(lives(&func), 2);
890 assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL), 1);
891 assert_eq!(stats.count(Kind::Missed, super::PAST_A_CALL_LIVE), 1);
892 }
893
894 #[test]
895 fn fuel_runs_out_over_both_kinds_of_check() {
896 let (_, mut func, block, pointer) = blank();
897 let mut build = Builder::new(&mut func, block);
898 access(&mut build, pointer, 16);
899 access(&mut build, pointer, 4);
900 build.ret(&[]);
901 let mut fuel = Fuel::of(1);
902 let stats = Discharge.run(&mut func, &mut Analyses::new(), &mut fuel);
903 assert_eq!(checks(&func), 1);
904 assert_eq!(lives(&func), 2);
905 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
906 assert_eq!(stats.count(Kind::Missed, super::NO_FUEL_LIVE), 1);
907 }
908
909 #[test]
910 fn a_distance_too_large_to_be_a_real_access_is_not_discharged() {
911 // The guard's bound. The two readings of the arithmetic agree while the numbers stay
912 // small, so a rule proved at sixty four bits is not asked about anything else. Nothing
913 // here is wrong, it simply is not proved, and a check that is not proved to be unnecessary
914 // stays.
915 let huge = i128::from(u64::MAX) * 4;
916 let fact = Fact { base: Value::new(0), offset: 0, size: huge };
917 let asked = Fact { base: Value::new(0), offset: huge / 2, size: 4 };
918 assert!(!super::covers(&fact, &asked));
919 }
920}