fix(ir,codegen): release conditionally-aliased call arguments at runtime (#619) - #644
Conversation
ReturnArgAlias::Parameters is a MAY summary -- a union over branches -- so a callee that returns its parameter only on one branch still reports that parameter as possibly returned. The caller suppressed the argument release on every path, which is required on the branch that hands the box back (illegalstudio#604) and leaks one block per call on the branches that do not (illegalstudio#619). Adds an EIR ReleaseUnlessAliases instruction: it compares the argument payload against the value the call returned and releases the argument only when they differ, so each call picks the right behaviour at runtime instead of the lowering guessing once for all paths. Both supported ABIs lower it, reusing the pointer-comparison shape the ABI-side cleanup slots already use. The comparison is only emitted when both sides are boxed mixed, i.e. directly comparable as single pointers. A mixed result that wraps a bare container holds a different pointer than the container, so comparing them would release a value the result owns; those arguments keep the previous suppression and their pre-existing leak, pinned by a test so the restriction stays deliberate.
|
@greptileai please review |
Greptile SummaryThis PR introduces a new EIR instruction
Confidence Score: 5/5Safe to merge; the fix is well-scoped, both ABI targets are covered, and the heap-debug test suite validates alias and non-alias paths end-to-end. The new instruction, its effects mask, the validator guard, and the two-target lowering are all internally consistent. The deliberate container-argument limitation is documented, pinned with a failing-intent test, and tracked in #665. The only observation is a load-order fragility in the new lowering helper — result is captured after int_result_reg is overwritten rather than before, reversing the convention established by the existing comparison helper — but current tests pass because the REFCOUNT_OP effects cause the register allocator to move the call result out of the int-result register before the instruction fires. Files Needing Attention: src/codegen/lower_inst/ownership.rs — the load ordering of value vs result in lower_release_unless_aliases.
|
| Filename | Overview |
|---|---|
| src/codegen/lower_inst/ownership.rs | Adds lower_release_unless_aliases; load-order of value vs result diverges from the established emit_branch_if_cleanup_temp_aliases_result pattern in a way that is fragile if result happens to be allocated to int_result_reg. |
| src/ir_lower/expr/mod.rs | Emits ReleaseUnlessAliases for Mixed/Union-typed owning temporaries when result_reuses_arg is true and both codegen_repr() values are boxed-mixed; falls through to the existing continue for non-comparable cases preserving prior behaviour. |
| src/ir/instr.rs | Adds ReleaseUnlessAliases opcode variant with correct effects (REFCOUNT_OP |
| src/ir/validator.rs | Adds check_count(2) for ReleaseUnlessAliases; operand-count guard is correct, no type-check validation at IR level but emission site enforces Mixed/Union restriction. |
| src/codegen/lower_inst.rs | Adds dispatch arm for ReleaseUnlessAliases; straightforward single-line addition. |
| tests/codegen/runtime_gc/regressions.rs | Adds three well-structured regression tests: non-alias-path leak fix, deliberate container limitation, and mixed alias/non-alias paths; arithmetic and expected outputs are correct. |
| CHANGELOG.md | New changelog entry accurately describes the fix, the boxed-mixed restriction, and the bare-container follow-up in #665. |
Sequence Diagram
sequenceDiagram
participant EIR as EIR Lowering
participant IR as EIR Instruction
participant CG as Codegen
participant OWN as Ownership Lowerer
Note over EIR: release_owned_call_arg_temporaries_with_signature()
EIR->>EIR: value_is_owning_temporary(arg)?
EIR->>EIR: "result_reuses_arg = proven/may alias?"
alt !independently_boxed and result_reuses_arg
EIR->>EIR: "arg_repr = codegen_repr(arg), result_repr = codegen_repr(result)"
alt both are Mixed or Union
EIR->>IR: emit Op::ReleaseUnlessAliases(arg, result)
else bare container or non-comparable
Note over EIR: continue — no release, existing leak tracked in #665
end
else no alias
EIR->>IR: emit Op::Release(arg)
end
Note over CG: At codegen time
CG->>OWN: lower_release_unless_aliases(inst)
OWN->>OWN: load value into int_result_reg
OWN->>OWN: load result into symbol_scratch_reg
OWN->>OWN: cmp int_result_reg, symbol_scratch_reg
alt pointers equal — alias path
OWN->>OWN: b.eq / je skip_label — ownership in result, do not release
else pointers differ — non-alias path
OWN->>OWN: emit_decref_if_refcounted — callee dropped arg, release it
end
OWN->>OWN: skip_label:
Reviews (4): Last reviewed commit: "Merge origin/main into fix/619-runtime-a..." | Re-trigger Greptile
|
CI note — the two failures are infrastructure, not test failures.
and the Neither reached an assertion — no test reported a failure. The other 111 checks are green. A re-run should clear both; the branch is also |
…owering Inserting lower_release_unless_aliases directly above lower_release moved that function's docblock onto the new one, leaving lower_release undocumented and the new doc with a stale opening line. AGENTS.md requires a docblock on every function.
|
Good catch from the bot — both findings were valid and are fixed in Inserting Verified after the change: The CI failures on this PR remain unrelated to the code — the checkout step timed out ( |
There was a problem hiding this comment.
Maintainer follow-up: I handled the requested PR maintenance directly.
- I opened #665 to track the bare-container leak that remains outside this boxed-mixed fix.
- I merged current main into the contributor branch without rewriting contributor history; the integration commit is c1a3fd2.
- I resolved the CHANGELOG conflict while preserving the entries from main and linking the follow-up issue.
- I revalidated the build, the four conditional-return regressions with EIR optimization both on and off, assembly-comment alignment, and git diff --check.
- Exact-head CI restarted on c1a3fd2 in run 30826743063.
There were no requested code changes within the boxed-mixed scope. Tracking and current-main integration have now been completed by me as maintainer, and CI is running on the exact updated head, so this request-changes review can be dismissed.
…-disambiguation # Conflicts: # CHANGELOG.md
Addressed directly by the maintainer: follow-up issue opened, current main merged without rewriting contributor history, focused validation completed, and exact-head CI restarted.
|
@Guikingone if you give a run of review on this PR I'll proceed to merge it. |
Fixes #619 for boxed
mixedarguments. Follow-up #665 tracks the bare-container path deliberately left out of scope.Integrated current
maine015218f5in maintainer mergec1a3fd23e.Root cause
ReturnArgAlias::Parametersis a MAY summary — a union over branches — soproven_aliases_parameteralso holds for a callee that returns the parameter only conditionally (if ($c) return $x; return 7;). The suppression site (src/ir_lower/expr/mod.rs) therefore skipped the argument release on every path. That is required on the branch that hands the box back, or it is freed twice (#604), and it leaks one block per call on the branches that do not.The lowering has to decide once, statically, for a fact that is only known per call. So the decision moves to runtime.
Fix
A new EIR instruction,
ReleaseUnlessAliases(arg, result): after the call, compare the argument payload against the value the callee returned and release the argument only when they differ. Same pointer means ownership moved into the result and the caller must keep its hands off; different pointer means the callee dropped it and the caller still owns it.Both supported ABIs lower it, reusing the comparison shape
emit_branch_if_cleanup_temp_aliases_resultalready uses for ABI-side cleanup slots. On a callee that genuinely always returns the parameter the comparison always matches, so #604's behaviour is unchanged.Deliberate restriction, and why
The comparison is emitted only when both sides are boxed
mixed, i.e. directly comparable as single pointers.I initially emitted it unconditionally. That regressed four tests with
heap debug detected bad refcount: amixedresult that wraps a bare container holds a different pointer than the container itself, so the comparison reads "not aliased" for a value the result does own, and releases it twice. Bare container arguments therefore keep the previous suppression and their pre-existing leak — the one that predates #618 — andtest_conditional_return_callee_container_arg_still_leaks_on_non_alias_pathpins that leak so the restriction stays deliberate and the test turns red the day the container path is covered. Covering it means reaching through the box to its payload field, which I'd rather do as its own change than smuggle in here.Tests
In
tests/codegen/runtime_gc/regressions.rs, next to the #604 family:maybe($i + 1, $i % 2)) — releasing on the aliasing iterations would double-free, skipping it on the others leaks; leaked exactly 10 blocks before, one per non-aliasing iterationtest_conditional_return_callee_alias_path_stays_balancedis the Direct owned boxed-mixed call argument with a consumed return value corrupts refcounts (heap debug: bad refcount) #604 guard and still passesVerification on
35dc9e18b, macOS ARM64ELEPHC_IR_OPT=offruntime_gccallableserror_testsir_backend_smoke_testcargo test -p elephc --libcargo build/git diff --checkNot run locally: the Linux targets — no Docker on this machine. The x86_64 lowering is the mirror of the AArch64 one and reuses the existing comparison helper's shape, but CI owns the real signal.
Maintainer integration (2026-08-03)
maine015218f51a5f321a8f8b7ddbced38f177ae1f53without rewriting contributor commits.mainentry and linking the residual container limitation to Bare container call arguments leak on non-aliasing conditional-return paths #665.cargo build, the fourconditional_return_calleeregressions with EIR optimization enabled and disabled, assembly-comment alignment, andgit diff --check.c1a3fd23eca9b97ed23538738b8b770de4569f59is the authoritative supported-target signal.