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 SummaryFixes issue #619: when a callee only conditionally returns a parameter (
Confidence Score: 5/5Safe to merge. The change is narrowly scoped to Mixed/Union boxed arguments, both ABI targets are covered, the existing #604 guard still passes, and the deliberate container-arg restriction is pinned by a test. The new instruction is correctly validated (2-operand count check), dispatched, and lowered on both AArch64 and x86_64 using the same pointer-comparison shape already proven for ABI cleanup slots. The comparable guard prevents the double-free regression for container args. Three new tests exercise the non-alias path, the mixed-paths balance, and the documented leak-pinning restriction. No issues found. Files Needing Attention: No files require special attention.
|
| Filename | Overview |
|---|---|
| src/codegen/lower_inst/ownership.rs | Adds lower_release_unless_aliases: loads arg and result box pointers, compares them, and releases only when they differ. Both AArch64 and x86_64 are handled. lower_release retains its docblock at line 114. |
| src/ir/instr.rs | Adds ReleaseUnlessAliases variant with correct effects (REFCOUNT_OP |
| src/ir/validator.rs | Adds operand-count validation (exactly 2) for ReleaseUnlessAliases; correctly isolated from the existing 1-operand ownership ops group. |
| src/ir_lower/expr/mod.rs | Emits ReleaseUnlessAliases instead of an unconditional skip when both arg and result have a directly comparable Mixed/Union boxed representation; the continue still fires for non-comparable container args, preserving the documented deliberate restriction. |
| src/codegen/lower_inst.rs | Adds dispatch arm for Op::ReleaseUnlessAliases in lower_instruction, delegating to the new ownership handler. |
| tests/codegen/runtime_gc/regressions.rs | Three new regression tests: non-alias path releases the arg, container arg still leaks (pins the deliberate restriction), and mixed alias/non-alias iterations stay balanced. Expected sums verified arithmetically correct. |
| CHANGELOG.md | Adds one [Unreleased] entry describing the fix, the scope (boxed mixed args), and the deliberate container restriction. |
Sequence Diagram
sequenceDiagram
participant EIR as ir_lower/expr
participant IR as EIR (ReleaseUnlessAliases)
participant CG as codegen/lower_inst
Note over EIR: result_reuses_arg = true, arg and result are Mixed/Union
EIR->>IR: emit ReleaseUnlessAliases(arg, result)
Note over CG: lower_release_unless_aliases()
CG->>CG: load_value_to_result(arg) → value_reg
CG->>CG: load_value_to_reg(result, scratch_reg)
CG->>CG: cmp value_reg, scratch_reg
alt pointers equal (aliased — ownership moved into result)
CG->>CG: b.eq / je → skip_label (no release)
else pointers differ (not aliased — callee dropped arg)
CG->>CG: emit_decref_if_refcounted(Mixed)
end
CG->>CG: skip_label:
Reviews (2): Last reviewed commit: "docs(codegen): restore lower_release's d..." | 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 ( |
Fixes #619 for boxed
mixedarguments. Based on currentmain9f74f5300.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.