Skip to content

Refactor EvalState to not use Arcs, label Store's significant drops#59

Open
Naxdy wants to merge 3 commits into
nixops4:mainfrom
DeterminateSystems:upstream-feat/eval-state-inner-arc
Open

Refactor EvalState to not use Arcs, label Store's significant drops#59
Naxdy wants to merge 3 commits into
nixops4:mainfrom
DeterminateSystems:upstream-feat/eval-state-inner-arc

Conversation

@Naxdy
Copy link
Copy Markdown

@Naxdy Naxdy commented Mar 26, 2026

This PR refactors the EvalState to no longer use an internal Arc and remove its Clone implementation. The reason for this is that an EvalState allocates several resources over its lifetime that are held until it is Dropped.

Downstream crates can still wrap it in Rc if they need Clone, or Arc<Mutex<>> if they also need Send + Sync. This also has the advantage of maintaining a single Context across clones.

This also inadvertently fixes a potential issue with PrimOp where we used to access EvalState via a mutable reference constructed from a raw pointer (while potentially still holding references elsewhere), which is now protected against by the borrow checker.

As for the Store, the inner Arc is maintained (for now), due to the need to work around NixOS/nix#11979 - but the Drop significance is now clearly labeled & documented.


Upstream of DeterminateSystems#15

Copy link
Copy Markdown
Contributor

@roberth roberth left a comment

Choose a reason for hiding this comment

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

  • Currently degrades memory safety.
  • Have questions, see comments

Comment on lines +335 to +338
/// An abstraction over the underlying eval state.
///
/// When an `EvalState` is constructed, it will allocate a number of threads to be used for
/// evaluating expressions. These threads will remain allocated until the `EvalState` is dropped.
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.

This could be true of C++ EvalState, but I'm not sure if that's relevant to document here. It also does many other things.
It begs the question though: how can we have multiple &mut EvalState?
We seem to have one for each PrimopState!
That would be ok if EvalState had been merely a smart pointer, but then we wouldn't need mut, I assume. However the comment contradicts that.

// We'll be leaking this Box.
// TODO: Use the GC with finalizer, if possible.
let user_data = ManuallyDrop::new(Box::new(PrimOpContext {
let user_data = Box::leak(Box::new(PrimOpContext {
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.

now protected against by the borrow checker.

Unfortunately we've created a false sense of security here. This Box::leak lets the &mut EvalState stick around user_data, effectively untracked by the borrow checker, and by removing the weak reference functionality, you've removed the runtime check, creating UB instead of a panic if it were to go wrong.

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