diff --git a/docs/btreemap_port/post_transpile_patch.py b/docs/btreemap_port/post_transpile_patch.py index d64e3353..b6294059 100644 --- a/docs/btreemap_port/post_transpile_patch.py +++ b/docs/btreemap_port/post_transpile_patch.py @@ -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, Type>`. The transpiled body has the same shape as `Handle::descend` — emitted @@ -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. diff --git a/tests/btree_tests_port_unstubbed.cpp b/tests/btree_tests_port_unstubbed.cpp index 8fc21d38..998110ee 100644 --- a/tests/btree_tests_port_unstubbed.cpp +++ b/tests/btree_tests_port_unstubbed.cpp @@ -14,6 +14,11 @@ // Test name convention: `_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 + import btree_port.btree.map; import btree_port.btree.set; @@ -22,7 +27,6 @@ import btree_port.btree.set; #include #include #include -#include #include #include #include // rusty::clone @@ -54,6 +58,52 @@ template auto make_set() { return BTreeSet::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 BTreeMap` // block in tests.rs adding a private `check()` method that walks the // navigation internals and asserts invariants (back-pointers, calc_length, @@ -65,6 +115,50 @@ template inline void check(const M&) {} } // anonymous +TEST_CASE("split_leaf_relocates_copyable_owned_value_unstubbed") { + SplitOwnedValueState state; + { + auto map = make_map(); + // 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(); + 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 diff --git a/transpiled/btree_port/btree_port.btree.btree_internal.cppm b/transpiled/btree_port/btree_port.btree.btree_internal.cppm index 1a883f6f..00afa7ac 100644 --- a/transpiled/btree_port/btree_port.btree.btree_internal.cppm +++ b/transpiled/btree_port/btree_port.btree.btree_internal.cppm @@ -6465,8 +6465,11 @@ struct Handle { (*new_node_shadow1).len = static_cast(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(this->idx_field); @@ -7471,7 +7474,10 @@ T slice_remove(std::span> 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>>(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); }