try_emplace skips argument side effects when the key already exists
perfloop-oss/cpp2rust · UNCATALOGUED CORRECTNESS
https://perfloop.ai/t/oss/case_455cjv2886
Verdict
VERIFIED · settled 2026-11-02 · pull request opened as Cpp2Rust/cpp2rust#583
What happened: The assertion is violated on the comparison and satisfied with this change.
Hypothesis
An existing key can make translated try_emplace skip side effects in its mapped-value arguments. C++ evaluates those arguments even when it does not construct an element. This changes caller state in a reproduced case; how often consumers pass such expressions is unknown.
The default-refcount f73 expansion puts init only inside if __inserted. Rule bodies are spliced inline, as documented in docs/src/rules/writing-rules.md:33-40. Converter::ConvertInitFragment builds the mapped object from the original argument expressions at that location. In the emitted Rust for m.try_emplace(key, ++counter), counter.prefix_inc() remains inside the guard. Simply moving the whole init before the guard would incorrectly make mapped construction eager, so it is not a sufficient class-wide repair.
checkProperty(prop_68rxdgx552) at eeabae779177a52591c09a635664c3913e42bd56 completed a finite grid of 144 calls over three keys, every initial key subset, counters 0..2, and pre/post increment. The 72 existing-key cases differed; the 72 missing-key controls matched. The reduced stored input [0,1,0,0] starts with m[0]=10 and counter=0, then calls try_emplace with lvalue key 0 and ++counter. C++ leaves counter=1; emitted Rust leaves counter=0. Both retain value 10, return its iterator and report false. The final stored command, python3 tests/property/map_try_emplace/run.py arguments, reproduced that input without enumeration and exited 1. The replay and check are retained on the property node.
These are public translator-CLI observations with LLVM 22.1.8 and Rust 1.99.0. The stored LLVM compatibility header only forward-declares an unused newer AST parameter type; no rule or runtime implementation was edited. Setup failures were repaired rather than counted as semantic failures. The separate result-model property prop_dqjmhebmfx passed all 259 traces through depth three, but that pure-argument control does not cover argument effects. README.md documents refcount as the default mode, and tests/unit/map.cpp already asserts try_emplace results with inert arguments.
The smallest useful Case check is the saved replay, followed by the finite argument grid and the constructor-log property prop_fgbdkvdjfp. Confirmation requires the increment on both branches without constructing a mapped object on a hit. Forwarding, reference binding and exceptional exits beyond the tested integer cases remain compatibility questions for the repair; no production-frequency gate is requested.
Related prior hyp_84wcez8rs6 concerns repeated substring position evaluation, not suppression of mapped arguments by an insertion guard. user:4 closed that Case with a temporary source-provisioning hold, not a semantic refutation; this proposal neither reopens it nor treats the hold as runtime evidence. The refreshed open PR set still contains https://github.com/Cpp2Rust/cpp2rust/pull/561. Its examined implementation at 14d0fc3f55a502d80606b233684666b610cdf87d adds matching for {} construction and return-type handling, not pre-guard argument evaluation. The remaining work is distinct.
Change to test: Preserve eager evaluation of the original mapped-value arguments on both hits and misses while leaving mapped-object construction conditional. Separate argument materialization from the initializer expansion, retaining forwarding and evaluation-once semantics.
Where it lives
Evidence
The assertion is violated on the comparison and satisfied with this change: `For const-lvalue-key std::map::try_emplace in the default and unsafe output models, the 144 integer cases over three keys, all initial key subsets, counters 0..2 and pre/post increment, and the non-throwing local/global, reference, copy/move and temporary-argument cases evaluate mapped arguments once on hits and misses, leave stored mapped-object construction conditional, and preserve caller state, forwarding, contents, returned iterator and insertion flag.`
Checks: 4 of 4 passed. Verification: no defect found.
Timeline
2026-11-01· Case opened2026-11-02· PR opened