Skip to main content

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