Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/src/internals/rfc-rooting-by-construction.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,47 @@ Rust and become the single source of truth both consume.
Four of five by construction, the fifth by making the frame own its own slot
numbering. That is a strong enough result to justify the work.

### Re-tested against four bugs found after this RFC was written (2026-08-04)

#7341's from-space quarantine produced 31 real stale-pointer bugs and four of
them were fixed (#7373–#7376). Scoring them against this proposal is the
strongest available calibration, because none of them existed when the table
above was written.

| bug | layer | shape | caught? |
|---|---|---|---|
| **#7375** `await` polls a moved promise | 1 (codegen) | promise unboxed, pump helpers emitted, promise reused | **Yes.** The pump calls are `&mut` emits, so the `Raw` is dead at the back-edge. The author must hold a `Rooted` and `get()` per block — which is exactly the fix that landed. |
| **#7373** `JSON.parse` reads a moved input | 3 (runtime) | slice derived, `gc_check_trigger()`, slice used | **No.** Rust locals in `perry-runtime`; this RFC governs `perry-codegen` lowering only. |
| **#7374** `RegExp` flags stored stale | 3 (runtime) | string allocated, `gc_malloc`, pre-collection pointer stored | **No.** Same reason. |
| **#7376** `Symbol` description stored stale | 3 (runtime) | identical to #7374 | **No.** Same reason. |

**One of four.** That is not an argument against the RFC — the one it catches it
catches completely, and #7375 had survived a code comment explicitly reasoning
about the surrounding hazard ("unbox the promise in each block that uses it",
which solves LLVM dominance and not GC movement). It is an argument about
**where the remaining risk lives**: three of the four were layer 3, which this
mechanism cannot reach by construction.

**The sharper finding is that all four were the same defect shape.** Every one
had rooting already — the pattern's root, the receiver's root, the per-block
unbox, the registry for registered symbols. What was missing in each case was
**ordering the root relative to the collection point**. That is precisely the
property this RFC enforces: a `Raw` that dies at the next `&mut` emit makes
"used after a collection point" unrepresentable rather than reviewable.

So the design generalises, and the open question this evidence raises is not
whether to adopt it for codegen, but whether layer 3 needs the same discipline
in `perry-runtime` — where `RuntimeHandleScope` exists (675 uses across 169
files) but is **optional**, and where three of these four bugs lived. A handle
type that made the raw pointer unusable across an allocating call would have
caught all three by the same mechanism.

**Caveat on the sample.** These four are the clusters that were *tractable* from
a backtrace. The eight catches left open in #7341 fault on an argument that is
already stale on entry, so they are caller-side and this RFC would not catch
them either — the value crosses a function boundary, and `Raw<'e>` does not
survive one. Counting them would make the ratio worse, not better.
Comment on lines +193 to +197

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clarify the function-boundary limitation.

Raw<'e> can cross a function boundary when its lifetime propagates. The later “Anything interprocedural” section makes this distinction explicit. The limitation here is that the callee receives an already-stale value and cannot enforce the caller’s missing root ordering.

Proposed wording
-The eight catches left open in `#7341` fault on an argument that is
-already stale on entry, so they are caller-side and this RFC would not catch
-them either — the value crosses a function boundary, and `Raw<'e>` does not
-survive one.
+The eight remaining cases in `#7341` involve arguments that are already stale
+on entry, so they are caller-side and this RFC would not catch them either.
+`Raw<'e>` can cross a function boundary when its lifetime is propagated; the
+limitation here is that the callee receives an already-stale value and cannot
+enforce the caller's missing root ordering.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Caveat on the sample.** These four are the clusters that were *tractable* from
a backtrace. The eight catches left open in #7341 fault on an argument that is
already stale on entry, so they are caller-side and this RFC would not catch
them either — the value crosses a function boundary, and `Raw<'e>` does not
survive one. Counting them would make the ratio worse, not better.
**Caveat on the sample.** These four are the clusters that were *tractable* from
a backtrace. The eight remaining cases in `#7341` involve arguments that are
already stale on entry, so they are caller-side and this RFC would not catch
them either. `Raw<'e>` can cross a function boundary when its lifetime is
propagated; the limitation here is that the callee receives an already-stale
value and cannot enforce the caller's missing root ordering.
Counting them would make the ratio worse, not better.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/internals/rfc-rooting-by-construction.md` around lines 193 - 197,
Revise the caveat in the sample section to distinguish function-boundary
crossings where the Raw<'e> lifetime propagates from calls receiving an
already-stale value. State that the RFC cannot detect the latter because the
callee cannot enforce the caller’s missing root ordering, while preserving the
existing explanation of the eight catches and ratio.


## Migration cost

The honest number is large but the distribution is favourable.
Expand Down
Loading