TB: tree traversal
This commit is contained in:
parent
cd954dbf14
commit
eb3ff3ccb0
@ -115,6 +115,151 @@ pub(super) struct Node {
|
|||||||
pub debug_info: NodeDebugInfo,
|
pub debug_info: NodeDebugInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Data given to the transition function
|
||||||
|
struct NodeAppArgs<'node> {
|
||||||
|
/// Node on which the transition is currently being applied
|
||||||
|
node: &'node Node,
|
||||||
|
/// Mutable access to its permissions
|
||||||
|
perm: UniEntry<'node, LocationState>,
|
||||||
|
/// Relative position of the access
|
||||||
|
rel_pos: AccessRelatedness,
|
||||||
|
}
|
||||||
|
/// Data given to the error handler
|
||||||
|
struct ErrHandlerArgs<'node, InErr> {
|
||||||
|
/// Kind of error that occurred
|
||||||
|
error_kind: InErr,
|
||||||
|
/// Tag that triggered the error (not the tag that was accessed,
|
||||||
|
/// rather the parent tag that had insufficient permissions or the
|
||||||
|
/// non-parent tag that had a protector).
|
||||||
|
faulty_tag: &'node NodeDebugInfo,
|
||||||
|
}
|
||||||
|
/// Internal contents of `Tree` with the minimum of mutable access for
|
||||||
|
/// the purposes of the tree traversal functions: the permissions (`perms`) can be
|
||||||
|
/// updated but not the tree structure (`tag_mapping` and `nodes`)
|
||||||
|
struct TreeVisitor<'tree> {
|
||||||
|
tag_mapping: &'tree UniKeyMap<BorTag>,
|
||||||
|
nodes: &'tree UniValMap<Node>,
|
||||||
|
perms: &'tree mut UniValMap<LocationState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to continue exploring the children recursively or not.
|
||||||
|
enum ContinueTraversal {
|
||||||
|
Recurse,
|
||||||
|
SkipChildren,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'tree> TreeVisitor<'tree> {
|
||||||
|
// Applies `f_propagate` to every vertex of the tree top-down in the following order: first
|
||||||
|
// all ancestors of `start`, then `start` itself, then children of `start`, then the rest.
|
||||||
|
// This ensures that errors are triggered in the following order
|
||||||
|
// - first invalid accesses with insufficient permissions, closest to the root first,
|
||||||
|
// - then protector violations, closest to `start` first.
|
||||||
|
//
|
||||||
|
// `f_propagate` should follow the following format: for a given `Node` it updates its
|
||||||
|
// `Permission` depending on the position relative to `start` (given by an
|
||||||
|
// `AccessRelatedness`).
|
||||||
|
// It outputs whether the tree traversal for this subree should continue or not.
|
||||||
|
fn traverse_parents_this_children_others<InnErr, OutErr>(
|
||||||
|
mut self,
|
||||||
|
start: BorTag,
|
||||||
|
f_propagate: impl Fn(NodeAppArgs<'_>) -> Result<ContinueTraversal, InnErr>,
|
||||||
|
err_builder: impl Fn(ErrHandlerArgs<'_, InnErr>) -> OutErr,
|
||||||
|
) -> Result<(), OutErr>
|
||||||
|
where {
|
||||||
|
struct TreeVisitAux<NodeApp, ErrHandler> {
|
||||||
|
f_propagate: NodeApp,
|
||||||
|
err_builder: ErrHandler,
|
||||||
|
stack: Vec<(UniIndex, AccessRelatedness)>,
|
||||||
|
}
|
||||||
|
impl<NodeApp, InnErr, OutErr, ErrHandler> TreeVisitAux<NodeApp, ErrHandler>
|
||||||
|
where
|
||||||
|
NodeApp: Fn(NodeAppArgs<'_>) -> Result<ContinueTraversal, InnErr>,
|
||||||
|
ErrHandler: Fn(ErrHandlerArgs<'_, InnErr>) -> OutErr,
|
||||||
|
{
|
||||||
|
fn pop(&mut self) -> Option<(UniIndex, AccessRelatedness)> {
|
||||||
|
self.stack.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the function to the current `tag`, and push its children
|
||||||
|
/// to the stack of future tags to visit.
|
||||||
|
fn exec_and_visit(
|
||||||
|
&mut self,
|
||||||
|
this: &mut TreeVisitor<'_>,
|
||||||
|
tag: UniIndex,
|
||||||
|
exclude: Option<UniIndex>,
|
||||||
|
rel_pos: AccessRelatedness,
|
||||||
|
) -> Result<(), OutErr> {
|
||||||
|
// 1. apply the propagation function
|
||||||
|
let node = this.nodes.get(tag).unwrap();
|
||||||
|
let recurse =
|
||||||
|
(self.f_propagate)(NodeAppArgs { node, perm: this.perms.entry(tag), rel_pos })
|
||||||
|
.map_err(|error_kind| {
|
||||||
|
(self.err_builder)(ErrHandlerArgs {
|
||||||
|
error_kind,
|
||||||
|
faulty_tag: &node.debug_info,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
// 2. add the children to the stack for future traversal
|
||||||
|
if matches!(recurse, ContinueTraversal::Recurse) {
|
||||||
|
let child_rel = rel_pos.for_child();
|
||||||
|
for &child in node.children.iter() {
|
||||||
|
// some child might be excluded from here and handled separately
|
||||||
|
if Some(child) != exclude {
|
||||||
|
self.stack.push((child, child_rel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_idx = self.tag_mapping.get(&start).unwrap();
|
||||||
|
let mut stack = TreeVisitAux { f_propagate, err_builder, stack: Vec::new() };
|
||||||
|
{
|
||||||
|
let mut path_ascend = Vec::new();
|
||||||
|
// First climb to the root while recording the path
|
||||||
|
let mut curr = start_idx;
|
||||||
|
while let Some(ancestor) = self.nodes.get(curr).unwrap().parent {
|
||||||
|
path_ascend.push((ancestor, curr));
|
||||||
|
curr = ancestor;
|
||||||
|
}
|
||||||
|
// Then descend:
|
||||||
|
// - execute f_propagate on each node
|
||||||
|
// - record children in visit
|
||||||
|
while let Some((ancestor, next_in_path)) = path_ascend.pop() {
|
||||||
|
// Explore ancestors in descending order.
|
||||||
|
// `next_in_path` is excluded from the recursion because it
|
||||||
|
// will be the `ancestor` of the next iteration.
|
||||||
|
// It also needs a different `AccessRelatedness` than the other
|
||||||
|
// children of `ancestor`.
|
||||||
|
stack.exec_and_visit(
|
||||||
|
&mut self,
|
||||||
|
ancestor,
|
||||||
|
Some(next_in_path),
|
||||||
|
AccessRelatedness::StrictChildAccess,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// All (potentially zero) ancestors have been explored, call f_propagate on start
|
||||||
|
stack.exec_and_visit(&mut self, start_idx, None, AccessRelatedness::This)?;
|
||||||
|
// up to this point we have never popped from `stack`, hence if the
|
||||||
|
// path to the root is `root = p(n) <- p(n-1)... <- p(1) <- p(0) = start`
|
||||||
|
// then now `stack` contains
|
||||||
|
// `[<children(p(n)) except p(n-1)> ... <children(p(1)) except p(0)> <children(p(0))>]`,
|
||||||
|
// all of which are for now unexplored.
|
||||||
|
// This is the starting point of a standard DFS which will thus
|
||||||
|
// explore all non-ancestors of `start` in the following order:
|
||||||
|
// - all descendants of `start`;
|
||||||
|
// - then the unexplored descendants of `parent(start)`;
|
||||||
|
// ...
|
||||||
|
// - until finally the unexplored descendants of `root`.
|
||||||
|
while let Some((tag, rel_pos)) = stack.pop() {
|
||||||
|
stack.exec_and_visit(&mut self, tag, None, rel_pos)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Tree {
|
impl Tree {
|
||||||
/// Create a new tree, with only a root pointer.
|
/// Create a new tree, with only a root pointer.
|
||||||
pub fn new(root_tag: BorTag, size: Size) -> Self {
|
pub fn new(root_tag: BorTag, size: Size) -> Self {
|
||||||
@ -177,6 +322,200 @@ impl<'tcx> Tree {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deallocation requires
|
||||||
|
/// - a pointer that permits write accesses
|
||||||
|
/// - the absence of Strong Protectors anywhere in the allocation
|
||||||
|
pub fn dealloc(
|
||||||
|
&mut self,
|
||||||
|
tag: BorTag,
|
||||||
|
range: AllocRange,
|
||||||
|
global: &GlobalState,
|
||||||
|
) -> InterpResult<'tcx> {
|
||||||
|
self.perform_access(AccessKind::Write, tag, range, global)?;
|
||||||
|
let access_info = &self.nodes.get(self.tag_mapping.get(&tag).unwrap()).unwrap().debug_info;
|
||||||
|
for (_range, perms) in self.rperms.iter_mut(range.start, range.size) {
|
||||||
|
TreeVisitor { nodes: &self.nodes, tag_mapping: &self.tag_mapping, perms }
|
||||||
|
.traverse_parents_this_children_others(
|
||||||
|
tag,
|
||||||
|
|args: NodeAppArgs<'_>| -> Result<ContinueTraversal, TransitionError> {
|
||||||
|
let NodeAppArgs { node, .. } = args;
|
||||||
|
if global.borrow().protected_tags.get(&node.tag)
|
||||||
|
== Some(&ProtectorKind::StrongProtector)
|
||||||
|
{
|
||||||
|
Err(TransitionError::ProtectedDealloc)
|
||||||
|
} else {
|
||||||
|
Ok(ContinueTraversal::Recurse)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|args: ErrHandlerArgs<'_, TransitionError>| -> InterpErrorInfo<'tcx> {
|
||||||
|
let ErrHandlerArgs { error_kind, faulty_tag } = args;
|
||||||
|
TbError {
|
||||||
|
faulty_tag,
|
||||||
|
access_kind: AccessKind::Write,
|
||||||
|
error_kind,
|
||||||
|
tag_of_access: access_info,
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps the following propagation procedure to each range:
|
||||||
|
/// - initialize if needed;
|
||||||
|
/// - compute new state after transition;
|
||||||
|
/// - check that there is no protector that would forbid this;
|
||||||
|
/// - record this specific location as accessed.
|
||||||
|
pub fn perform_access(
|
||||||
|
&mut self,
|
||||||
|
access_kind: AccessKind,
|
||||||
|
tag: BorTag,
|
||||||
|
range: AllocRange,
|
||||||
|
global: &GlobalState,
|
||||||
|
) -> InterpResult<'tcx> {
|
||||||
|
let access_info = &self.nodes.get(self.tag_mapping.get(&tag).unwrap()).unwrap().debug_info;
|
||||||
|
for (_range, perms) in self.rperms.iter_mut(range.start, range.size) {
|
||||||
|
TreeVisitor { nodes: &self.nodes, tag_mapping: &self.tag_mapping, perms }
|
||||||
|
.traverse_parents_this_children_others(
|
||||||
|
tag,
|
||||||
|
|args: NodeAppArgs<'_>| -> Result<ContinueTraversal, TransitionError> {
|
||||||
|
let NodeAppArgs { node, mut perm, rel_pos } = args;
|
||||||
|
|
||||||
|
let old_state =
|
||||||
|
perm.or_insert_with(|| LocationState::new(node.default_initial_perm));
|
||||||
|
|
||||||
|
// Optimize the tree traversal.
|
||||||
|
// The optimization here consists of observing thanks to the tests
|
||||||
|
// `foreign_read_is_noop_after_write` and `all_transitions_idempotent`
|
||||||
|
// that if we apply twice in a row the effects of a foreign access
|
||||||
|
// we can skip some branches.
|
||||||
|
// "two foreign accesses in a row" occurs when `perm.latest_foreign_access` is `Some(_)`
|
||||||
|
// AND the `rel_pos` of the current access corresponds to a foreign access.
|
||||||
|
if rel_pos.is_foreign() {
|
||||||
|
let new_access_noop =
|
||||||
|
match (old_state.latest_foreign_access, access_kind) {
|
||||||
|
// Previously applied transition makes the new one a guaranteed
|
||||||
|
// noop in the two following cases:
|
||||||
|
// (1) justified by `foreign_read_is_noop_after_write`
|
||||||
|
(Some(AccessKind::Write), AccessKind::Read) => true,
|
||||||
|
// (2) justified by `all_transitions_idempotent`
|
||||||
|
(Some(old), new) if old == new => true,
|
||||||
|
// In all other cases there has been a recent enough
|
||||||
|
// child access that the effects of the new foreign access
|
||||||
|
// need to be applied to this subtree.
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if new_access_noop {
|
||||||
|
// Abort traversal if the new transition is indeed guaranteed
|
||||||
|
// to be noop.
|
||||||
|
return Ok(ContinueTraversal::SkipChildren);
|
||||||
|
} else {
|
||||||
|
// Otherwise propagate this time, and also record the
|
||||||
|
// access that just occurred so that we can skip the propagation
|
||||||
|
// next time.
|
||||||
|
old_state.latest_foreign_access = Some(access_kind);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// A child access occurred, this breaks the streak of "two foreign
|
||||||
|
// accesses in a row" and we reset this field.
|
||||||
|
old_state.latest_foreign_access = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_perm = old_state.permission;
|
||||||
|
let protected = global.borrow().protected_tags.contains_key(&node.tag);
|
||||||
|
let new_perm =
|
||||||
|
Permission::perform_access(access_kind, rel_pos, old_perm, protected)
|
||||||
|
.ok_or(TransitionError::ChildAccessForbidden(old_perm))?;
|
||||||
|
if protected
|
||||||
|
// Can't trigger Protector on uninitialized locations
|
||||||
|
&& old_state.initialized
|
||||||
|
&& !old_perm.protector_allows_transition(new_perm)
|
||||||
|
{
|
||||||
|
return Err(TransitionError::ProtectedTransition(old_perm, new_perm));
|
||||||
|
}
|
||||||
|
old_state.permission = new_perm;
|
||||||
|
old_state.initialized |= !rel_pos.is_foreign();
|
||||||
|
Ok(ContinueTraversal::Recurse)
|
||||||
|
},
|
||||||
|
|args: ErrHandlerArgs<'_, TransitionError>| -> InterpErrorInfo<'tcx> {
|
||||||
|
let ErrHandlerArgs { error_kind, faulty_tag } = args;
|
||||||
|
TbError { faulty_tag, access_kind, error_kind, tag_of_access: access_info }
|
||||||
|
.build()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Integration with the BorTag garbage collector
|
||||||
|
impl Tree {
|
||||||
|
pub fn remove_unreachable_tags(&mut self, live_tags: &FxHashSet<BorTag>) {
|
||||||
|
assert!(self.keep_only_needed(self.root, live_tags)); // root can't be removed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traverses the entire tree looking for useless tags.
|
||||||
|
/// Returns true iff the tag it was called on is still live or has live children,
|
||||||
|
/// and removes from the tree all tags that have no live children.
|
||||||
|
///
|
||||||
|
/// NOTE: This leaves in the middle of the tree tags that are unreachable but have
|
||||||
|
/// reachable children. There is a potential for compacting the tree by reassigning
|
||||||
|
/// children of dead tags to the nearest live parent, but it must be done with care
|
||||||
|
/// not to remove UB.
|
||||||
|
///
|
||||||
|
/// Example: Consider the tree `root - parent - child`, with `parent: Frozen` and
|
||||||
|
/// `child: Reserved`. This tree can exist. If we blindly delete `parent` and reassign
|
||||||
|
/// `child` to be a direct child of `root` then Writes to `child` are now permitted
|
||||||
|
/// whereas they were not when `parent` was still there.
|
||||||
|
fn keep_only_needed(&mut self, idx: UniIndex, live: &FxHashSet<BorTag>) -> bool {
|
||||||
|
let node = self.nodes.get(idx).unwrap();
|
||||||
|
// FIXME: this function does a lot of cloning, a 2-pass approach is possibly
|
||||||
|
// more efficient. It could consist of
|
||||||
|
// 1. traverse the Tree, collect all useless tags in a Vec
|
||||||
|
// 2. traverse the Vec, remove all tags previously selected
|
||||||
|
// Bench it.
|
||||||
|
let children: SmallVec<_> = node
|
||||||
|
.children
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|child| self.keep_only_needed(*child, live))
|
||||||
|
.collect();
|
||||||
|
let no_children = children.is_empty();
|
||||||
|
let node = self.nodes.get_mut(idx).unwrap();
|
||||||
|
node.children = children;
|
||||||
|
if !live.contains(&node.tag) && no_children {
|
||||||
|
// All of the children and this node are unreachable, delete this tag
|
||||||
|
// from the tree (the children have already been deleted by recursive
|
||||||
|
// calls).
|
||||||
|
// Due to the API of UniMap we must absolutely call
|
||||||
|
// `UniValMap::remove` for the key of this tag on *all* maps that used it
|
||||||
|
// (which are `self.nodes` and every range of `self.rperms`)
|
||||||
|
// before we can safely apply `UniValMap::forget` to truly remove
|
||||||
|
// the tag from the mapping.
|
||||||
|
let tag = node.tag;
|
||||||
|
self.nodes.remove(idx);
|
||||||
|
for perms in self.rperms.iter_mut_all() {
|
||||||
|
perms.remove(idx);
|
||||||
|
}
|
||||||
|
self.tag_mapping.remove(&tag);
|
||||||
|
// The tag has been deleted, inform the caller
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
// The tag is still live or has live children, it must be kept
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VisitTags for Tree {
|
||||||
|
fn visit_tags(&self, visit: &mut dyn FnMut(BorTag)) {
|
||||||
|
// To ensure that the root never gets removed, we visit it
|
||||||
|
// (the `root` node of `Tree` is not an `Option<_>`)
|
||||||
|
visit(self.nodes.get(self.root).unwrap().tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Relative position of the access
|
/// Relative position of the access
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum AccessRelatedness {
|
pub enum AccessRelatedness {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user