miri/borrow_tracker/tree_borrows/
foreign_access_skipping.rs

1use super::AccessKind;
2use super::tree::AccessRelatedness;
3
4/// To speed up tree traversals, we want to skip traversing subtrees when we know the traversal will have no effect.
5/// This is often the case for foreign accesses, since usually foreign accesses happen several times in a row, but also
6/// foreign accesses are idempotent. In particular, see tests `foreign_read_is_noop_after_foreign_write` and `all_transitions_idempotent`.
7/// Thus, for each node we keep track of the "strongest idempotent foreign access" (SIFA), i.e. which foreign access can be skipped.
8/// Note that for correctness, it is not required that this is the strongest access, just any access it is idempotent under. In particular, setting
9/// it to `None` is always correct, but the point of this optimization is to have it be as strong as possible so that more accesses can be skipped.
10/// This enum represents the kinds of values we store:
11/// - `None` means that the node (and its subtrees) are not (guaranteed to be) idempotent under any foreign access.
12/// - `Read` means that the node (and its subtrees) are idempotent under foreign reads, but not (yet / necessarily) under foreign writes.
13/// - `Write` means that the node (and its subtrees) are idempotent under foreign writes. This also implies that it is idempotent under foreign
14///   reads, since reads are stronger than writes (see test `foreign_read_is_noop_after_foreign_write`). In other words, this node can be skipped
15///   for all foreign accesses.
16///
17/// Since a traversal does not just visit a node, but instead the entire subtree, the SIFA field for a given node indicates that the access to
18/// *the entire subtree* rooted at that node can be skipped. In order for this to work, we maintain the global invariant that at
19/// each location, the SIFA at each child must be stronger than that at the parent. For normal reads and writes, this is easily accomplished by
20/// tracking each foreign access as it occurs, so that then the next access can be skipped. This also obviously maintains the invariant, because
21/// if a node undergoes a foreign access, then all its children also see this as a foreign access. However, the invariant is broken during retags,
22/// because retags act across the entire allocation, but only emit a read event across a specific range. This means that for all nodes outside that
23/// range, the invariant is potentially broken, since a new child with a weaker SIFA is inserted. Thus, during retags, special care is taken to
24/// "manually" reset the parent's SIFA to be at least as strong as the new child's. This is accomplished with the `ensure_no_stronger_than` method.
25///
26/// Note that we derive Ord and PartialOrd, so the order in which variants are listed below matters:
27/// None < Read < Write. Do not change that order. See the `test_order` test.
28#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
29pub enum IdempotentForeignAccess {
30    #[default]
31    None,
32    Read,
33    Write,
34}
35
36impl IdempotentForeignAccess {
37    /// Returns true if a node where the strongest idempotent foreign access is `self`
38    /// can skip the access `happening_next`. Note that if this returns
39    /// `true`, then the entire subtree will be skipped.
40    pub fn can_skip_foreign_access(self, happening_next: IdempotentForeignAccess) -> bool {
41        debug_assert!(happening_next.is_foreign());
42        // This ordering is correct. Intuitively, if the last access here was
43        // a foreign write, everything can be skipped, since after a foreign write,
44        // all further foreign accesses are idempotent
45        happening_next <= self
46    }
47
48    /// Updates `self` to account for a foreign access.
49    pub fn record_new(&mut self, just_happened: IdempotentForeignAccess) {
50        if just_happened.is_local() {
51            // If the access is local, reset it.
52            *self = IdempotentForeignAccess::None;
53        } else {
54            // Otherwise, keep it or stengthen it.
55            *self = just_happened.max(*self);
56        }
57    }
58
59    /// Returns true if this access is local.
60    pub fn is_local(self) -> bool {
61        matches!(self, IdempotentForeignAccess::None)
62    }
63
64    /// Returns true if this access is foreign, i.e. not local.
65    pub fn is_foreign(self) -> bool {
66        !self.is_local()
67    }
68
69    /// Constructs a foreign access from an `AccessKind`
70    pub fn from_foreign(acc: AccessKind) -> IdempotentForeignAccess {
71        match acc {
72            AccessKind::Read => Self::Read,
73            AccessKind::Write => Self::Write,
74        }
75    }
76
77    /// Usually, tree traversals have an `AccessKind` and an `AccessRelatedness`.
78    /// This methods converts these into the corresponding `IdempotentForeignAccess`, to be used
79    /// to e.g. invoke `can_skip_foreign_access`.
80    pub fn from_acc_and_rel(acc: AccessKind, rel: AccessRelatedness) -> IdempotentForeignAccess {
81        if rel.is_foreign() { Self::from_foreign(acc) } else { Self::None }
82    }
83
84    /// During retags, the SIFA needs to be weakened to account for children with weaker SIFAs being inserted.
85    /// Thus, this method is called from the bottom up on each parent, until it returns false, which means the
86    /// "children have stronger SIFAs" invariant is restored.
87    pub fn ensure_no_stronger_than(&mut self, strongest_allowed: IdempotentForeignAccess) -> bool {
88        if *self > strongest_allowed {
89            *self = strongest_allowed;
90            true
91        } else {
92            false
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::IdempotentForeignAccess;
100
101    #[test]
102    fn test_order() {
103        // The internal logic relies on this order.
104        // Do not change.
105        assert!(IdempotentForeignAccess::None < IdempotentForeignAccess::Read);
106        assert!(IdempotentForeignAccess::Read < IdempotentForeignAccess::Write);
107    }
108}