Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/btreemap_port/post_transpile_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3245,6 +3245,65 @@ def apply_step54_insert_path_fixes(path: Path) -> None:
print(f" no step-54 fix sites matched in: {path.name}")


def fix_split_leaf_data_owned_relocation(path: Path) -> None:
"""Move the split median out of the slots the old node abandons.

MaybeUninit::assume_init_read intentionally copies copy-constructible
owners. split_leaf_data then shortens the source node below the median,
so no later drop visits those slots. The consuming assume_init overload
performs the required relocation and destroys each moved-from source.
"""
src = path.read_text()
old = (
" auto k = rusty::deref_call(this->node, rusty::detail::__mdisp_key_area_mut{}, this->idx_field).assume_init_read();\n"
" auto v = rusty::deref_call(this->node, rusty::detail::__mdisp_val_area_mut{}, this->idx_field).assume_init_read();\n"
)
new = (
" // The median slots fall outside the shortened source-node length,\n"
" // so this is ownership transfer, not a copy. assume_init_read()\n"
" // deep-copies copyable owners and strands the originals here.\n"
" auto k = rusty::deref_call(this->node, rusty::detail::__mdisp_key_area_mut{}, this->idx_field).assume_init();\n"
" auto v = rusty::deref_call(this->node, rusty::detail::__mdisp_val_area_mut{}, this->idx_field).assume_init();\n"
)
if old in src:
path.write_text(src.replace(old, new, 1))
print(f" fixed split median ownership transfer in: {path.name}")
elif new in src:
print(f" no changes to: {path.name} (split median transfer already fixed)")
else:
raise RuntimeError(
f"split_leaf_data median extraction shape not found in {path}"
)


def fix_slice_remove_owned_relocation(path: Path) -> None:
"""Relocate the element whose slot slice_remove makes logically dead.

MaybeUninit::assume_init_read copies copy-constructible owners. The
following left shift and caller-side length decrement abandon the copied
source, so an Rc-like value retains an unreachable strong reference.
The consuming assume_init overload moves the value and destroys the
moved-from source before the slot is overwritten.
"""
src = path.read_text()
old = (
" auto ret = ((*rusty::ptr::add(slice_ptr, std::move(idx)))).assume_init_read();\n"
)
new = (
" // The removed slot becomes logically uninitialized when the remaining\n"
" // elements shift left. Relocate its owner instead of cloning copyable\n"
" // values: a clone would be stranded in the dead trailing slot.\n"
" auto ret = ((*rusty::ptr::add(slice_ptr, std::move(idx)))).assume_init();\n"
)
if old in src:
path.write_text(src.replace(old, new, 1))
print(f" fixed slice_remove ownership transfer in: {path.name}")
elif new in src:
print(f" no changes to: {path.name} (slice_remove transfer already fixed)")
else:
raise RuntimeError(f"slice_remove extraction shape not found in {path}")


