Repository navigation
fix(ts): replace an indexed element whose drop may panic - #176
Conversation
Quest-Claim: 10add91ec8013576
A cleanup's index temporaries are declared before the `try`, typed where they are spilled; a store through a reference makes a cleanup visible; and a borrow through one reference holds only that reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 36d084be4937fdf79535eafce787fd085774009f.
No actionable bugs found in the changed code. The direction is sound: typed hoisting makes cleanup-visible index/reference temporaries available without introducing per-replacement rt.Place allocations, and counting indirect cleanup stores preserves replacement after a panicking drop. Sharing the panic predicate between planning and emission avoids unnecessary handlers.
Checked the lowering changes, surrounding place/cleanup logic, and regression fixtures for indexed replacement, bounds failure, borrowed array slots and loop cleanup, including the generated waker replacement. Static GitHub inspection only; I did not run the translator, TypeScript checks or differential tests.
…anup # Conflicts: # ts/src/lower/mir/place.rs
|
Merging at the maintainer's request. The approach is the one in the body: index temporaries a cleanup reads are typed and declared before the (Written by Claude Opus 5.5) |
Problem
v[i] = x, where the old element's drop can panic, lowered to TypeScript thattscrejects. The drop-and-replace's unwind cleanup replaysv[at] = xin thecatch, butatis aconstdeclared inside thetry. kio'sself.children[index] = Some(..)has this shape.Reproducing it turned up a second instance of the same bug: when the cleanup only stores the new value through a reference (with nothing dropped after it),
cleanup_visibledidn't count the store. So the function got nocatch, and the caller's slot kept the old value it had already dropped. That's a double drop, andfutures.tsin the task fixture already had one: a waker replaced through a by-ref closure capture.Approach
The temporary is in scope in the catch. At the spill, each temporary a place names records its TS type:
index,at, andsliceinindex(), and the indices and reference rootsstable_targetholds. After lowering,declare_caughtfinds each declaration inside atrywhosecatchreads it (skipping names the handler declares itself, as inline drop glue does). It turns that declaration into an assignment and declares the name up front withlet x!: T, the existingDeclareidiom. A temporary with no recorded type that acatchreads is an error with a span, never broken output.stable_targetholds less. For a field or element of what one reference points at, only the reference can change while the borrow lives, so it holds only that root (typed as its local) rather than the whole object (self.slots). That's howRing::lent(an array-field slot lent out, then replaced) type-checks.Stores through a reference are visible cleanup.
cleanup_visiblecounts anAssignthrough a deref.plan_unwindnow only considers terminators that can panic in JS, using the samecan_paniccheck asset_unwind. Without that, functions whose only drops are of values that do nothing in JS (WaiterList.register) would get a deadcatch.Tests.
blown's scenarios 8 to 10 in the task fixture (runalready enumerates every one of them incases) cover:Vecreplaced in place, then past its end.&mut self, with the old value panicking.The native and translated logs match on the differential and unwinding runs.
Choices made without asking:
let x!: Thoist overvar xplusx!in the catch.varneeds no types, but it is foreign to the generated code's idiom.unwindsets in coroutine drop paths (blocksFuture,boxedFuture). Their cleanups clear drop flags throughself. That's conservative and only on panicking drop paths.Impact
catchthat stores the new value (kio.Park.hold, a closure infutures.ts), and a cleanup's index temporaries are declared before thetry.Alternatives
vardeclarations read asx!in the catch: no type plumbing, but non-idiomatic output.rt.Placevariable for every alias a cleanup reads: typed from MIR, but it allocates per replace in hot paths.Follow-ups
stable_pathholds only the outermost object, so an index inside it (a[i].b[j]withireassigned while the borrow lives) is read live in own places. This predates this PR and isn't changed by it.Checks:
just ts test,just ts check,just ts moq, andquest checkpass locally. Theffi,driver, anduniffichecks weren't run, since this PR doesn't touch them.Closes the quest quest/m2/ts-index-cleanup.md.
(Written by Claude Opus 5.5)
🤖 Generated with Claude Code