Skip to content

-mm=own: c ? h : h2 bound in a loop borrows the one it chooses - #672

Merged
ASDAlexander77 merged 1 commit into
mainfrom
own-ternary-outer-loop
Oct 23, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
own-ternary-outer-loop

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Problem

Under -mm=own, binding cond ? x : y inside a loop, where x and y are locals made before the loop, was rejected for every type, a plain class included:

'this value' is moved inside a loop but was made outside it; only a borrow could do that, and -mm=own cannot prove one yet

The ternary lowers to an arith.select of the two values, or to a cf branch that passes each as a block argument when one side has a call. own did not treat either as a read, so it counted the merge as taking the value. #671 found this and rewrote the loop case in 00interface_cast_unrelated to avoid it.

Fix

  • borrowsThroughMerge / mergedValuesOf (OwnershipInferenceMoves.cpp): a merge whose value is only read now counts as a borrow of each value it chooses. "Only read" means walkBorrowed finds no store, return, capture or retain of the merged value, and none of its uses runs after one of the source's releases.
    • The uses it finds count as reads of the source, so a use of the merged value after a move of h is still a use after that move.
    • A merge whose value is kept is still a taker, decided as before. Every program accepted before is accepted the same way.
  • let each = c ? h : h2: when h and h2 are values the function made, each borrows the merge the way let b = a does. Its ts.RetainSlot and ts.ReleaseSlot are erased, and decideRetains skips them through mergeBorrowRetains.
  • walkBorrowed (passesOn) passes through a select, as it already did through a branch.
  • The verdict-2 path in checkValueMoves (borrowing lets) also leaves such a merge out of its takers.
  • The own spec has a new §35.

Tests

  • own/own_ternary_outer_loop.ts binds the ternary inside a loop and reads h and h2 after the loop. It covers a const and a let, a class and an interface, the branch form (i % 4 == 0 ? h : (odd(i) ? h2 : h)) and one side made inside the loop. It runs under own, rc, gc and none, JIT and AOT, and own emits no counting.
  • tests/00interface_cast_unrelated.ts gets its loop case back. Its element case ([h]) stays rewritten, because that one really is a move of h.
  • Negatives: own_err_ternary_loop_stored (pushed), own_err_ternary_loop_returned (a let returned) and own_err_ternary_loop_captured (captured by an escaping closure). All three are still moved inside a loop.
  • Tooth: built without the change, all six functions in the run test fail to compile, and so does the interface test's loop case.
  • Windows: ctest -C Release 5446/5446 passed. Linux (WSL, ninja Release): 5428/5428 passed.

Not covered

  • let each = c ? h : h2 where h and h2 are themselves lets. The merge is of their reads, which movesOf does not check, so it is still rejected. It was rejected before this PR as well. A const each of the same reads compiles, before and after.
  • Under rc, a folded const that a ternary branch yields is never released. releaseDiscardedTemporaries skips any value that a ts.Result uses. So the class-typed cases leak h and h2 under rc, and under own too. The interface cases don't leak. This belongs in OwnedReturnConsumptionPass, not in own, and a separate session is working on it.

🤖 Generated with Claude Code

A ternary of two locals lowers to an arith.select (or, when a side has a call,
a cf branch passing each as a block argument). Neither was a read to own, so
the merge took the value, and inside a loop that was "moved inside a loop but
was made outside it" for every type.

A merge whose value is only read - walkBorrowed finds no store, return,
capture or retain of it, a `let` declared from a merge of values the
function made borrowing as `let b = a` does - is now a borrow of each value it chooses, checked against that value's
releases. What reads the merged value is a read of each source, so a use of it
after a move of one is still a use after the move. A merge whose value is
kept is still a taker, decided as before. walkBorrowed passes through a select.

own_ternary_outer_loop runs under every model; 00interface_cast_unrelated gets
its loop case back. Stored, returned and captured merges stay errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 6c16f6d into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-ternary-outer-loop branch October 23, 2026 17:15
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