Conversation
… block in append_with()
…rified doc comments
a5a105e to
a7fa7dd
Compare
| // SAFETY: We read in self_val's and hand it over to our conflict function | ||
| // which will always return a value that we can use to overwrite what's | ||
| // in self_val | ||
| unsafe { | ||
| let val = ptr::read(self_val); | ||
| let next_val = (conflict)(self_key, val, first_other_val); | ||
| ptr::write(self_val, next_val); | ||
| } |
There was a problem hiding this comment.
this should really be a method of Cursor*, you need a handler for when conflict panics that removes the entry without dropping whatever you ptr::read from.
something like:
impl<K, V> CursorMut<'a, K, V> {
/// call `f` with the next entry's key and value, replacing the next entry's value with the returned value. if `f` unwinds, the next entry is removed.
/// equivalent to a more efficient version of:
/// ```rust
/// if let Some((k, v)) = self.remove_next() {
/// let v = f(&k, v);
/// // Safety: key is unmodified
/// unsafe { self.insert_after_unchecked(k, v) };
/// }
/// ```
pub(super) fn with_next(&mut self, f: impl FnOnce(&K, V) -> V) {
struct RemoveNextOnDrop<'a, 'b, K, V> {
cursor: &'a mut CursorMut<'b, K, V>,
forget_next: bool,
}
impl<K, V> Drop for RemoveNextOnDrop<'_, '_, K, V> {
fn drop(&mut self) {
if self.forget_next {
// call an equivalent to CursorMut::remove_next()
// except that instead of returning `V`, it never moves or drops it.
self.0.forget_next_value();
}
}
}
let mut remove_next_on_drop = RemoveNextOnDrop {
cursor: self,
forget_next: false, // we don't know that we have a next value yet
};
if let Some((k, v_mut)) = remove_next_on_drop.cursor.peek_next() {
remove_next_on_drop.forget_next = true;
// Safety: we move the V out of the next entry,
// we marked the entry's value to be forgotten
// when remove_next_on_drop is dropped that
// way we avoid returning to the caller leaving
// a moved-out invalid value if `f` unwinds.
let v = unsafe { std::ptr::read(v_mut) };
let v = f(k, v);
// Safety: move the V back into the next entry
unsafe { std::ptr::write(v_mut, v) };
remove_next_on_drop.forget_next = false;
}
}
}The equivalent CursorMutKey method should instead have f be impl FnOnce(K, V) -> (K, V) and needs to forget both the key and value since they were both ptr::read, and not just the value.
|
Appreciate your patience and feedback in all of this @programmerjake! |
…ehavior for conflict func
|
@programmerjake Took your suggestion on Also, I've been wondering if |
| let mut emptied_internal_root = false; | ||
| if let Ok(next_kv) = current.next_kv() { | ||
| let ((_, val), pos) = | ||
| next_kv.remove_kv_tracking(|| emptied_internal_root = true, self.alloc.clone()); |
There was a problem hiding this comment.
remove_kv_tracking may panic and drop the key and value, tbh this is very complicated and makes me think it could be better for with_next() instead of calling forget_next[_key_and]_value, to transmute the cursor reference type from &mut CursorMut<K, V> to &mut CursorMut<K, ManuallyDrop<V>> and then just call remove_next. that said, idk if that would be sound, asking on Zulip: #t-libs > could we use transmute tricks in the impl of BTreeMap? @ 💬
There was a problem hiding this comment.
True, I think transmute should be fine since &mut CursorMut<K, V> and &mut CursorMut<K, ManuallyDrop<V>> are the same size and it forgets the original, so we wouldn't be running the destructor on the original. (but of course I can't hold myself to that since I haven't played around with mem::transmute much myself)
There was a problem hiding this comment.
it might differ with -Zrandomize-layout or other future struct layout changes. I haven't reviewed the whole BTreeMap implementation to see what layout guarantees it has.
|
Reminder, once the PR becomes ready for a review, use |
6d826f5 to
97f5547
Compare
|
@programmerjake Made the change, lmk what you think! |
b26e4e1 to
6c436c1
Compare
…nstead of drop handling, refactored code a bit, and updated comments
6c436c1 to
9b423c5
Compare
… searching through tree
|
The tests I took verbatim from One thing I'll point out is that interestingly this #[test]
#[cfg_attr(not(panic = "unwind"), ignore = "test requires unwinding support")]
fn test_append_drop_leak() {
let a = CrashTestDummy::new(0);
let b = CrashTestDummy::new(1);
let c = CrashTestDummy::new(2);
let mut left = BTreeMap::new();
let mut right = BTreeMap::new();
left.insert(a.spawn(Panic::Never), ());
left.insert(b.spawn(Panic::Never), ());
left.insert(c.spawn(Panic::Never), ());
right.insert(b.spawn(Panic::InDrop), ()); // first duplicate key, dropped during append
right.insert(c.spawn(Panic::Never), ());
catch_unwind(move || left.append(&mut right)).unwrap_err();
assert_eq!(a.dropped(), 1);
assert_eq!(b.dropped(), 1); // should be 2 were it not for Rust issue #47949
assert_eq!(c.dropped(), 2);
}When I ported a similar version of this for #[test]
#[cfg_attr(not(panic = "unwind"), ignore = "test requires unwinding support")]
fn test_merge_drop_leak() {
let a = CrashTestDummy::new(0);
let b = CrashTestDummy::new(1);
let c = CrashTestDummy::new(2);
let mut left = BTreeMap::new();
let mut right = BTreeMap::new();
left.insert(a.spawn(Panic::Never), ());
left.insert(b.spawn(Panic::Never), ());
left.insert(c.spawn(Panic::Never), ());
right.insert(b.spawn(Panic::InDrop), ()); // first duplicate key, dropped during append
right.insert(c.spawn(Panic::Never), ());
catch_unwind(move || left.merge(right, |_, _, _| ())).unwrap_err();
assert_eq!(a.dropped(), 1);
assert_eq!(b.dropped(), 2);
assert_eq!(c.dropped(), 2);
}The |
|
I added a panic test within conflict, but I think due to #47949, it isn't incrementing the counter for |
…ge_test macro was doing
|
r? @jhpratt rustbot has assigned @jhpratt. Use Why was this reviewer chosen?The reviewer was selected based on:
|
This is an optimized version of #151981. See ACP for more information on
BTreeMap::mergedoes.CC @programmerjake. Let me know what you think of how I'm using
CursorMutandIntoIterhere and whether the unsafe code here looks good. I decided to useptr::read()andptr::write()to grab the value fromCursorMutasVthan&mut V, use it within theconflictfunction, and overwrite the content of conflicting key afterward.I know this needs some polishing, especially with refactoring some redundant looking code in a nicer way, some of which could probably just be public API methods for
CursorMut. It does pass all the tests that I currently have forBTreeMap::merge(inspired fromBTreeMap::append) though, so that's good.