Skip to content

A Shared global lent to a call that assigns it: the caller holds a count - #643

Merged
ASDAlexander77 merged 2 commits into
mainfrom
fix-rc-shared-global-argument
Oct 22, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
fix-rc-shared-global-argument

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #639, covering the two cases it left out of holdGlobalArguments.

Shared handles: fixed

run(bare), where bare is a Shared<T> global and run gives bare a new value, freed the block the parameter still read. This happened under rc and own alike: own counts handles with rc's routines, and its #639 rule (rejectGlobalArgumentsDropped) skips handles.

Holding every handle argument would change what Shared.count reports inside the callee. Spec 23.1 defines that count as the number of handles, and a parameter is a borrow, not a handle. So the hold is narrowed instead:

  • A handle is held only across a call that may assign the global it was read from: directly, in a callee (a transitive per-function set of assigned globals), or through a call that names no function (counted as possibly assigning anything). A store into a place inside the global (a field, an element, s.value) counts as assigning it.
  • So no existing Shared.count expectation changes: every own_shared_* test passes unmodified. Inside a call that does assign the global, the hold is the count that keeps the block alive once the global has moved on. The new test asserts it is then the only count.
  • Under own, only a handle read straight out of a global is held. A handle read out of a global's field is still a borrow of that field, and own rejects it with the same error as on main.
  • Under rc, a value read through a global handle (run(G.value)) is now also recognised as global-rooted and held, like a field read.

Calls in a try body (ts.Invoke): did not reproduce as described

This pass runs before the affine lowering, and it is the affine lowering that turns a call inside a try body into a ts.Invoke. So those calls were already held: Retain before the call, and the Release ends up first in the Invoke's normal successor (checked in --emit=mlir-affine). The remaining gap is the unwind edge: when the callee throws, that one count leaks. It can't be observed for a plain class, and --verify-ownership does not see unwind edges. Fixing it needs a per-call release on the exception path, which is a new mechanism. It is left open, and the comment that said try-body calls were skipped is corrected.

Tests

  • own/own_shared_global_argument.ts, in the shared_models foreach (gc/none/rc/own) and the rc/own --verify-ownership list. It covers a direct assignment, an assignment through a callee, a call in a try body, and checks that a call which doesn't assign takes no count. On main it fails under rc and own.
  • tests/00shared_global_field_argument.ts, in the default lists and TSLANG_CORPUS (rc + none): a handle read out of a global's field. On main it fails under rc.
  • Teeth: skipping the handle hold fails test-compile-rc-own_shared_global_argument, test-compile-own-own_shared_global_argument and test-compile-rc-corpus-00shared-global-field-argument (and their jit twins). Without the switch they pass.
  • 00shared_global_field_argument.ts also pins run(G.value): a value read through a global handle, given to a call that replaces it (fails on main under rc).
  • Windows ctest -C Release: 4930/4930. WSL Linux full run: 4912/4912 (before the G.value case was added; that test-only commit was re-run on Windows only).

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits October 22, 2026 19:06
`run(bare)`, with `bare` a `Shared<T>` global and `run` giving `bare` a new
value, freed the block the parameter still read, under rc and own alike:
#639's hold of a global argument across a call left handles out, since a
held count shows in `Shared.count`. A handle is now held, under rc and own,
across a call that may assign the global it was read from - directly, in a
callee, or through a call that names no function - and only there, so
`Shared.count` elsewhere is still the number of handles (spec 23.1); no
existing count changes. Under rc a handle read out of a global's field is
held the same way, and so is a value read through a global handle (`run(G.value)`); under own a read out of a field stays a borrow, rejected as before.

The comment about calls in a try body is corrected: this pass runs before
the affine lowering makes them `ts.Invoke`, so they were already held on
the normal path. A throw out of such a call skips the release (one count
leaked), which is left open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`run(G.value)`, with `run` doing `G.value = mk(..)`, failed on main under rc
and is held by this branch; the test now pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit d631edf into main Oct 22, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-rc-shared-global-argument branch October 22, 2026 18:36
ASDAlexander77 added a commit that referenced this pull request Oct 22, 2026
holdGlobalArguments (#639, #643) retains a global read given to a call that
may overwrite it and releases it after the call. A throw out of the call
skipped the release: a try around the call caught the exception, and the
global's block kept a count nobody gave back - under rc for any held value,
under own and rc for a Shared<T> handle, where `Shared.count` showed it.

When a try in the same function takes the call's exceptions, the held value
now also goes into a slot made at the function's entry, null outside the
call, and the handler that runs first (the try's cleanup, else its catch
clause, else its finally; the finally for a call in a catch clause - the
landing pad's own order) begins with a release of that slot. An exception
that leaves the function still leaks the count, as it does for locals.

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