Skip to content

Split InterpreterFrame hot/cold fields into FrameColdData - #8433

Closed
youknowone wants to merge 11 commits into
RustPython:mainfrom
youknowone:frame-cold-split
Closed

Split InterpreterFrame hot/cold fields into FrameColdData#8433
youknowone wants to merge 11 commits into
RustPython:mainfrom
youknowone:frame-cold-split

Conversation

@youknowone

@youknowone youknowone commented Aug 2, 2026

Copy link
Copy Markdown
Member

Summary

Move 10 rarely-used fields from InterpreterFrame into a lazily-allocated FrameColdData struct, reducing per-frame initialization cost on the hot call path.

Stacked on #8431 — merge #8431 first, then this PR.

Changes

InterpreterFrame carried ~200+ bytes of tracing/debugging fields that are only accessed when:

  • sys.settrace() / sys.setprofile() is active
  • Frame inspection (f_back, f_locals, f_trace) occurs
  • GC traversal runs

These 10 fields are now behind a single UnsafeCell<Option<Box<FrameColdData>>> (8 bytes):

  • trace, trace_lines, trace_opcodes
  • temporary_refs, f_locals_hidden_overlay, f_extra_locals
  • escaped, retained_back
  • pending_stack_pops, pending_unwind_from_stack

The cold() accessor lazily allocates on first use. Frames that never trigger tracing or debugging pay zero allocation cost. GC traversal skips cold data when it has not been allocated.

Files changed

  • frame.rsFrameColdData struct, field migration, cold() accessor, GC traverse update
  • builtins/frame.rs — All f_trace, f_trace_lines, f_trace_opcodes, f_back, clear() accesses
  • object/ext.rsswap_to_temporary_refs() accesses
  • protocol/callable.rs — Trace event handling
  • vm/mod.rsexit_iframe() cleanup, tracing hooks
  • builtins/type.rs__bases__ setter temporary_refs
  • stdlib/_thread.rs — Cross-thread frame materialization

Performance

Incremental call overhead: best measurement 26.8 ns/call (target ≤30 ns).
All frame/traceback/generator/call tests pass.

Summary by CodeRabbit

  • Performance

    • Improved Python function and method execution efficiency through optimized frame handling and tail-call support.
    • Reduced memory overhead for active execution frames by storing infrequently used state separately.
    • Improved handling of suspended calls, generators, coroutines, and deep call chains.
  • Stability

    • Strengthened frame cleanup, tracing, exception propagation, and garbage-collection behavior during complex execution flows.
  • Chores

    • Removed the automatic environment setup hook from the project configuration.

Extract the frame entry (recursion check, TLS link, exception save)
and exit (materialization sync, TLS restore, GC tracking) logic
from with_iframe into standalone enter_iframe/exit_iframe methods.
with_iframe now calls them, with no behavioral change.

This prepares for the trampoline loop where enter/exit are called
individually rather than wrapped around a closure.

Assisted-by: Claude
Add InterpreterFrame::new_on_datastack() that bump-allocates both the
InterpreterFrame struct and its LocalsPlus data array in a single
datastack push, eliminating one allocation per function call.

Update datastack_frame_size_bytes_for_code() to include InterpreterFrame
size. Convert invoke_prepared_exact_args() and the invoke() fast path to
use the combined allocation.

Add release_datastack_frame() method on InterpreterFrame that drops all
localsplus values, runs field destructors (trace, temporary_refs,
retained_back, etc.), and returns the datastack base pointer for pop.

Assisted-by: Claude
Add ExecutionResult::TailCall variant and a trampoline in
run_frame_fast that flattens Python-to-Python calls into a single
Rust stack frame instead of recursing through the eval loop.

CallPyExactArgs now prepares the callee frame on the datastack and
returns TailCall when tailcall_enabled is set (run_iframe path only).
The trampoline dispatches via a state machine (EnterCallee / ReturnValue
/ Unwind) in a single loop, avoiding mutual recursion between helper
functions that would exhaust the C stack.

Exception propagation through suspended frames uses
trampoline_handle_exception which adds traceback entries and calls
unwind_blocks on each caller.

Assisted-by: Claude
…, add bound method TailCall

- Add enter_iframe_unchecked for trampoline callee entry (recursion
  already checked by specialization_call_recursion_guard)
- Move callable ownership from per-frame temporary_refs mutex to
  trampoline-local SuspendedFrame.owned_refs via VM side channel
- Add TailCall support for CallBoundMethodExactArgs
- Move args directly from caller stack to callee fastlocals
- Read materialized pointer once in exit_iframe

Incremental call overhead: ~55 ns -> ~35 ns

Assisted-by: Claude
The VM is per-thread so RefCell's runtime borrow checking is
unnecessary overhead. Replace with UnsafeCell for direct access.

Assisted-by: Claude
Use NonNull + Option instead of raw *mut T with manual null checks.
The compiler enforces non-null via the type system, and Option<NonNull>
has the same size as a raw pointer thanks to niche optimization.
Also extract take_pending_tailcall helper to deduplicate the pattern.

Assisted-by: Claude
Rename SendNonNull to PendingFrame and make it fully private: the
struct, its field, and the pending_tailcall_frame Cell are all
non-pub. External code accesses the side channel only through
set_pending_tailcall (pub(crate)) and take_pending_tailcall (private).

This ensures the unsafe Send+Sync impl cannot be reused elsewhere
without justifying a new safety argument.

Assisted-by: Claude
enter_iframe_unchecked was skipping C-stack checks under the
assumption that the trampoline stays in one Rust stack frame. But
each run_iframe call still consumes Rust stack, so deep Python
recursion through the trampoline can exhaust the C stack (observed
as STATUS_STACK_OVERFLOW on Windows CI).

Keep the C-stack check (every 8th call) while still skipping the
Python recursion depth check (already done by
specialization_call_recursion_guard).

Assisted-by: Claude
- Fix double-free: mark entry frame with is_entry flag in
  SuspendedFrame so the trampoline skips its datastack release
  (the caller owns that cleanup)
- Clear iframe.previous in exit_iframe before unlinking the chain,
  matching with_frame and resume_gen_frame behavior
- Add scopeguard in with_iframe for panic safety
- Use saturating_sub(1) for lasti in trampoline_handle_exception
- Extract datastack_iframe_localsplus_offset helper to avoid
  duplicated alignment computation

Assisted-by: Claude
Move 10 rarely-used fields (trace, trace_lines, trace_opcodes,
temporary_refs, f_locals_hidden_overlay, f_extra_locals, escaped,
retained_back, pending_stack_pops, pending_unwind_from_stack) from
InterpreterFrame into a lazily-allocated FrameColdData struct.

InterpreterFrame now carries a single UnsafeCell<Option<Box<FrameColdData>>>
(8 bytes) instead of ~200+ bytes of cold fields. The cold() accessor
allocates on first access; frames that never trigger tracing or
debugging pay no allocation cost.

GC traversal skips cold data when it has not been allocated.

Assisted-by: Claude
Copilot AI review requested due to automatic review settings August 2, 2026 17:19
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The VM stores infrequently used frame state in lazy cold data, co-allocates frames with locals on the datastack, and executes eligible Python calls through an iterative tail-call trampoline. The PR also updates related frame accessors and removes the Claude session-start hook.

Changes

VM frame execution

Layer / File(s) Summary
Lazy cold frame state
crates/vm/src/frame.rs, crates/vm/src/builtins/frame.rs, crates/vm/src/builtins/type.rs, crates/vm/src/object/ext.rs, crates/vm/src/protocol/callable.rs, crates/vm/src/stdlib/_thread.rs, crates/vm/src/vm/mod.rs
Frame tracing, temporary references, locals overlays, retained callers, deferred unwind state, GC traversal, and cleanup now use FrameColdData.
Datastack frame lifecycle
crates/vm/src/frame.rs, crates/vm/src/builtins/function.rs, crates/vm/src/vm/mod.rs
Frames and LocalsPlus can share datastack allocation. Fast function calls use owned frames and explicit iframe entry and exit APIs.
Iterative tail-call trampoline
crates/vm/src/frame.rs, crates/vm/src/vm/mod.rs, crates/vm/src/vm/thread.rs, crates/vm/src/coroutine.rs
Exact Python-function and bound-method calls prepare callee frames for trampoline execution, which handles suspended callers, returns, exceptions, and cleanup.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: copilot, shaharnaveh, bschoenmaeckers

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: moving rarely used InterpreterFrame fields into FrameColdData.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR reduces hot-path InterpreterFrame size and initialization cost by moving tracing/debug/inspection state into a lazily allocated FrameColdData, and updates the VM/frame execution paths to work with the new hot/cold split (including TailCall/trampoline plumbing and datastack frame lifecycle updates).

Changes:

  • Introduces FrameColdData and migrates rarely-used InterpreterFrame fields behind a lazy cold() accessor; updates GC traversal to skip cold data when unallocated.
  • Updates frame-related call sites (f_trace*, retained_back, temporary_refs, stack unwind bookkeeping) to route through cold().
  • Extends the TailCall/trampoline and datastack frame lifecycle code to cooperate with the new frame layout and cleanup semantics.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/vm/src/frame.rs Adds FrameColdData, cold() accessor, datastack co-allocation helpers, TailCall result + trampoline exception helper; updates traversal and field accesses.
crates/vm/src/vm/mod.rs Adds TailCall side channels on VirtualMachine, trampoline loop, and refactors with_iframe into enter_iframe/exit_iframe.
crates/vm/src/vm/thread.rs Initializes new TailCall side-channel fields when creating a threaded VM.
crates/vm/src/builtins/frame.rs Routes Python frame attribute accessors/mutators and clear paths through cold() fields.
crates/vm/src/protocol/callable.rs Updates opcode tracing and per-frame trace clearing to use cold() fields.
crates/vm/src/object/ext.rs Routes temporary_refs bookkeeping through cold().
crates/vm/src/stdlib/_thread.rs Updates cross-thread frame chain materialization to use cold().retained_back.
crates/vm/src/builtins/type.rs Routes temporary keep-alives through cold().temporary_refs.
crates/vm/src/builtins/function.rs Switches fast-path call frame creation to InterpreterFrame::new_on_datastack and updates datastack sizing helper.
crates/vm/src/coroutine.rs Marks TailCall as unreachable for generator/coroutine execution paths.
.claude/settings.json Removes the Claude session-start hook configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/vm/src/frame.rs
Comment on lines +1276 to +1281
/// Access the lazily-allocated cold data, allocating on first use.
#[inline]
pub(crate) fn cold(&self) -> &FrameColdData {
let ptr = self.cold.get();
unsafe { (*ptr).get_or_insert_with(|| Box::new(FrameColdData::default())) }
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (1)
crates/vm/src/vm/mod.rs (1)

2158-2184: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove the duplicated C-stack check in enter_iframe.

enter_iframe runs depth & 7 == 0 && self.check_c_stack_overflow() at Line 2165, then calls enter_iframe_unchecked, which runs the identical check at Line 2182. The check executes twice on every non-trampoline frame entry. enter_iframe_unchecked already documents that it keeps the C-stack check, so enter_iframe only needs check_recursive_call.

♻️ Proposed simplification
     ) -> PyResult<IframeEntryState> {
         self.check_recursive_call("")?;
-
-        let depth = self.recursion_depth.get();
-        if depth & 7 == 0 && self.check_c_stack_overflow() {
-            return Err(self.new_recursion_error(String::new()));
-        }
-
         self.enter_iframe_unchecked(iframe)
     }
🤖 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 `@crates/vm/src/vm/mod.rs` around lines 2158 - 2184, Remove the duplicated
depth-gated check_c_stack_overflow block from enter_iframe, leaving
check_recursive_call and the call to enter_iframe_unchecked intact. Keep the
existing C-stack validation exclusively in enter_iframe_unchecked.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/vm/src/frame.rs`:
- Around line 2321-2350: Extract the duplicated ExecutingFrame initialization
into a shared constructor such as ExecutingFrame::from_iframe, accepting iframe,
tailcall_enabled, resolve_builtins_dict, and vm. Replace the construction blocks
in Py<FrameObject>::with_exec, Py<FrameObject>::yield_from_target,
trampoline_handle_exception, and run_iframe, passing each site’s differing
builtins_dict behavior and tailcall_enabled value while preserving all existing
field assignments.
- Around line 10635-10668: The prepared callee frame is not cleaned up if
argument transfer unwinds before publication. In the frame-preparation flow
around InterpreterFrame::new_on_datastack and vm.set_pending_tailcall, add an
unwind guard that releases the callee data-stack frame and restores the caller
stack base, then disarm it only after set_pending_tailcall succeeds; apply the
same protection to tailcall_prepare_bound_method_frame.
- Around line 10604-10726: Merge tailcall_prepare_frame and
tailcall_prepare_bound_method_frame by extracting a shared helper that accepts
the resolved PyFunction, stack argument count, and optional self value. Move the
shared locals creation, datastack frame construction, fastlocals population,
stack cleanup, and pending-tailcall publication into that helper. Update both
existing methods to resolve their function and self values, then delegate while
preserving ownership transfer for the callable and bound-function references.
- Around line 2308-2369: Update trampoline_handle_exception to emit
monitoring::fire_py_unwind when EVENT_PY_UNWIND is enabled, using the computed
call-site offset idx as u32 * 2, immediately before invoking unwind_blocks.
Preserve the existing traceback setup and unwind result propagation.
- Around line 871-881: Make the lazy initialization performed by
FrameObject::cold race-safe instead of mutating
UnsafeCell<Option<Box<FrameColdData>>> through a shared reference. Either
enforce owner-thread/STW-only access before allocation across cold() and its
tracing/accessor callers, or replace the storage and publication path with
atomic initialization that guarantees one shared FrameColdData allocation is
retained and visible to concurrent readers.
- Around line 1455-1458: Update the release or compatibility notes to record the
public ExecutionResult::TailCall variant addition and identify it as a breaking
API change requiring downstream exhaustive matches to handle the new variant.

In `@crates/vm/src/vm/mod.rs`:
- Around line 1424-1442: The fast-call execution paths need unwind-safe cleanup.
In crates/vm/src/vm/mod.rs lines 1424-1442, guard entry_state with scopeguard
around crate::frame::run_iframe, including each run_iframe call and suspended
frame_stack handling in run_frame_fast_trampoline. In
crates/vm/src/builtins/function.rs lines 632-650, guard the iframe so
release_datastack_frame and vm.datastack_pop(base) execute if vm.run_frame_fast
unwinds; apply the same protection around vm.run_frame_fast(iframe) in
invoke_prepared_exact_args at lines 803-827.
- Around line 2387-2389: Reformat the condition in the traced-frame return path
with default rustfmt, and reorder its operands to check
self.is_none(&self.profile_func.borrow()) before accessing
frame.iframe().cold().trace, avoiding unnecessary cold-block allocation for
profile-only frames while preserving the existing logic.
- Around line 2249-2253: Update the retained_back assignment in exit_iframe to
preserve an existing caller reference: lock retained_back, assign the
materialize_chain result only when the guard is None, and leave the existing
value unchanged otherwise. Keep the old_chain check and frame materialization
flow intact.
- Around line 1534-1686: Extract the repeated caller cleanup into a private
helper associated with SuspendedFrame or the trampoline scope, preserving the
order of dropping owned references, calling exit_iframe, and conditionally
releasing and popping the datastack frame; replace every duplicated cleanup
block in ReturnValue and Unwind handling. Also extract the repeated TailCall
SuspendedFrame re-push logic into a helper and use it in all three TailCall
arms, preserving pending-tailcall reference draining and callee transfer.

---

Nitpick comments:
In `@crates/vm/src/vm/mod.rs`:
- Around line 2158-2184: Remove the duplicated depth-gated
check_c_stack_overflow block from enter_iframe, leaving check_recursive_call and
the call to enter_iframe_unchecked intact. Keep the existing C-stack validation
exclusively in enter_iframe_unchecked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: e3a61024-2055-409e-8bf6-fd0c6e1c48c0

📥 Commits

Reviewing files that changed from the base of the PR and between 12f3646 and 212d9c1.

📒 Files selected for processing (11)
  • .claude/settings.json
  • crates/vm/src/builtins/frame.rs
  • crates/vm/src/builtins/function.rs
  • crates/vm/src/builtins/type.rs
  • crates/vm/src/coroutine.rs
  • crates/vm/src/frame.rs
  • crates/vm/src/object/ext.rs
  • crates/vm/src/protocol/callable.rs
  • crates/vm/src/stdlib/_thread.rs
  • crates/vm/src/vm/mod.rs
  • crates/vm/src/vm/thread.rs
💤 Files with no reviewable changes (1)
  • .claude/settings.json

Comment thread crates/vm/src/frame.rs
Comment on lines +871 to +881
/// Base pointer of the datastack allocation when this frame and its
/// localsplus are bump-allocated together. Null for heap-backed frames.
pub(crate) datastack_base: *mut u8,
/// Pointer to the owning `Py<FrameObject>`, or null for stack-allocated
/// frames that have not been materialized yet.
/// Stored as `usize` for `PyAtomic` compatibility.
pub(crate) materialized: PyAtomic<usize>,

/// Lazily-allocated cold data (tracing, debugging, frame inspection).
/// `None` until first access via `cold()`.
pub(crate) cold: UnsafeCell<Option<Box<FrameColdData>>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate any unsafe Sync/Send impls for the frame types.
rg -nP -C4 'unsafe\s+impl\s+(Sync|Send)\s+for\s+(InterpreterFrame|FrameObject|FrameUnsafeCell)' crates/vm/src

# List every cold() call site with context to classify owner-thread vs cross-thread.
rg -nP -C3 '\.cold\(\)' crates/vm/src

# Check which FrameObject getters guard with check_locals_access.
rg -nP -C6 'fn (f_trace|set_f_trace|f_trace_lines|set_f_trace_lines|f_trace_opcodes|set_f_trace_opcodes)\b' crates/vm/src/builtins/frame.rs

Repository: RustPython/RustPython

Length of output: 26925


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the unsafe Sync impl context and the cold() accessor.
sed -n '860,900p' crates/vm/src/frame.rs
sed -n '1260,1290p' crates/vm/src/frame.rs

# Inspect the FrameObject definitions and any unsafe Sync for frame object storage.
rg -n -C3 'struct FrameObject|type FrameObject|unsafe impl (Sync|Send)|pyclass|PyAtomic' crates/vm/src/frame.rs

# Inspect VM current_frame and owner checks around frame access.
sed -n '1130,1165p' crates/vm/src/frame.rs
rg -n -C4 'fn current_frame|current_frame\(' crates/vm/src/vm/mod.rs crates/vm/src/frame.rs

# Read only relevant source definitions and call sites with enough context to see guard usage.
sed -n '620,750p' crates/vm/src/builtins/frame.rs
sed -n '2100,2130p' crates/vm/src/vm/mod.rs
sed -n '2368,2385p' crates/vm/src/vm/mod.rs
sed -n '830,920p' crates/vm/src/builtins/frame.rs

Repository: RustPython/RustPython

Length of output: 34515


Make cold() race-safe on FrameObject payloads.

cold() writes a value into UnsafeCell<Option<Box<FrameColdData>>> through a shared reference. The frame types are marked Send/Sync, and tracing/accessor methods such as f_trace, set_f_trace, f_trace_lines, set_f_trace_lines, f_trace_opcodes, and set_f_trace_opcodes reach it from any thread holding the FrameObject without ownership/STW guards before allocation. Concurrent first access can allocate two boxes and leave Py<FrameObject> with a reference to an unshared/dropped value.

Restrict lazy allocation to owner-thread or STW contexts, or publish the FrameColdData pointer atomically so initialization is race-free.

🤖 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 `@crates/vm/src/frame.rs` around lines 871 - 881, Make the lazy initialization
performed by FrameObject::cold race-safe instead of mutating
UnsafeCell<Option<Box<FrameColdData>>> through a shared reference. Either
enforce owner-thread/STW-only access before allocation across cold() and its
tracing/accessor callers, or replace the storage and publication path with
atomic initialization that guarantees one shared FrameColdData allocation is
retained and visible to concurrent readers.

Comment thread crates/vm/src/frame.rs
Comment on lines +1455 to +1458
/// The bytecode loop wants to tail-call into a new frame that has
/// already been prepared on the datastack. The trampoline reads the
/// pending frame pointer from `vm.pending_tailcall_frame`.
TailCall,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every match on ExecutionResult across the workspace.
rg -nP -C4 'ExecutionResult::' crates

Repository: RustPython/RustPython

Length of output: 16683


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ExecutionResult declaration and exports/usages =="
rg -n 'pub enum ExecutionResult|ExecutionResult' crates/vm/Cargo.toml crates/vm/src -g '*.rs' | head -200

echo
echo "== workspace Cargo references to vm crate =="
rg -n 'rustpython[-_ ]?vm|rustpython_vm|vm]' Cargo.toml crates/*/Cargo.toml crates/*/*/Cargo.toml 2>/dev/null || true

echo
echo "== match expressions that might pattern-match ExecutionResult without ExecutionResult:: =="
python3 - <<'PY'
import pathlib, re
for p in pathlib.Path('crates').rglob('*.rs'):
    text=p.read_text(errors='ignore')
    if 'ExecutionResult' not in text:
        continue
    for i,line in enumerate(text.splitlines(), start=1):
        if 'ExecutionResult' in line:
            print(f"{p}:{i}:{line}")
PY

echo
echo "== public exports context =="
sed -n '1,200p' crates/vm/src/lib.rs
sed -n '1420,1445p' crates/vm/src/frame.rs
sed -n '80,112p' crates/vm/src/coroutine.rs

Repository: RustPython/RustPython

Length of output: 15715


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate release notes files =="
fd -i 'rename|release|changelog|news|changes' . -t f -x sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' sh {}

echo
echo "== TailCall mentions across tracked files =="
rg -n 'TailCall|tailcall|execution result|ExecutionResult' . --glob '!target/**' --glob '!__pycache__/**' | sed -n '1,240p'

Repository: RustPython/RustPython

Length of output: 38802


Record the ExecutionResult::TailCall public API addition.

ExecutionResult is public, so external downstream crates that exhaustively match on it will need to handle TailCall. Update the release/compat notes for this breaking API change.

🤖 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 `@crates/vm/src/frame.rs` around lines 1455 - 1458, Update the release or
compatibility notes to record the public ExecutionResult::TailCall variant
addition and identify it as a breaking API change requiring downstream
exhaustive matches to handle the new variant.

Comment thread crates/vm/src/frame.rs
Comment on lines +2308 to +2369
/// Handle an exception propagating into a suspended caller frame in the
/// trampoline. Adds a traceback entry at the caller's call site, then
/// tries the caller's exception table via `unwind_blocks`.
///
/// Returns:
/// - `Ok(None)` — handler found, the caller's `run_iframe` can be re-entered
/// - `Ok(Some(result))` — handler returned a result (break from the run loop)
/// - `Err(exc)` — no handler, exception propagates to the next caller
pub(crate) fn trampoline_handle_exception(
iframe: &mut InterpreterFrame,
exception: &PyBaseExceptionRef,
vm: &VirtualMachine,
) -> FrameResult {
let code: &Py<PyCode> = unsafe { &*iframe.code };
let globals: &Py<PyDict> = unsafe { &*iframe.globals };
let builtins: &PyObject = unsafe { &*iframe.builtins };
let func_obj: Option<&PyObject> = if iframe.func_obj.is_null() {
None
} else {
Some(unsafe { &*iframe.func_obj })
};
let builtins_dict = if globals.class().is(vm.ctx.types.dict_type) {
builtins
.downcast_ref_if_exact::<PyDict>(vm)
.map(|d| unsafe { PyExact::ref_unchecked(d) })
} else {
None
};
let iframe_ptr = iframe as *const InterpreterFrame;
let mut exec = ExecutingFrame {
code,
localsplus: &mut iframe.localsplus,
locals: &iframe.locals,
globals,
builtins,
builtins_dict,
lasti: &iframe.lasti,
iframe: iframe_ptr,
func_obj,
prev_line: &mut iframe.prev_line,
monitoring_mask: 0,
tailcall_enabled: false,
};

// lasti points past the CallPyExactArgs instruction (+ cache entries).
// The exception occurred at the previous instruction (the call site).
let idx = (exec.lasti() as usize).saturating_sub(1);

// Add traceback entry at the call site.
if let Some((loc, _end_loc)) = exec.code.locations.get(idx) {
let next = exception.__traceback__();
let new_traceback = PyTraceback::new(next, exec.frame_object(vm), idx as u32 * 2, loc.line);
exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx)));
}

exec.unwind_blocks(
vm,
UnwindReason::Raising {
exception: exception.clone(),
},
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Show the monitoring fires in the recursive unwind path.
rg -nP -C3 'EVENT_PY_UNWIND|fire_py_unwind|EVENT_RERAISE|fire_reraise' crates/vm/src

# Check whether tail-call eligibility consults monitoring state.
rg -nP -C6 'tailcall_enabled|is_generator_like|specialization_eval_frame_active' crates/vm/src/frame.rs

Repository: RustPython/RustPython

Length of output: 28309


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the recursive unwind/trace handling and trampoline setup, plus monitoring event handling.
sed -n '2160,2275p' crates/vm/src/frame.rs
sed -n '2320,2380p' crates/vm/src/frame.rs
sed -n '3130,3230p' crates/vm/src/frame.rs
sed -n '3425,3470p' crates/vm/src/frame.rs

# Inspect monitoring event fire helpers.
sed -n '960,1035p' crates/vm/src/stdlib/sys/monitoring.rs

# Inspect tailcall_prepare_frame guard locations.
rg -n "tailcall_prepare_frame|is_generator_like|specialization_eval_frame_active" crates/vm/src/frame.rs

Repository: RustPython/RustPython

Length of output: 17703


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the trampoline call instruction implementation, tailcall preparation, and resume path.
sed -n '5800,5865p' crates/vm/src/frame.rs
sed -n '10090,10145p' crates/vm/src/frame.rs
sed -n '2280,2320p' crates/vm/src/frame.rs

# Inspect where RunIfResume instructions or suspended frames re-enter the trampoline.
rg -n -C4 'RunIfResume|trampoline_handle_exception|resume\(' crates/vm/src/frame.rs

Repository: RustPython/RustPython

Length of output: 8919


Fire PY_UNWIND monitoring in trampoline_handle_exception.

In recursive frame execution, monitoring::fire_py_unwind is fired when the exception escapes the frame. The trampoline path does not enable tailcall_enabled, so tail-call preparation is skipped and sys.settrace is unaffected, but sys.monitoring still reads vm.state.monitoring_events and expects monitoring events. If this path is reachable with EVENT_PY_UNWIND enabled, emit fire_py_unwind here with idx as u32 * 2 before returning to unwind_blocks.

🤖 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 `@crates/vm/src/frame.rs` around lines 2308 - 2369, Update
trampoline_handle_exception to emit monitoring::fire_py_unwind when
EVENT_PY_UNWIND is enabled, using the computed call-site offset idx as u32 * 2,
immediately before invoking unwind_blocks. Preserve the existing traceback setup
and unwind result propagation.

Comment thread crates/vm/src/frame.rs
Comment on lines +2321 to +2350
let code: &Py<PyCode> = unsafe { &*iframe.code };
let globals: &Py<PyDict> = unsafe { &*iframe.globals };
let builtins: &PyObject = unsafe { &*iframe.builtins };
let func_obj: Option<&PyObject> = if iframe.func_obj.is_null() {
None
} else {
Some(unsafe { &*iframe.func_obj })
};
let builtins_dict = if globals.class().is(vm.ctx.types.dict_type) {
builtins
.downcast_ref_if_exact::<PyDict>(vm)
.map(|d| unsafe { PyExact::ref_unchecked(d) })
} else {
None
};
let iframe_ptr = iframe as *const InterpreterFrame;
let mut exec = ExecutingFrame {
code,
localsplus: &mut iframe.localsplus,
locals: &iframe.locals,
globals,
builtins,
builtins_dict,
lasti: &iframe.lasti,
iframe: iframe_ptr,
func_obj,
prev_line: &mut iframe.prev_line,
monitoring_mask: 0,
tailcall_enabled: false,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the repeated ExecutingFrame construction.

The same construction block now appears four times: Py<FrameObject>::with_exec, Py<FrameObject>::yield_from_target, trampoline_handle_exception, and run_iframe. The blocks differ only in builtins_dict and tailcall_enabled. Add one constructor, for example ExecutingFrame::from_iframe(iframe, tailcall_enabled, resolve_builtins_dict, vm), and call it from all four sites.

As per coding guidelines: "When branches differ only in a value but share logic, extract the differing value and invoke the common logic once."

🤖 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 `@crates/vm/src/frame.rs` around lines 2321 - 2350, Extract the duplicated
ExecutingFrame initialization into a shared constructor such as
ExecutingFrame::from_iframe, accepting iframe, tailcall_enabled,
resolve_builtins_dict, and vm. Replace the construction blocks in
Py<FrameObject>::with_exec, Py<FrameObject>::yield_from_target,
trampoline_handle_exception, and run_iframe, passing each site’s differing
builtins_dict behavior and tailcall_enabled value while preserving all existing
field assignments.

Source: Coding guidelines

Comment thread crates/vm/src/frame.rs
Comment on lines +10604 to +10726
/// Prepare a callee frame on the datastack for a TailCall.
/// Pops args, self_or_null, and callable from the caller's stack,
/// builds the callee InterpreterFrame, and stores its pointer in
/// `vm.pending_tailcall_frame`.
///
/// The callable must be at stack position `nargs + 1` (already validated).
fn tailcall_prepare_frame(
&mut self,
nargs: u32,
self_or_null_is_some: bool,
vm: &VirtualMachine,
) {
let base = usize::from(self_or_null_is_some);
let effective_nargs = nargs as usize + base;

// Peek at the callable (still on the stack) to build the callee
// frame. The callable stays on the caller's stack until we're done
// constructing the callee.
let callable = self.nth_value(nargs + 1);
let func = callable.downcast_ref_if_exact::<PyFunction>(vm).unwrap();

let code: &Py<PyCode> = &func.code;

let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) {
FrameLocals::lazy()
} else {
FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact(
func.globals.clone(),
))
};

let callee_iframe = InterpreterFrame::new_on_datastack(
code,
&func.globals,
&func.builtins,
Some(func.as_object()),
locals,
func.closure.as_ref().map_or(&[], |c| c.as_slice()),
vm,
);

// Move args directly from the caller's stack into callee fastlocals,
// avoiding an intermediate buffer.
{
let fastlocals = callee_iframe.localsplus.fastlocals_mut();
for (dst, arg) in fastlocals[base..effective_nargs]
.iter_mut()
.zip(self.pop_multiple(nargs as usize))
{
*dst = Some(arg);
}
let self_or_null = self.pop_value_opt();
debug_assert_eq!(self_or_null.is_some(), self_or_null_is_some);
if self_or_null.is_some() {
fastlocals[0] = self_or_null;
}
}

// Pop the callable and transfer ownership to the trampoline via
// the VM side channel, avoiding a per-frame mutex lock on
// temporary_refs.
let callable = self.pop_value();
unsafe { &mut *vm.pending_tailcall_refs.get() }.push(callable);

vm.set_pending_tailcall(callee_iframe);
}

/// Prepare a callee frame for a bound method TailCall.
/// Pops args, self_or_null (null), and callable from the caller's stack,
/// builds the callee InterpreterFrame with bound_self prepended, and
/// stores its pointer in `vm.pending_tailcall_frame`.
fn tailcall_prepare_bound_method_frame(
&mut self,
nargs: u32,
bound_function: PyObjectRef,
bound_self: PyObjectRef,
vm: &VirtualMachine,
) {
let effective_nargs = nargs as usize + 1; // +1 for bound_self

let func = bound_function
.downcast_ref_if_exact::<PyFunction>(vm)
.unwrap();
let code: &Py<PyCode> = &func.code;

let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) {
FrameLocals::lazy()
} else {
FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact(
func.globals.clone(),
))
};

let callee_iframe = InterpreterFrame::new_on_datastack(
code,
&func.globals,
&func.builtins,
Some(func.as_object()),
locals,
func.closure.as_ref().map_or(&[], |c| c.as_slice()),
vm,
);

// Move args directly from the caller's stack into callee fastlocals.
let fastlocals = callee_iframe.localsplus.fastlocals_mut();
for (dst, arg) in fastlocals[1..effective_nargs]
.iter_mut()
.zip(self.pop_multiple(nargs as usize))
{
*dst = Some(arg);
}
self.pop_value_opt(); // null (self_or_null)
let callable = self.pop_value(); // callable (bound method)
fastlocals[0] = Some(bound_self);

// Transfer ownership to the trampoline via the VM side channel.
let refs = unsafe { &mut *vm.pending_tailcall_refs.get() };
refs.push(bound_function);
refs.push(callable);

vm.set_pending_tailcall(callee_iframe);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Merge the two tail-call preparation functions.

tailcall_prepare_frame and tailcall_prepare_bound_method_frame share the locals selection, the new_on_datastack call, the argument move loop, and the pending-ref publication. They differ only in where the callee function comes from and what fills fastlocals[0].

Extract one helper that takes the resolved &Py<PyFunction>, the number of stack arguments, and an optional self value, then let both call sites supply those three values.

As per coding guidelines: "When branches differ only in a value but share logic, extract the differing value and invoke the common logic once."

🤖 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 `@crates/vm/src/frame.rs` around lines 10604 - 10726, Merge
tailcall_prepare_frame and tailcall_prepare_bound_method_frame by extracting a
shared helper that accepts the resolved PyFunction, stack argument count, and
optional self value. Move the shared locals creation, datastack frame
construction, fastlocals population, stack cleanup, and pending-tailcall
publication into that helper. Update both existing methods to resolve their
function and self values, then delegate while preserving ownership transfer for
the callable and bound-function references.

Source: Coding guidelines

Comment thread crates/vm/src/frame.rs
Comment on lines +10635 to +10668
let callee_iframe = InterpreterFrame::new_on_datastack(
code,
&func.globals,
&func.builtins,
Some(func.as_object()),
locals,
func.closure.as_ref().map_or(&[], |c| c.as_slice()),
vm,
);

// Move args directly from the caller's stack into callee fastlocals,
// avoiding an intermediate buffer.
{
let fastlocals = callee_iframe.localsplus.fastlocals_mut();
for (dst, arg) in fastlocals[base..effective_nargs]
.iter_mut()
.zip(self.pop_multiple(nargs as usize))
{
*dst = Some(arg);
}
let self_or_null = self.pop_value_opt();
debug_assert_eq!(self_or_null.is_some(), self_or_null_is_some);
if self_or_null.is_some() {
fastlocals[0] = self_or_null;
}
}

// Pop the callable and transfer ownership to the trampoline via
// the VM side channel, avoiding a per-frame mutex lock on
// temporary_refs.
let callable = self.pop_value();
unsafe { &mut *vm.pending_tailcall_refs.get() }.push(callable);

vm.set_pending_tailcall(callee_iframe);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

The prepared callee frame leaks its data-stack allocation if this function unwinds.

InterpreterFrame::new_on_datastack reserves the callee frame at line 10635. vm.set_pending_tailcall publishes it at line 10668. Between those two points the frame is owned by nobody:

  • self.pop_multiple(nargs as usize) panics on stack underflow.
  • Overwriting a fastlocal slot drops the previous value, and a __del__ implementation can panic.

If either happens, the callee allocation stays on the data stack and release_datastack_frame never runs. The trampoline never sees the frame, so it cannot clean it up either. Compare PyFunction::invoke_prepared_exact_args in crates/vm/src/builtins/function.rs, which pairs the allocation with an unconditional release.

Guard the region, for example with scopeguard::guard that releases the callee frame and pops the base on unwind, and disarm the guard after set_pending_tailcall succeeds. The same gap exists in tailcall_prepare_bound_method_frame at lines 10697-10724.

🤖 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 `@crates/vm/src/frame.rs` around lines 10635 - 10668, The prepared callee frame
is not cleaned up if argument transfer unwinds before publication. In the
frame-preparation flow around InterpreterFrame::new_on_datastack and
vm.set_pending_tailcall, add an unwind guard that releases the callee data-stack
frame and restores the caller stack base, then disarm it only after
set_pending_tailcall succeeds; apply the same protection to
tailcall_prepare_bound_method_frame.

Comment thread crates/vm/src/vm/mod.rs
Comment on lines 1424 to +1442
pub fn run_frame_fast(&self, iframe: &mut crate::frame::InterpreterFrame) -> PyResult {
match self.with_iframe(iframe, |iframe| crate::frame::run_iframe(iframe, self))? {
ExecutionResult::Return(value) => Ok(value),
_ => panic!("Got unexpected result from function"),
use crate::frame::ExecutionResult;

let entry_state = self.enter_iframe(iframe)?;
let result = crate::frame::run_iframe(iframe, self);

match result {
Ok(ExecutionResult::Return(value)) => {
self.exit_iframe(entry_state);
Ok(value)
}
Ok(ExecutionResult::TailCall) => self.run_frame_fast_trampoline(iframe, entry_state),
Ok(ExecutionResult::Yield(_)) => panic!("Yield in non-generator frame"),
Err(exc) => {
self.exit_iframe(entry_state);
Err(exc)
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The fast-call path has no unwind-safe frame teardown. with_iframe now guards exit_iframe with scopeguard, but the datastack fast-call path does not. If run_iframe unwinds, the frame stays on the thread-local frame chain, recursion_depth stays incremented, and the datastack allocation is never popped or dropped.

  • crates/vm/src/vm/mod.rs#L1424-L1442: wrap entry_state in a scopeguard around the crate::frame::run_iframe call, and apply the same protection to the run_iframe calls and the suspended frame_stack inside run_frame_fast_trampoline.
  • crates/vm/src/builtins/function.rs#L632-L650: guard the iframe so release_datastack_frame and vm.datastack_pop(base) run when vm.run_frame_fast unwinds.
  • crates/vm/src/builtins/function.rs#L803-L827: apply the same guard around vm.run_frame_fast(iframe) in invoke_prepared_exact_args.
📍 Affects 2 files
  • crates/vm/src/vm/mod.rs#L1424-L1442 (this comment)
  • crates/vm/src/builtins/function.rs#L632-L650
  • crates/vm/src/builtins/function.rs#L803-L827
🤖 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 `@crates/vm/src/vm/mod.rs` around lines 1424 - 1442, The fast-call execution
paths need unwind-safe cleanup. In crates/vm/src/vm/mod.rs lines 1424-1442,
guard entry_state with scopeguard around crate::frame::run_iframe, including
each run_iframe call and suspended frame_stack handling in
run_frame_fast_trampoline. In crates/vm/src/builtins/function.rs lines 632-650,
guard the iframe so release_datastack_frame and vm.datastack_pop(base) execute
if vm.run_frame_fast unwinds; apply the same protection around
vm.run_frame_fast(iframe) in invoke_prepared_exact_args at lines 803-827.

Comment thread crates/vm/src/vm/mod.rs
Comment on lines +1534 to +1686
Action::ReturnValue(value) => {
let Some(caller) = frame_stack.pop() else {
// All frames consumed — this is the final return.
return Ok(value);
};
let SuspendedFrame {
iframe: caller_iframe_ptr,
entry_state: caller_entry,
owned_refs: _caller_refs,
is_entry: caller_is_entry,
} = caller;
let caller_iframe = unsafe { &mut *caller_iframe_ptr };
caller_iframe.localsplus.push_stack(value);

let result = crate::frame::run_iframe(caller_iframe, self);
match result {
Ok(ExecutionResult::TailCall) => {
let refs = unsafe { &mut *self.pending_tailcall_refs.get() }
.drain(..)
.collect();
drop(_caller_refs);
frame_stack.push(SuspendedFrame {
iframe: caller_iframe_ptr,
entry_state: caller_entry,
owned_refs: refs,
is_entry: caller_is_entry,
});
action = Action::EnterCallee(self.take_pending_tailcall());
}
Ok(ExecutionResult::Return(value)) => {
drop(_caller_refs);
self.exit_iframe(caller_entry);
if !caller_is_entry {
unsafe {
if let Some(base) = caller_iframe.release_datastack_frame() {
self.datastack_pop(base);
}
}
}
action = Action::ReturnValue(value);
}
Ok(ExecutionResult::Yield(_)) => panic!("Yield in non-generator frame"),
Err(exc) => {
drop(_caller_refs);
self.exit_iframe(caller_entry);
if !caller_is_entry {
unsafe {
if let Some(base) = caller_iframe.release_datastack_frame() {
self.datastack_pop(base);
}
}
}
action = Action::Unwind(exc);
}
}
}

Action::Unwind(exc) => {
let Some(caller) = frame_stack.pop() else {
return Err(exc);
};
let SuspendedFrame {
iframe: caller_iframe_ptr,
entry_state: caller_entry,
owned_refs: _caller_refs,
is_entry: caller_is_entry,
} = caller;
let caller_iframe = unsafe { &mut *caller_iframe_ptr };

let handled =
crate::frame::trampoline_handle_exception(caller_iframe, &exc, self);

match handled {
Ok(None) => {
// Handler found — resume the caller's dispatch loop.
let result = crate::frame::run_iframe(caller_iframe, self);
match result {
Ok(ExecutionResult::TailCall) => {
let refs = unsafe { &mut *self.pending_tailcall_refs.get() }
.drain(..)
.collect();
drop(_caller_refs);
frame_stack.push(SuspendedFrame {
iframe: caller_iframe_ptr,
entry_state: caller_entry,
owned_refs: refs,
is_entry: caller_is_entry,
});
action = Action::EnterCallee(self.take_pending_tailcall());
}
Ok(ExecutionResult::Return(value)) => {
drop(_caller_refs);
self.exit_iframe(caller_entry);
if !caller_is_entry {
unsafe {
if let Some(base) =
caller_iframe.release_datastack_frame()
{
self.datastack_pop(base);
}
}
}
action = Action::ReturnValue(value);
}
Ok(ExecutionResult::Yield(_)) => {
panic!("Yield in non-generator frame")
}
Err(new_exc) => {
drop(_caller_refs);
self.exit_iframe(caller_entry);
if !caller_is_entry {
unsafe {
if let Some(base) =
caller_iframe.release_datastack_frame()
{
self.datastack_pop(base);
}
}
}
action = Action::Unwind(new_exc);
}
}
}
Ok(Some(ExecutionResult::Return(value))) => {
drop(_caller_refs);
self.exit_iframe(caller_entry);
if !caller_is_entry {
unsafe {
if let Some(base) = caller_iframe.release_datastack_frame() {
self.datastack_pop(base);
}
}
}
action = Action::ReturnValue(value);
}
Ok(Some(_)) => {
panic!("Unexpected execution result in trampoline unwind")
}
Err(new_exc) => {
drop(_caller_refs);
self.exit_iframe(caller_entry);
if !caller_is_entry {
unsafe {
if let Some(base) = caller_iframe.release_datastack_frame() {
self.datastack_pop(base);
}
}
}
action = Action::Unwind(new_exc);
}
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Extract the repeated caller-teardown sequence into a helper.

The block drop(_caller_refs); self.exit_iframe(...); if !caller_is_entry { unsafe { if let Some(base) = ...release_datastack_frame() { self.datastack_pop(base); } } } appears six times, and the TailCall re-push block appears three times. The repetition makes it easy to omit one step in a future change, and each omission is a memory-safety defect rather than a cosmetic one.

Add two private helpers on SuspendedFrame or on the trampoline scope, for example finish_frame(&self, entry, iframe_ptr, is_entry) and resuspend(&self, stack, iframe_ptr, entry, is_entry), then call them from every arm.

🤖 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 `@crates/vm/src/vm/mod.rs` around lines 1534 - 1686, Extract the repeated
caller cleanup into a private helper associated with SuspendedFrame or the
trampoline scope, preserving the order of dropping owned references, calling
exit_iframe, and conditionally releasing and popping the datastack frame;
replace every duplicated cleanup block in ReturnValue and Unwind handling. Also
extract the repeated TailCall SuspendedFrame re-push logic into a helper and use
it in all three TailCall arms, preserving pending-tailcall reference draining
and callee transfer.

Comment thread crates/vm/src/vm/mod.rs
Comment on lines +2249 to +2253
if !old_chain.is_null() {
let prev_iframe = unsafe { &*old_chain };
let back_fo = prev_iframe.materialize_chain(self);
*fo.iframe().cold().retained_back.lock() = Some(back_fo);
}

Copy link
Copy Markdown
Contributor

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

retained_back is overwritten unconditionally here, unlike every other write site.

with_frame (Line 2118), release_datastack_frame, and the f_back chain walker all guard the write with if guard.is_none(). exit_iframe replaces the existing value. If the frame already captured a caller reference during execution, this write discards it and installs the materialize_chain result. Confirm that replacement is intended; otherwise use the same is_none guard for consistent f_back results.

🤖 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 `@crates/vm/src/vm/mod.rs` around lines 2249 - 2253, Update the retained_back
assignment in exit_iframe to preserve an existing caller reference: lock
retained_back, assign the materialize_chain result only when the guard is None,
and leave the existing value unchanged otherwise. Keep the old_chain check and
frame materialization flow intact.

Comment thread crates/vm/src/vm/mod.rs
Comment on lines 2387 to 2389
if self.use_tracing.get()
&& (frame.iframe().trace.lock().is_some() || !self.is_none(&self.profile_func.borrow()))
&& (frame.iframe().cold().trace.lock().is_some() || !self.is_none(&self.profile_func.borrow()))
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run cargo fmt; Line 2388 exceeds the default rustfmt width.

The condition on Line 2388 is a single long line. Default rustfmt wraps it. Reformat the file before merge.

Note also that cold() allocates the cold block on first access. This check runs on the return path of every traced frame, including frames that only have a profile function and never set trace. Reordering the condition to test self.profile_func first would avoid that allocation.

As per coding guidelines: "Follow default rustfmt style and run cargo fmt for Rust code."

🤖 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 `@crates/vm/src/vm/mod.rs` around lines 2387 - 2389, Reformat the condition in
the traced-frame return path with default rustfmt, and reorder its operands to
check self.is_none(&self.profile_func.borrow()) before accessing
frame.iframe().cold().trace, avoiding unnecessary cold-block allocation for
profile-only frames while preserving the existing logic.

Source: Coding guidelines

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.

2 participants