Skip to content

fix(ts): replace an indexed element whose drop may panic - #176

Merged
kixelated merged 4 commits into
mainfrom
quest/m2/ts-index-cleanup
Oct 28, 2026
Merged

kixelated merged 4 commits into
mainfrom
quest/m2/ts-index-cleanup

Conversation

@kixelated

@kixelated kixelated commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

v[i] = x, where the old element's drop can panic, lowered to TypeScript that tsc rejects. The drop-and-replace's unwind cleanup replays v[at] = x in the catch, but at is a const declared inside the try. kio's self.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_visible didn't count the store. So the function got no catch, and the caller's slot kept the old value it had already dropped. That's a double drop, and futures.ts in 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, and slice in index(), and the indices and reference roots stable_target holds. After lowering, declare_caught finds each declaration inside a try whose catch reads 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 with let x!: T, the existing Declare idiom. A temporary with no recorded type that a catch reads is an error with a span, never broken output.

  • stable_target holds 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 how Ring::lent (an array-field slot lent out, then replaced) type-checks.

  • Stores through a reference are visible cleanup. cleanup_visible counts an Assign through a deref. plan_unwind now only considers terminators that can panic in JS, using the same can_panic check as set_unwind. Without that, functions whose only drops are of values that do nothing in JS (WaiterList.register) would get a dead catch.

  • Tests. blown's scenarios 8 to 10 in the task fixture (run already enumerates every one of them in cases) cover:

    • A Vec replaced in place, then past its end.
    • kio's slab shape through &mut self, with the old value panicking.
    • An array slot lent out, then replaced.
    • A loop whose cleanup only stores.

    The native and translated logs match on the differential and unwinding runs.

Choices made without asking:

  • I took the typed let x!: T hoist over var x plus x! in the catch. var needs no types, but it is foreign to the generated code's idiom.
  • I left the extra unwind sets in coroutine drop paths (blocksFuture, boxedFuture). Their cleanups clear drop flags through self. That's conservative and only on panicking drop paths.

Impact

  • Public API: none.
  • Generated code: functions whose cleanup stores through a reference now get a catch that stores the new value (kio.Park.hold, a closure in futures.ts), and a cleanup's index temporaries are declared before the try.

Alternatives

  • var declarations read as x! in the catch: no type plumbing, but non-idiomatic output.
  • An rt.Place variable for every alias a cleanup reads: typed from MIR, but it allocates per replace in hot paths.

Follow-ups

  • stable_path holds only the outermost object, so an index inside it (a[i].b[j] with i reassigned 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, and quest check pass locally. The ffi, driver, and uniffi checks 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

kixelated and others added 3 commits October 28, 2026 06:37
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>
@kixelated kixelated changed the title Replacing an indexed element whose drop may panic fix(ts): replace an indexed element whose drop may panic Oct 28, 2026
@kixelated
kixelated marked this pull request as ready for review October 28, 2026 14:02
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T14:10:50.795539Z 3da824e New commits
🔒 Security Review ✅ Completed 2026-10-28T14:13:04.126379Z 3da824e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@kixelated

Copy link
Copy Markdown
Collaborator Author

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 try, stores through a reference count as visible cleanup, and stable_target holds only the reference root. Main merged in, with #149's conflict resolved by hand. CI passed on 3da824e, and Codex found nothing on either head.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 1af6db6 into main Oct 28, 2026
1 check passed
@kixelated
kixelated deleted the quest/m2/ts-index-cleanup branch October 28, 2026 14:26
kixelated added a commit that referenced this pull request Oct 28, 2026
Regenerates lang's dyns.ts snapshot, which #176 and #177 left stale on main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant