Skip to content

rc: a const chosen by a conditional expression is released at the end of its block - #673

Merged
ASDAlexander77 merged 1 commit into
mainfrom
rc-ternary-named-const-release
Oct 23, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
rc-ternary-named-const-release

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Problem

Under -mm=rc, and so under -mm=own, which runs its inference on rc's IR, this gave neither new a ts.Release:

const h = new C();
const h2 = new C();
const each: C = b ? h : h2;

let each = b ? h : h2 leaked both as well, and b ? h : new C() leaked h. const each: C = h; was fine. This is why the class cases of own_ternary_outer_loop (branch own-ternary-outer-loop) never freed h/h2 under either model. The interface cases did not leak: there the ts.New is used by ts.NewInterface, not by the branch.

releaseDiscardedTemporaries gives a folded const (__owned_result_named) its reference back at the end of its block. But allUsesReleasableInOwnBlock refused any value with a terminator user, and the ternary's ts.If yields h through ts.Result.

Fix (OwnedReturnConsumptionPass.cpp)

  • allUsesReleasableInOwnBlock now walks a worklist. When a ts.Result ends a branch of a ts.If with one result (handedOnByConditional), the value is handed on to the If's result. That result's users are then judged by the same rules: no other terminator, in the block or in a structured statement in it, and before the generator release range ends. A reader of a result nobody owns retains it. Where a branch makes the result owned, consumeConditionalResults retained h for it. Either way, the release at the end of h's block stays balanced.
  • storedIntoUnownedLocalOutside takes the same step. So let n: C; if (b) { const h = new C(); n = b ? h : h2; } still keeps h (it leaks, as before) instead of freeing what n reads after the if.
  • escapesThroughStore takes it too, so a choice yielded from a generator under own is not released before the yield. This is defensive only: a generator's locals live in its state object, so no folded const reaches it today.

Tests

  • tests/00conditional_named_const_release.ts covers const, let, made-on-one-side, nested, in a loop, returned, and given to an unowned local outside. Each value is read after churn() reuses freed blocks. The file is in TSLANG_CORPUS (default/rc/none, compile + jit). It is not run under own: own still rejects these merges, partly until own-ternary-outer-loop lands, and returned/stored merges stay errors there.
  • conditional-named-const-release.cmake (test-rc-conditional-named-const-release) reads the rc IR. Each named new must be released exactly once, the one in assignedOutside never, and the owned ts.If in chosenOrNew once. With the pass from main it fails: constChosen` releases %3 0 times, not 1.
  • Windows Release: 5441/5441 passed. Linux (WSL, ninja): 5423/5423 passed.
  • Merged locally with origin/own-ternary-outer-loop (not committed): own/own_ternary_outer_loop.ts passes under own/rc/gc/none (jit and compile), and --verify-ownership is clean. constClass, letClass and branched now release both news under rc and own.

🤖 Generated with Claude Code

… of its block

`const h = new C(); const h2 = new C(); const each: C = b ? h : h2;` gave
neither `new` a release under rc, and so under own, which reads rc's IR. A
folded const is given back at the end of its block by
releaseDiscardedTemporaries, but allUsesReleasableInOwnBlock refused any value
with a terminator user, and the ternary's `ts.If` yields `h` through
`ts.Result`. `let each = b ? h : h2` leaked both too, and `b ? h : new C()`
leaked `h`.

A `ts.Result` ending a branch of a one-result `ts.If` now hands the value on
to the If's result, whose users are judged by the same rules in its place.
What reads a result nobody owns retains it, and a branch that makes the result
owned retained `h` for it, so the release at the end of the block stays
balanced. storedIntoUnownedLocalOutside and escapesThroughStore follow the
same step, so a choice given to a local that owns nothing, or yielded from a
generator under own, still keeps its value.

00conditional_named_const_release runs under every model but own (which
rejects these merges until c ? h : h2 borrows), and
conditional-named-const-release.cmake reads the releases under rc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 105a816 into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the rc-ternary-named-const-release branch October 23, 2026 17:28
ASDAlexander77 added a commit that referenced this pull request Oct 23, 2026
§35's "Not here" said that under rc a folded const a ternary yields is never
released, so the class cases leaked under rc and own. #673 releases it at the
end of its block.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant