[noupstream] Refactor EvalState & other types to account for DetNix' thread-safety#20
Conversation
📝 WalkthroughWalkthroughContext management refactoring across multiple crates removes persistent Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
8459602 to
5b08d89
Compare
EvalState thread-safeEvalState & other types to account for DetNix' thread-safety
141c665 to
d090124
Compare
d090124 to
15b159b
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
nix-bindings-expr/src/primop.rs (1)
50-54:⚠️ Potential issue | 🔴 CriticalLeaked
PrimOpContextcreates aliased mutable references toEvalState.
PrimOpContextis intentionally leaked (line 68, with TODO comment) and stores a&'a mut EvalStatereference. That same reference also lives in the returnedPrimOpstruct (line 90). If Nix invokes the callback concurrently across threads—especially after thread-safety changes toEvalState—multiple&mutreferences to the same object violate Rust's aliasing rules and constitute undefined behavior.The fix requires refactoring the callback state to use a shared, immutable reference (
&EvalState) paired with interior mutability, or a shared handle likeArc<EvalState>, so that concurrent access does not produce aliased mutable references.Also applies to: 68-72, 121-125, 143-143
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@nix-bindings-expr/src/primop.rs` around lines 50 - 54, The current PrimOp::new leaks a PrimOpContext holding a &'a mut EvalState and also stores that same mutable reference in the returned PrimOp, which can create aliased &mut EvalState references when callbacks run concurrently; change the callback state to hold a shared reference instead (e.g., &EvalState with interior mutability types like RefCell/Mutex/RwLock or use Arc<EvalState> for cross-thread sharing) and update PrimOpContext, the leaked allocation site (the TODO leak), and the PrimOp struct fields to use the shared/cloneable handle so the boxed callback f and any stored context no longer contain &'a mut EvalState but a shared, thread-safe reference/handle to EvalState.
🧹 Nitpick comments (1)
nix-bindings-util/src/context.rs (1)
8-15: Update theContextdocs to match the new allocation model.The type docs still say
EvalStateandStorekeep a private reusableContext, but this PR moved those call sites to per-operationContext::new(). Leaving the old description here makes the new threading story misleading.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@nix-bindings-util/src/context.rs` around lines 8 - 15, Update the Context documentation to reflect the new per-operation allocation model: replace references that claim EvalState and Store keep a private reusable Context with a note that callers now create a fresh Context per operation via Context::new(), mention that Context is cheap to allocate and safe to reuse across threads (unsafe impl Send remains), and remove or update the example about storing a private context in EvalState or Store and the mention of Clone::clone; keep the guidance about using check_call! for proper usage semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@nix-bindings-expr/src/eval_state.rs`:
- Around line 507-514: The code currently panics by calling unwrap() on the
result of check_call!(raw::get_type(...)) in value_type; instead propagate
backend errors and convert the optional raw type into a Result. Change
value_type (and the call site using check_call! on raw::get_type) to use the ?
operator to propagate check_call! errors, then handle the Option from
ValueType::from_raw without unwrap (e.g., map the None case to an Err with a
meaningful error using ValueType::from_raw(...).ok_or_else(...)). Keep the
initial force(value)? call and return Ok(ValueType) on success.
- Around line 371-382: Gate the unconditional thread-safety impls behind the
DetNix feature: wrap the unsafe impls for EvalStateRef and EvalState
(specifically the blocks declaring "unsafe impl Send for EvalStateRef {}",
"unsafe impl Send for EvalState {}", and "unsafe impl Sync for EvalState {}")
with a cfg feature attribute (e.g. #[cfg(feature = "DetNix")] or your crate's
DetNix feature name) so the Send/Sync guarantees are only compiled when DetNix
is enabled; ensure no other code paths expose these impls unconditionally.
---
Outside diff comments:
In `@nix-bindings-expr/src/primop.rs`:
- Around line 50-54: The current PrimOp::new leaks a PrimOpContext holding a &'a
mut EvalState and also stores that same mutable reference in the returned
PrimOp, which can create aliased &mut EvalState references when callbacks run
concurrently; change the callback state to hold a shared reference instead
(e.g., &EvalState with interior mutability types like RefCell/Mutex/RwLock or
use Arc<EvalState> for cross-thread sharing) and update PrimOpContext, the
leaked allocation site (the TODO leak), and the PrimOp struct fields to use the
shared/cloneable handle so the boxed callback f and any stored context no longer
contain &'a mut EvalState but a shared, thread-safe reference/handle to
EvalState.
---
Nitpick comments:
In `@nix-bindings-util/src/context.rs`:
- Around line 8-15: Update the Context documentation to reflect the new
per-operation allocation model: replace references that claim EvalState and
Store keep a private reusable Context with a note that callers now create a
fresh Context per operation via Context::new(), mention that Context is cheap to
allocate and safe to reuse across threads (unsafe impl Send remains), and remove
or update the example about storing a private context in EvalState or Store and
the mention of Clone::clone; keep the guidance about using check_call! for
proper usage semantics.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8c9e24d8-e1d7-494f-898d-60b433bcdbab
📒 Files selected for processing (7)
nix-bindings-expr/src/eval_state.rsnix-bindings-expr/src/primop.rsnix-bindings-expr/src/value.rsnix-bindings-flake/src/lib.rsnix-bindings-store/src/path/mod.rsnix-bindings-store/src/store.rsnix-bindings-util/src/context.rs
Since DetNix supports parallel eval, and after discussing with @edolstra , we can represent our thread-safety in Rust, by implementing
SendandSyncfor most types, as well as refactorEvalStateto take in immutable references to itself for most calls.This allows us to safely use e.g. an
Arc<EvalState>across multiple threads for parallel eval.Summary by CodeRabbit
Refactor
Improvements