Iterator erase omits required key and mapped-value destructors

perfloop-oss/cpp2rust · UNCATALOGUED CORRECTNESS

https://perfloop.ai/t/oss/case_qbv6ef7ygt

Verdict

VERIFIED · settled 2026-11-03 · pull request opened as Cpp2Rust/cpp2rust#585

What happened: The assertion is violated on the comparison and satisfied with this change.

Hypothesis

For record types with explicit destructors, iterator erase removes the map entry without invoking the required key and mapped-value finalizers. These calls can perform observable cleanup even when map contents and the returned successor are correct. The public-runtime mismatch is reproduced; end-to-end consumer coverage and prevalence remain unmeasured.

The scope is refcount f3 and its Map::erase target. At the accepted revision, user-defined C++ destruction is represented by the Destructor trait and explicit calls, not an automatic Rust Drop implementation. ConverterRefCount::EmitScopedDestructor emits such calls for local records, and the committed destructor.rs expectation demonstrates them. Map::erase only unlinks the node and removes its ordered-index reference. It neither invokes the key/value callbacks nor receives an erased-entry finalizer from f3.

checkProperty(prop_gwj9267wx5) at e2b679de9fa36c3b015b4604759ab0ff53003fa4 exited 101 on the reduced input [1,0,0,0]. The public runtime held one entry with key -1 and mapped value -1, both implementing the existing Destructor seam. Direct callback controls produced the expected log. After iterator erase, neither required callback appeared. The log was still empty after dropping the erased iterator, with no operation performed through that invalidated iterator. The returned eight-byte replay is stored with the check. The run stopped on its first input, not after the planned 72-call grid. An earlier fixture-only missing-ByteRepr compilation failure was repaired before this semantic result and is not defect evidence.

This contribution is independent of hyp_w6f6hdvwv2. That finding demonstrates a Rust-owned file eventually closing when the last erased handle drops. Here the explicit C++ callbacks never run even after that storage owner drops; detaching the payload alone does not dispatch them. It is also distinct from the prior try_emplace placeholder-construction repair hyp_kadaafbr9k: this check resets construction effects and measures removal callbacks. The completed 7,381-trace integer state-transition check prop_zq46x1e8fb is a regression control, not evidence that callbacks run.

The smallest Case check is the stored callback replay, followed by native-versus-emitted C++ map erasure with observable key and mapped destructors. Compare valid prebound-iterator removal, retained but unused iterator copies, size, order, successor and untouched live positions. Confirmation requires the required callbacks exactly once before return, without imposing a destructor order not established by the fixture. A shared erased-entry finalization path can address the class of supported destructible key/value combinations instead of special-casing one record. The Case must establish that the hooks preserve safe generated Rust and trivial-type compatibility, and account for the rule/runtime interface and documentation work. No CPU saving or finite-resource incident is claimed by the counter log.

The refreshed open page still contained https://github.com/Cpp2Rust/cpp2rust/pull/561 and https://github.com/Cpp2Rust/cpp2rust/pull/579. The examined #561 implementation at 14d0fc3f55a502d80606b233684666b610cdf87d changes empty-list matching and return-type handling, not erased-entry finalization. The examined #579 ownership implementation at 2a70743fb1f6950cffcc8a985a36c5a98542589d changes descriptors and stream storage, not invocation of Map element Destructor hooks. Completed operand capture in https://github.com/Cpp2Rust/cpp2rust/issues/575 likewise does not provide these missing callbacks.

Change to test: Arrange erased-entry finalization to invoke required C++ key and mapped-value destructors exactly once before returning, while keeping trivial types usable. Carry the generated destructor hooks through the rule/runtime boundary instead of treating release of Rc storage as a substitute for those callbacks.

Where it lives

perfloop-oss/cpp2rust · rules/map/tgt_refcount.rs

Evidence

The assertion is violated on the comparison and satisfied with this change: `In refcount output, erasing valid prebound mutable or const map iterators runs each stored key and mapped-value C++ destructor exactly once before return, including with retained unused iterator copies, for record/record, record/int, int/record and int/int entries; successor, size, order and untouched live positions remain valid in the three-entry removal fixture.`

Checks: 3 of 3 passed. Verification: no defect found.

Timeline