Skip to content

Run element destructors on refcount map iterator erase - #585

Closed
perfloop-agent wants to merge 2 commits into
Cpp2Rust:masterfrom
perfloop-oss:perfloop-pr-open-rw6k6jshqr
Closed

perfloop-agent wants to merge 2 commits into
Cpp2Rust:masterfrom
perfloop-oss:perfloop-pr-open-rw6k6jshqr

Conversation

@perfloop-agent

@perfloop-agent perfloop-agent commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Refcount map iterator erase now calls the stored key and mapped-value C++ destructors before returning. Releasing Rust storage is not a substitute for these explicit callbacks, and unused iterator copies can keep that storage alive.

Map::erase returns the successor and a strong owner of the removed entry. The mutable and const iterator rules finish destruction after the runtime has unlinked the entry, removed its ordered-index reference and released its borrows. The rules reuse the existing destructor dispatch, which does nothing for trivial types. No finalizer is passed into the runtime or stored in the map, and neither key nor mapped value needs a new Clone bound. The rule-writing documentation explains the ownership boundary.

Testing

The scoped map iterator-erase lit runner reports 5 passed and 1 expected failure, with no unexpected failures or skips.

tests/unit/map_erase_destructor.cpp compares native C++ with generated code for record/record, record/int, int/record and int/int entries. It checks callbacks at return and after unused iterator copies leave scope, plus successor, size, remaining element order and untouched live positions. It also erases a move-only key from a nested default-constructed map using a temporary iterator and no retained copy. The existing map fixtures check trivial-type behavior and key-copy counts.

Runtime and rules Clippy, Rust formatting, fixture C++ formatting and documentation formatting checks pass. The destructor fixture also compiles as generated Rust with unsafe code forbidden; without the fix it fails the callback assertion.

This repair covers refcount iterator erase. Key-based erase, whole-map destruction and unsafe output are unchanged; the destructor fixture remains an expected failure in the unsafe model. Unused iterator copies may still retain Rust storage.

Case and evidence


Made by Perfloop. Human sponsor: Tomás Senart. To ask for a change, comment or review in plain words. How Perfloop handles pull requests.

Comment thread rules/map/shim.rs Resolved
Comment thread rules/map/tgt_refcount.rs Outdated
Map::erase(a0, &a1)
Map::erase(a0, &a1, |__key, __value| {
use libcc2rs::__swap::{Destroy, DestroyNone, DestroyWithDtor};
(&&Destroy(&__value)).destroy();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not a pretty solution to pass a destroy function to every call. We would need to pass destructors in quite a few places (including map destructor)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback parameter is removed. Map::erase returns the successor and an owning handle to the removed entry, and the expanded iterator rules run its destructors before yielding the successor. This avoids per-map hook initialization while leaving whole-map destruction unchanged. The callback removal revises the previously reviewed proposal, and this PR covers destruction of an erased entry only; would you like whole-map destruction (clear and map drop) handled in this PR or in a follow-up?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like any solution that requires calling destructors manually. That is not safe because after translation, if someone modifies the code by hand, it's very error prone. C++ destructors should be called automatically by Drop or similar. Might be a larger change than what you had planned.

Signed-off-by: Perfloop Agent <agent@perfloop.ai>
Signed-off-by: Tomás Senart <ts@perfloop.ai>
Signed-off-by: Perfloop Agent <agent@perfloop.ai>
Signed-off-by: Tomás Senart <ts@perfloop.ai>
@perfloop
perfloop Bot force-pushed the perfloop-pr-open-rw6k6jshqr branch from c8a7b42 to 7a77c54 Compare November 5, 2026 03:19
@perfloop-agent

Copy link
Copy Markdown
Contributor Author

I would replace the destructor calls in the erase rules with automatic destruction owned by the map entries:

  • The map would hold a non-cloneable RAII entry owner whose Drop runs the key and mapped-value C++ destructors while their allocations remain alive. Iterator copies would retain only shared node/storage handles, not this destruction owner. Putting Drop on the existing shared node alone would delay destruction until the last iterator copy disappears.
  • Destruction support would be available through the element types: generated records dispatch to their C++ destructor bodies, trivial types do nothing, and nested containers destroy their owned entries. This must work through Default, including nested maps, rather than installing hooks only at explicit constructor calls. The raw record would not also gain a second Drop that repeats existing scoped cleanup.
  • Iterator and key erase would unlink the entry, remove its index reference, release borrows, then let its owner drop before returning. clear would detach and drain all entry owners; map Drop would drain the remaining owners through the same machinery. Generated erase/clear call sites would contain no explicit destructor calls.
  • Moves would transfer entry owners without cloning keys. Copies would create fresh owners only where copying is already supported; move-only keys would need no new Clone bound. Assignment would also need to destroy the replaced entries correctly.

Rough estimate: 400–700 handwritten lines across rules/map/shim.rs, rules/map/tgt_refcount.rs, a clear rule in rules/map/src.cpp, ownership/destruction support in libcc2rs/src/rc.rs and capture.rs, libcc2rs-macros/src/destructor.rs, relevant converter destruction/default handling, and tests/docs, plus regenerated expectations. This is an unimplemented design estimate; exact-once timing, re-entrancy, nested defaults and copy/move replacement still need validation. It is too large for #585 as a narrow iterator-erase fix; I recommend a separate ownership change.

Is this map-entry RAII design what you mean by automatic destruction, and would you prefer it to replace the approach in #585 or be developed in a new PR?

@nunoplopes

Copy link
Copy Markdown
Contributor

Yeah, this is too much for you. Let me close this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants