Skip to content

Conversation

@coolreader18
Copy link
Member

@coolreader18 coolreader18 commented Sep 3, 2025

This branch has been sitting around in my local checkout for a while, and I was finally reminded of it today.

THREAD_LOCAL.with(|x: &RefCell<_>| x.borrow_mut().foo()) -> THREAD_LOCAL.with_borrow_mut(|x: &mut _| x.foo()), since Rust 1.73.

Summary by CodeRabbit

Release Notes

  • Dependencies

    • Added new dependencies: scoped-tls and scopeguard
  • Performance and Optimization

    • Improved internal thread-local storage and context management mechanisms
    • Refined borrow patterns across multiple modules for more efficient resource handling
  • Technical Improvements

    • Updated VM thread management and context variable handling
    • Simplified thread-local storage access patterns
    • Enhanced WASM virtual machine initialization and management
  • Maintenance

    • Minor refactoring of internal implementation details
    • No changes to public API surfaces

Note: These changes primarily focus on internal infrastructure improvements.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 3, 2025

Walkthrough

Introduces scoped TLS and scopeguard dependencies. Refactors multiple thread-local RefCell access patterns to with_borrow/with_borrow_mut helpers. Converts VM current-thread storage to scoped TLS, revising VM entry/access control flow. Adjusts sys/thread context accessors and cleanup logic. Minor iterator-based update in Levenshtein buffer initialization.

Changes

Cohort / File(s) Summary
Dependencies
Cargo.toml, vm/Cargo.toml
Added dependencies: scoped-tls and scopeguard; wired vm crate to workspace versions.
Thread-local borrow refactors
common/src/str.rs, stdlib/src/contextvars.rs, vm/src/stdlib/sys.rs, vm/src/stdlib/thread.rs, wasm/lib/src/vm_class.rs
Replaced RefCell borrow patterns with with_borrow/with_borrow_mut helpers; streamlined reads/writes; adjusted cleanup in thread sentinel handling; minor iterator initialization optimization in Levenshtein buffer.
VM TLS mechanism
vm/src/vm/thread.rs
Migrated VM_CURRENT from manual RefCell TLS to scoped_tls::scoped_thread_local; updated enter/access helpers to use scoped set/with and scopeguard for stack unwind.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant VM as VirtualMachine
  participant TLS as VM_CURRENT (scoped TLS)
  participant Stack as VM_STACK

  rect rgb(240,248,255)
    note over Caller,VM: enter_vm(vm, f)
    Caller->>VM: enter_vm(vm, f)
    VM->>Stack: push(vm)
    VM->>TLS: set(vm) {scoped}
    activate TLS
    TLS-->>VM: run f()
    VM->>VM: scopeguard defer Stack.pop()
    deactivate TLS
    VM-->>Caller: result
  end

  rect rgb(255,250,240)
    note over Caller,TLS: with_current_vm(f)
    Caller->>TLS: with(f) (requires is_set)
    TLS-->>Caller: f(&VM)
  end
Loading
sequenceDiagram
  autonumber
  participant Py as Python code
  participant CV as ContextVar
  participant Ctxs as CONTEXTS (TLS)
  participant Ctx as PyContext

  rect rgb(245,255,250)
    note over Py,Ctxs: current(vm)
    Py->>Ctxs: with_borrow_mut
    alt last exists
      Ctxs-->>Py: &last
    else create
      Ctxs->>Ctx: new default
      Ctxs->>Ctxs: push(Ctx)
      Ctxs-->>Py: &first
    end
  end

  rect rgb(250,245,255)
    note over Py,CV: CV.get(default)
    Py->>Ctxs: with_borrow
    Ctxs-->>Py: last context ref
    Py->>CV: check cache or lookup vars
    CV-->>Py: value or default
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Wtf8-compatible fixes #5985 — Also modifies common/src/str.rs Levenshtein logic (byte-slice change) touching the same buffer init area.

Suggested reviewers

  • youknowone

Poem

I nuzzle the stack where VMs now dwell,
Scoped threads whisper, “all is well.”
Contexts hop neatly, borrow-light,
Guards stand watch through unwind night.
With paws on hooks and buffers anew—
I thump: refactors done, carrots due! 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
stdlib/src/contextvars.rs (1)

377-399: Fix unsound read of AtomicCell via as_ptr; use swap(None)/store to avoid UB and races

Dereferencing as_ptr without synchronization can observe freed/stale memory if another thread swaps the cell concurrently; it also dereferences a raw pointer (UB risk). Use atomic swap to take ownership, validate, and restore or replace.

-            let found = super::CONTEXTS.with_borrow(|ctxs| {
-                let ctx = ctxs.last()?;
-                let cached_ptr = zelf.cached.as_ptr();
-                debug_assert!(!cached_ptr.is_null());
-                if let Some(cached) = unsafe { &*cached_ptr } {
-                    if zelf.cached_id.load(Ordering::SeqCst) == ctx.get_id()
-                        && cached.idx + 1 == ctxs.len()
-                    {
-                        return Some(cached.object.clone());
-                    }
-                }
-                let vars = ctx.borrow_vars();
-                let obj = vars.get(zelf)?;
-                zelf.cached_id.store(ctx.get_id(), Ordering::SeqCst);
-
-                // TODO: ensure cached is not changed
-                let _removed = zelf.cached.swap(Some(ContextVarCache {
-                    object: obj.clone(),
-                    idx: ctxs.len() - 1,
-                }));
-
-                Some(obj.clone())
-            });
+            let found = super::CONTEXTS.with_borrow(|ctxs| {
+                let ctx = ctxs.last()?;
+                // Take a snapshot of the cache without aliasing; restore if still valid.
+                if let Some(cached) = zelf.cached.swap(None) {
+                    if zelf.cached_id.load(Ordering::SeqCst) == ctx.get_id()
+                        && cached.idx + 1 == ctxs.len()
+                    {
+                        // Put it back and return fast-path.
+                        let obj = cached.object.clone();
+                        zelf.cached.store(Some(cached));
+                        return Some(obj);
+                    }
+                    // stale cache is dropped
+                }
+                // Miss: read from current context and repopulate cache.
+                let obj = {
+                    let vars = ctx.borrow_vars();
+                    vars.get(zelf)?.clone()
+                };
+                zelf.cached_id.store(ctx.get_id(), Ordering::SeqCst);
+                zelf.cached.store(Some(ContextVarCache {
+                    object: obj.clone(),
+                    idx: ctxs.len() - 1,
+                }));
+                Some(obj)
+            });

This keeps the lock-free happy path and eliminates raw-pointer aliasing. It also prevents dropping a valid cache entry on the fast path.

🧹 Nitpick comments (5)
common/src/str.rs (1)

536-539: Borrow-helper + iterator init: cleaner and safe

Switching to with_borrow_mut removes needless RefCell ceremony, and the iterator-based fill respects a_end bounds checked above. Nice touch.

If you want to shave a tiny bit of work, you can avoid repeated multiplies by using an accumulator:

- for (i, x) in buffer.iter_mut().take(a_end).enumerate() {
-     *x = (i + 1) * MOVE_COST;
- }
+ let mut v = MOVE_COST;
+ for x in buffer.iter_mut().take(a_end) {
+     *x = v;
+     v += MOVE_COST;
+ }
vm/src/stdlib/sys.rs (1)

829-835: Depth storage type mismatch is fine; consider unifying types to drop casts

Converting i32→u32 on set and u32→i32 on get is safe given the earlier non-negative check and the i32 parameter bound. Consider storing i32 in COROUTINE_ORIGIN_TRACKING_DEPTH to remove the casts and make intent explicit.

- crate::vm::thread::COROUTINE_ORIGIN_TRACKING_DEPTH.set(depth as u32);
+ crate::vm::thread::COROUTINE_ORIGIN_TRACKING_DEPTH.set(depth);

and

- crate::vm::thread::COROUTINE_ORIGIN_TRACKING_DEPTH.get() as i32
+ crate::vm::thread::COROUTINE_ORIGIN_TRACKING_DEPTH.get()

Also applies to: 839-840

vm/src/vm/thread.rs (1)

20-23: Clarify panic message; behavior change is acceptable

Panic-on-unset is fine, but “null” is misleading now. Suggest clearer wording.

-    if !VM_CURRENT.is_set() {
-        panic!("call with_current_vm() but VM_CURRENT is null");
-    }
+    if !VM_CURRENT.is_set() {
+        panic!("with_current_vm() called but VM_CURRENT is not set");
+    }
wasm/lib/src/vm_class.rs (1)

167-169: Avoid unnecessary mutable borrow in with_unchecked

You only clone the Rc; shared borrow suffices.

-        let stored_vm = STORED_VMS.with_borrow_mut(|vms| vms.get_mut(&self.id).unwrap().clone());
+        let stored_vm = STORED_VMS.with_borrow(|vms| vms.get(&self.id).unwrap().clone());
stdlib/src/contextvars.rs (1)

141-153: Nit: avoid extra clone and index by returning ctx directly

Slightly simpler and avoids one clone of the PyRef.

-            super::CONTEXTS.with_borrow_mut(|ctxs| {
+            super::CONTEXTS.with_borrow_mut(|ctxs| {
                 if let Some(ctx) = ctxs.last() {
                     ctx.clone()
                 } else {
                     let ctx = Self::empty(vm);
                     ctx.inner.idx.set(0);
                     ctx.inner.entered.set(true);
                     let ctx = ctx.into_ref(&vm.ctx);
                     ctxs.push(ctx);
-                    ctxs[0].clone()
+                    ctxs.last().unwrap().clone()
                 }
             })
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d17dcd8 and 9b1d30f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • Cargo.toml (1 hunks)
  • common/src/str.rs (1 hunks)
  • stdlib/src/contextvars.rs (3 hunks)
  • vm/Cargo.toml (1 hunks)
  • vm/src/stdlib/sys.rs (3 hunks)
  • vm/src/stdlib/thread.rs (2 hunks)
  • vm/src/vm/thread.rs (2 hunks)
  • wasm/lib/src/vm_class.rs (8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style (cargo fmt to format)
Always run clippy to lint code (cargo clippy) before completing tasks. Fix any warnings or lints that are introduced by your changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • stdlib/src/contextvars.rs
  • vm/src/stdlib/sys.rs
  • common/src/str.rs
  • vm/src/stdlib/thread.rs
  • wasm/lib/src/vm_class.rs
  • vm/src/vm/thread.rs
🧬 Code graph analysis (1)
vm/src/stdlib/thread.rs (1)
wasm/lib/src/vm_class.rs (1)
  • new (42-75)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Check Rust code with rustfmt and clippy
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Check the WASM package and demo
🔇 Additional comments (20)
vm/Cargo.toml (1)

68-69: Confirm non-native target builds
Workspace deps for scoped-tls and scopeguard look correct; please manually verify that rustpython-vm builds cleanly for wasm32-unknown-unknown and your standard desktop targets.

Cargo.toml (1)

210-211: Add to [workspace.dependencies]: LGTM

Pinning to major "1" for scoped-tls and scopeguard is appropriate and consistent with the vm crate workspace usage.

vm/src/stdlib/sys.rs (2)

910-918: Borrow-helper read: concise and correct

with_borrow(Clone::clone).to_pyobject(vm) cleanly maps Option → Python object, avoiding temporary borrows. No concerns.


889-895: Approve hook setter semantics preserved
OptionalArg<Option> paths correctly distinguish “unset” vs explicit None. .set(finalizer)/.set(firstiter) will clear vs set hooks as intended, and get_asyncgen_hooks maps an underlying None to Python None via the Option<T>::to_pyobject impl.

vm/src/stdlib/thread.rs (3)

337-341: Cleanup via SENTINELS.take() looks good

Drains TLS cleanly and avoids nested borrows; unlock logic preserved.


356-356: TLS declaration OK

Const init and type remain unchanged; style nit-only change.


361-361: Nice ergonomic switch to with_borrow_mut

Removes manual RefCell dance and reduces borrow scope.

vm/src/vm/thread.rs (3)

17-18: Scoped TLS for VM_CURRENT is a solid safety win

Eliminates raw pointer handling and simplifies access.


29-31: defer! pop + set() is tidy and exception-safe

Stack restoration is guaranteed; no objections.


54-55: Consistent VM binding via scoped TLS

Keeps VM_CURRENT coherent with VM_STACK; reads cleanly.

wasm/lib/src/vm_class.rs (9)

61-65: Init hooks via with_borrow are fine

Borrow scope is minimal; matches prior behavior.


81-82: add_init_func switch to with_borrow_mut is correct

Direct push without manual borrow boilerplate.


100-105: stored_vm_from_wasm read path looks good

Clones Rc under shared borrow; safe and concise.


108-109: weak_vm read path LGTM

Downgrade under shared borrow is appropriate.


117-123: VMStore::init mutation path is clean

Avoids explicit borrow_mut management; idempotent insert preserved.


128-129: _get simplified nicely

then_some keeps things compact.


139-147: destroy path OK

Removal + explicit drop is clear; commented drop handlers retained.


151-152: ids uses with_borrow appropriately

Collects keys without extra cloning.


187-188: valid read path LGTM

Shared borrow + contains_key is perfect here.

stdlib/src/contextvars.rs (1)

110-113: LGTM: switch to with_borrow_mut keeps semantics and drops an extra RefCell dance

Indexing before push still matches the cached idx logic used later. No functional change.

Comment on lines +128 to 134
super::CONTEXTS.with_borrow_mut(|ctxs| {
let err_msg =
"cannot exit context: thread state references a different context object";
ctxs.pop_if(|ctx| ctx.get_id() == zelf.get_id())
.map(drop)
.ok_or_else(|| vm.new_runtime_error(err_msg))
})?;
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 3, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Don’t rely on non-standard Vec::pop_if unless an explicit extension trait is in scope

pop_if is not in std. If you don’t have a local extension trait implementing it for Vec, this won’t compile. Even if you do, the intent (LIFO-only pop when last matches) can be expressed clearly with std-only code.

Apply this std-only fallback:

-            super::CONTEXTS.with_borrow_mut(|ctxs| {
-                let err_msg =
-                    "cannot exit context: thread state references a different context object";
-                ctxs.pop_if(|ctx| ctx.get_id() == zelf.get_id())
-                    .map(drop)
-                    .ok_or_else(|| vm.new_runtime_error(err_msg))
-            })?;
+            super::CONTEXTS.with_borrow_mut(|ctxs| {
+                let err_msg =
+                    "cannot exit context: thread state references a different context object";
+                if ctxs.last().map_or(false, |ctx| ctx.get_id() == zelf.get_id()) {
+                    ctxs.pop();
+                    Ok(())
+                } else {
+                    Err(vm.new_runtime_error(err_msg))
+                }
+            })?;

To confirm whether pop_if exists in the repo, run:


🏁 Script executed:

#!/bin/bash
# Look for a Vec extension providing `pop_if` and all callsites.
rg -nP -C2 '\bpop_if\s*\('
rg -nP -C3 'trait\s+\w+\s*{[^}]*pop_if\s*\('

Length of output: 512


Replace non-existent Vec::pop_if with standard code
Vec::pop_if isn’t defined in std or this repo, so the call will fail to compile. Replace:

ctxs.pop_if(|ctx| ctx.get_id() == zelf.get_id())
    .map(drop)
    .ok_or_else(|| vm.new_runtime_error(err_msg))

with:

-if ctxs.pop_if(|ctx| ctx.get_id() == zelf.get_id())
-    .map(drop)
-    .ok_or_else(|| vm.new_runtime_error(err_msg))
+if ctxs.last().map_or(false, |ctx| ctx.get_id() == zelf.get_id()) {
+    ctxs.pop();
+    Ok(())
+} else {
+    Err(vm.new_runtime_error(err_msg))
+}
🤖 Prompt for AI Agents
In stdlib/src/contextvars.rs around lines 128–134, Vec::pop_if does not exist so
replace that call with a search for the matching element using
ctxs.iter().rposition(|ctx| ctx.get_id() == zelf.get_id()) and then remove the
found index (ctxs.remove(pos)) to drop it; if rposition returns None, return the
vm.new_runtime_error(err_msg) as before. Ensure you call map/drop on the removed
element (or simply drop it) and keep the overall ? error propagation semantics.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

.

The current implementation using pop_if in the diff is correct and will compile successfully. My suggestion to replace it with standard library code was based on incorrect information. The code change from the traditional pattern to using with_borrow_mut combined with pop_if is a valid improvement that takes advantage of newer Rust convenience methods, which aligns perfectly with the PR's objective.

Thank you for the correction and providing the documentation link!


✏️ Learnings added
Learnt from: youknowone
PR: RustPython/RustPython#6123
File: stdlib/src/contextvars.rs:128-134
Timestamp: 2025-09-03T13:15:23.384Z
Learning: Vec::pop_if was stabilized in Rust 1.86.0 (released April 3, 2025) and is available in standard library for conditionally removing the last element from a vector based on a predicate.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +128 to 134
super::CONTEXTS.with_borrow_mut(|ctxs| {
let err_msg =
"cannot exit context: thread state references a different context object";
ctxs.pop_if(|ctx| ctx.get_id() == zelf.get_id())
.map(drop)
.ok_or_else(|| vm.new_runtime_error(err_msg))
})?;
Copy link
Member

Choose a reason for hiding this comment

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

@youknowone youknowone merged commit cca4fe6 into RustPython:main Sep 3, 2025
12 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Sep 15, 2025
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