def implement_handle_force(path: Path) -> None:
"""Hand-port `Handle::force` on `Handle<NodeRef<…, LeafOrInternal>, Type>`.
The transpiled body has the same shape as `Handle::descend` — emitted
Expand Down Expand Up @@ -7156,6 +7215,8 @@ def main() -> int:
# __NodeRefArgs, insert_fit/split/split_leaf_data simplifications,
# LeafNode::new_ via new_in, middle.split path correction).
apply_step54_insert_path_fixes(internal)
fix_split_leaf_data_owned_relocation(internal)
fix_slice_remove_owned_relocation(internal)
# Step 60: codify step 58/59 fixes — __IsNodeRef concept injection,
# InternalNode::new_ bypass, correct_parent_link arg recovery,
# .height → .height_field rewrites.
Expand Down
96 changes: 95 additions & 1 deletion tests/btree_tests_port_unstubbed.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
// Test name convention: `<rust_test_name>_unstubbed` so registration
// doesn't collide with the corresponding stub in btree_tests_port.cppm.

// libc++'s vector declarations are also exported through the imported map
// module. Load the textual header first so Clang does not redeclare its ABI
// tags after the module import.
#include <vector>

import btree_port.btree.map;
import btree_port.btree.set;

Expand All @@ -22,7 +27,6 @@ import btree_port.btree.set;
#include <stdexcept>
#include <tuple>
#include <utility>
#include <vector>
#include <cstring>
#include <rusty/alloc.hpp>
#include <rusty/move.hpp> // rusty::clone
Expand Down Expand Up @@ -54,6 +58,52 @@ template<typename T> auto make_set() {
return BTreeSet<T>::new_in(::rusty::alloc::Global{});
}

struct SplitOwnedValueState {
int live = 0;
};

// Copyable on purpose: this exercises MaybeUninit::assume_init_read's copy
// overload, which used to strand the median value whenever a leaf split.
struct SplitOwnedValue {
SplitOwnedValueState* state;

explicit SplitOwnedValue(SplitOwnedValueState& state_in)
: state(&state_in) {
++state->live;
}
SplitOwnedValue(const SplitOwnedValue& other) : state(other.state) {
++state->live;
}
SplitOwnedValue(SplitOwnedValue&& other) noexcept : state(other.state) {
other.state = nullptr;
}
SplitOwnedValue& operator=(const SplitOwnedValue& other) {
if (this != &other) {
if (state != nullptr) {
--state->live;
}
state = other.state;
++state->live;
}
return *this;
}
SplitOwnedValue& operator=(SplitOwnedValue&& other) noexcept {
if (this != &other) {
if (state != nullptr) {
--state->live;
}
state = other.state;
other.state = nullptr;
}
return *this;
}
~SplitOwnedValue() {
if (state != nullptr) {
--state->live;
}
}
};

// `.check()` shim. The original rustc tests define an `impl<K,V> BTreeMap`
// block in tests.rs adding a private `check()` method that walks the
// navigation internals and asserts invariants (back-pointers, calc_length,
Expand All @@ -65,6 +115,50 @@ template<typename M> inline void check(const M&) {}

} // anonymous

TEST_CASE("split_leaf_relocates_copyable_owned_value_unstubbed") {
SplitOwnedValueState state;
{
auto map = make_map<int, SplitOwnedValue>();
// A leaf holds 11 entries. The twelfth insertion forces median
// extraction before the source node is shortened.
for (int i = 0; i < 12; ++i) {
map.insert(i, SplitOwnedValue(state));
}
assert(map.len() == 12u);
assert(state.live == 12);
}
assert(state.live == 0);
}

TEST_CASE("slice_remove_relocates_copyable_owned_value_unstubbed") {
SplitOwnedValueState state;
{
auto map = make_map<int, SplitOwnedValue>();
for (int i = 0; i < 8; ++i) {
map.insert(i, SplitOwnedValue(state));
}
assert(map.len() == 8u);
assert(state.live == 8);

{
auto removed = map.remove(3);
assert(removed.is_some());
assert(map.len() == 7u);
assert(state.live == 8);
}
assert(state.live == 7);

{
auto removed = map.remove(7);
assert(removed.is_some());
assert(map.len() == 6u);
assert(state.live == 7);
}
assert(state.live == 6);
}
assert(state.live == 0);
}

// ─────────────────────────────────────────────────────────────────────
// rustc map/tests.rs::test_get_key_value (trimmed)
// Full Rust source also exercises map.remove + post-remove checks; the
Expand Down
12 changes: 9 additions & 3 deletions transpiled/btree_port/btree_port.btree.btree_internal.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -6465,8 +6465,11 @@ struct Handle {
(*new_node_shadow1).len = static_cast<uint16_t>(new_len);
// @unsafe
{
auto k = rusty::deref_call(this->node, rusty::detail::__mdisp_key_area_mut{}, this->idx_field).assume_init_read();
auto v = rusty::deref_call(this->node, rusty::detail::__mdisp_val_area_mut{}, this->idx_field).assume_init_read();
// The median slots fall outside the shortened source-node length,
// so this is ownership transfer, not a copy. assume_init_read()
// deep-copies copyable owners and strands the originals here.
auto k = rusty::deref_call(this->node, rusty::detail::__mdisp_key_area_mut{}, this->idx_field).assume_init();
auto v = rusty::deref_call(this->node, rusty::detail::__mdisp_val_area_mut{}, this->idx_field).assume_init();
move_to_slice(rusty::as_mut_slice(rusty::deref_call(this->node, rusty::detail::__mdisp_key_area_mut{}, rusty::range(rusty::detail::deref_if_pointer_like(this->idx_field) + 1, old_len))), rusty::slice_to((*new_node_shadow1).keys, new_len));
move_to_slice(rusty::as_mut_slice(rusty::deref_call(this->node, rusty::detail::__mdisp_val_area_mut{}, rusty::range(rusty::detail::deref_if_pointer_like(this->idx_field) + 1, old_len))), rusty::slice_to((*new_node_shadow1).vals, new_len));
rusty::deref_call(this->node, rusty::detail::__mdisp_len_mut{}) = static_cast<uint16_t>(this->idx_field);
Expand Down Expand Up @@ -7471,7 +7474,10 @@ T slice_remove(std::span<rusty::MaybeUninit<T>> slice, size_t idx) {
const auto len = rusty::len(slice);
assert((rusty::detail::deref_if_pointer_like(idx) < rusty::detail::deref_if_pointer_like(len)));
const auto slice_ptr = reinterpret_cast<std::add_pointer_t<rusty::MaybeUninit<T>>>(rusty::as_mut_ptr(slice));
auto ret = ((*rusty::ptr::add(slice_ptr, std::move(idx)))).assume_init_read();
// The removed slot becomes logically uninitialized when the remaining
// elements shift left. Relocate its owner instead of cloning copyable
// values: a clone would be stranded in the dead trailing slot.
auto ret = ((*rusty::ptr::add(slice_ptr, std::move(idx)))).assume_init();
rusty::ptr::copy(rusty::ptr::add(slice_ptr, rusty::detail::deref_if_pointer_like(idx) + 1), rusty::ptr::add(slice_ptr, std::move(idx)), (rusty::detail::deref_if_pointer_like(len) - rusty::detail::deref_if_pointer_like(idx)) - 1);
return std::move(ret);
}
Expand Down
Loading