Skip to content

Multithreaded evaluator#125

Merged
cole-h merged 176 commits intomainfrom
multithreaded-eval-v2
Sep 3, 2025
Merged

Multithreaded evaluator#125
cole-h merged 176 commits intomainfrom
multithreaded-eval-v2

Conversation

@edolstra
Copy link
Copy Markdown
Collaborator

@edolstra edolstra commented Jun 23, 2025

Motivation

Updated version of NixOS#10938.

Context

Summary by CodeRabbit

  • New Features

    • Experimental parallel evaluation: thread-pool executor, eval-cores setting, new __parallel primitive; parallelized flake checks, flake show, search, and JSON/XML rendering.
  • Performance

    • Memory-aware GC sizing and stack fixes; evaluator threading; Boost thread support; default remote-store max connections increased to 64.
  • UX Improvements

    • Better rendering for failed values in text/JSON/XML; improved error-location accuracy and diagnostics.
  • CI/Tests

    • Expanded CI matrix and richer diagnostic output; updated functional tests for adjusted error locations.

edolstra added 30 commits May 20, 2024 10:09
This is a mapping from paths to "resolved" paths (i.e. with
`default.nix` added, if appropriate). `fileParseCache` and
`fileEvalCache` are now keyed on the resolved path *only*.
Previously, the optimistic concurrency approach in `evalFile()` meant
that a `nix search nixpkgs ^` would do hundreds of duplicated
parsings/evaluations. Now, we reuse the thunk locking mechanism to
ensure it's done only once.
This refactoring allows the symbol table to be stored as something
other than std::strings.
This allows symbol IDs to be offsets into an arena whose base offset
never moves, and can therefore be dereferenced without any locks.
This makes it less likely that we concurrently execute tasks that
would block on a common subtask, e.g. evaluating `libfoo` and
`libfoo_variant` are likely to have common dependencies.
@github-actions github-actions bot temporarily deployed to pull request August 29, 2025 16:10 Inactive
@github-actions github-actions bot temporarily deployed to pull request August 31, 2025 14:44 Inactive
@github-actions github-actions bot temporarily deployed to pull request September 1, 2025 12:33 Inactive
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 2, 2025

Walkthrough

Adds a parallel Executor/futures, a thread-aware bit‑packed Value including a failed kind, mmap-backed contiguous symbol arena, Boehm GC stack-pointer fixup and memory-aware heap sizing, new eval-cores/stack settings, broad concurrency refactors across evaluation/printing/IO, CI workflow tweaks, and test updates.

Changes

Cohort / File(s) Summary
CI workflow adjustments
\.github/workflows/build.yml
Adds matrix eval-cores = 24; replaces inline env with a named Run flake regression tests step; sets env vars (PARALLEL, FLAKE_REGRESSION_GLOB, NIX_CONFIG, PREFETCH); adds diagnostics and nix-shim checks; changes failure path to NIX_REMOTE=/tmp/nix for eval-all.sh.
Boost packaging tweak
packaging/dependencies.nix
Adds "--with-thread" to boost.extraB2Args.
C API: failed value type
src/libexpr-c/nix_api_value.h, src/libexpr-c/nix_api_value.cc
Adds NIX_TYPE_FAILED to ValueType and maps internal nFailed to it in nix_get_type.
GC and memory init
src/libexpr/eval-gc.cc
Adds getFreeMem(); implements fixupBoehmStackPointer and registers it with Boehm GC; replaces percent-based initial heap sizing with memory-detection (min 32 MiB, cap 4 GiB) and expands heap accordingly.
Evaluator core, counters & settings
src/libexpr/eval.cc, src/libexpr/include/nix/expr/eval.hh, src/libexpr/include/nix/expr/eval-settings.hh, src/libexpr/include/nix/expr/counter.hh, src/libexpr/include/nix/expr/meson.build, src/libexpr/nixexpr.cc, src/libexpr/include/nix/expr/nixexpr.hh, src/libexpr/include/nix/expr/eval-inline.hh
Adds Executor integration and thread-local callDepth; introduces Counter type and multiple counters; adds eval-cores setting and evalStackSize; rewrites force/finish semantics for bit-packed atomic Value/thunk states; removes black‑hole sentinel and some Boehm caches; exports new headers.
Parallel evaluation subsystem
src/libexpr/include/nix/expr/parallel-eval.hh, src/libexpr/parallel-eval.cc, src/libexpr/meson.build
New thread‑pool Executor, FutureVector, waiter domains, wait/notify specializations for thunks, prim __parallel builtin, and Boost thread integration/workers.
Value representation and printing
src/libexpr/include/nix/expr/value.hh, src/libexpr/print.cc, src/libexpr/print-ambiguous.cc, src/libexpr/value-to-json.cc, src/libexpr/value-to-xml.cc, src/libexpr/primops.cc
Reworks Value as a two-word, bit‑packed, thread-aware layout; adds tFailed/nFailed and failure payload; adapts printing and ambiguous/JSON/XML output; parallelizes JSON traversal; makeRegexCache() now returns ref<RegexCache>.
Symbol table rewrite
src/libexpr/include/nix/expr/symbol-table.hh, src/libexpr/symbol-table.cc
Replaces chunked-vector storage with mmap-backed ContiguousArena, uses boost::concurrent_flat_set, offset-based access, and arena allocation APIs.
Source accessors & mounts synchronization
src/libfetchers/filtering-source-accessor.cc, src/libutil/mounted-source-accessor.cc
Wraps internal path sets/maps with SharedSync and updates accesses to use read/write locks for thread safety.
Position table concurrency
src/libutil/include/nix/util/pos-table.hh
Replaces origins map with SharedSync<State> and uses read/write locks for resolve/addOrigin.
Experimental feature: ParallelEval
src/libutil/include/nix/util/experimental-features.hh, src/libutil/experimental-features.cc
Adds ParallelEval enum and feature-details entry; updates feature count/array to include the new feature.
Store settings adjustments
src/libstore/include/nix/store/globals.hh, src/libstore/globals.cc, src/libstore/include/nix/store/remote-store.hh
Changes Settings::getDefaultCores() to a static method; increases RemoteStoreConfig::maxConnections default from 1 to 64.
CLI parallelization and stack size
src/nix/flake.cc, src/nix/search.cc, src/nix/main.cc
Parallelizes flake checks/show and search via FutureVector; makes aggregations thread-safe; uses evalStackSize for thread stacks; simplifies manpage call and reduces namespace qualifiers in main.
Flake trivial forcing tweak
src/libflake/flake.cc
Uses !value.isFinished() && value.isTrivial() instead of value.isThunk() when deciding to force trivial values.
Primops minor change
src/libexpr/primops/fetchClosure.cc
Uses std::string_view for a local attribute name binding.
Tests updates
src/libexpr-tests/value/print.cc, src/libexpr-tests/value/value.cc, tests under tests/functional/...
Adjusts test helper signatures and scenarios; updates expected error locations; simplifies flake show functional test; updates grep expectations.
Utilities
src/libutil/include/nix/util/util.hh
Adds template helper getOptional(map, key) returning std::optional<typename T::mapped_type>.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client (Eval)
  participant EX as Executor
  participant W as Worker Threads

  C->>EX: spawn(list of (work,prio))
  EX->>EX: enqueue items (priority queue)
  EX-->>W: notify worker(s)
  loop per item
    W->>EX: dequeue Item
    W->>W: execute work()
    alt success
      W-->>EX: set promise value
    else exception
      W-->>EX: set promise exception
    end
  end
Loading
sequenceDiagram
  autonumber
  participant S as EvalState
  participant V as Value (thunk)
  participant D as WaitDomain

  Note over S,V: Force value v at pos
  S->>V: CAS p0 -> pending/awaited
  alt another thread owns
    S->>D: waitOnThunk(v)
    D-->>S: notify when finished
  else this thread evaluates
    S->>V: evaluate (call/app or expr->eval)
    S->>V: finish() and notifyWaiters()
  end
  alt failed
    V-->>S: rethrow stored exception
  end
Loading
sequenceDiagram
  autonumber
  participant J as JSON Printer
  participant EX as Executor
  participant W as Worker Threads

  J->>EX: create FutureVector
  J->>J: recurse(root)
  alt parallel enabled & workers available
    J->>EX: spawn per-child recurse()
    EX-->>W: schedule tasks
    W->>J: compute child JSON and merge contexts
  else inline
    J->>J: recurse sequentially
  end
  J->>EX: finishAll()
  EX-->>J: all futures done
  J-->>J: assemble final JSON
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~180 minutes

Poem

I twitch my ears at threads awake,
A warren of thunks now learn to break;
We queue the work and hop in line,
With futures bright and counters fine.
If values fail, we mark them true—«failed»—and stitch the JSON through. 🐇⚙️

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch multithreaded-eval-v2

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

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

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.

@github-actions github-actions bot temporarily deployed to pull request September 2, 2025 13:16 Inactive
Copy link
Copy Markdown

@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: 13

Caution

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

⚠️ Outside diff range comments (1)
src/libexpr/include/nix/expr/value.hh (1)

700-754: Failed payload allocation lifetime/ownership is unclear; likely leak.

mkFailed() allocates a Failed with new and stores the raw pointer. I don’t see a corresponding delete path or tracing hook; this risks leaking on every failure. Please either:

  • store Failed by value in a fixed representation,
  • use a managed pointer (unique_ptr in a tagged layout, or shared_ptr),
  • or integrate with the GC/traceable allocator so it’s collected.
-    inline void mkFailed() noexcept
-    {
-        setStorage(new Value::Failed{.ex = std::current_exception()});
-    }
+    inline void mkFailed() noexcept
+    {
+        // Option A: store by value via pdSingleDWord + indirection to a GC-managed arena.
+        // Option B (sketch): use a shared_ptr if GC integration isn’t trivial.
+        setStorage(allocateFailedFromGC(std::current_exception())); // define allocator accordingly
+    }

If you prefer a minimal change, at least wrap the allocation in a deleter reachable from the GC cycle or ensure setStorage transitions eventually free Failed*.

♻️ Duplicate comments (9)
src/libexpr/include/nix/expr/eval-settings.hh (1)

366-380: Good: clear doc for eval-cores and affected commands.

This addresses the prior request to document impact; default and “0 = all cores” semantics are clear.

src/libexpr/include/nix/expr/symbol-table.hh (1)

20-24: Consider adding a cast function for type safety

Based on previous review feedback requesting _cast functions, consider introducing a named cast function for clarity and type safety instead of using reinterpret_cast directly.

     operator std::string_view() const noexcept
     {
         // The actual string is stored directly after the value.
-        return reinterpret_cast<const char *>(this + 1);
+        return castToString();
     }
+
+private:
+    const char * castToString() const noexcept
+    {
+        return reinterpret_cast<const char *>(this + 1);
+    }
src/libexpr/print-ambiguous.cc (1)

81-83: Handle failed values in ambiguous printer — consider de-duplicating the literal.

The new nFailed branch is correct. To prevent drift with Printer::printFailed, centralize the "«failed»" rendering (shared helper or constant).

src/libexpr/include/nix/expr/eval-inline.hh (1)

91-142: Complex control flow with goto needs careful review

While the goto usage here appears to be for performance optimization in a hot path, the complex state machine with multiple exit points makes the code harder to maintain and reason about.

Consider restructuring to avoid goto while maintaining performance:

+    auto processResult = [&](auto p0_val) {
+        if (InternalType(p0_val & 0xff) == tFailed)
+            std::rethrow_exception((std::bit_cast<Failed *>(p1))->ex);
+    };
+
     if (pd == pdThunk) {
         try {
             // ... existing thunk processing logic ...
         } catch (...) {
             // ... existing error handling ...
             throw;
         }
+        return;
     }
-    else if (pd == pdPending || pd == pdAwaited)
+    
+    if (pd == pdPending || pd == pdAwaited) {
         p0_ = waitOnThunk(state, pd == pdAwaited);
+        processResult(p0_);
+        return;
+    }
 
-done:
-    if (InternalType(p0_ & 0xff) == tFailed)
-        std::rethrow_exception((std::bit_cast<Failed *>(p1))->ex);
+    processResult(p0_);
src/libexpr/include/nix/expr/eval.hh (1)

970-975: Counter naming clarification needed.

The name microsecondsWaiting could be clearer. Also ensure these new performance counters are properly initialized.

Consider renaming to microsWaiting or waitTimeMicros for clarity, as suggested in the past review comment.

src/libexpr/include/nix/expr/parallel-eval.hh (1)

76-76: Address the FIXME comment about destructor behavior.

There's an existing comment from cole-h asking about the reason this hasn't been fixed. The FIXME suggests that the destructor should handle cancellation/waiting for futures, which is important for proper cleanup and avoiding resource leaks or undefined behavior when a FutureVector is destroyed with pending work.

Do you want me to generate a solution that properly implements cancellation/waiting in the destructor?

src/libexpr/eval.cc (1)

1182-1184: Potential race condition when copying forced value.

The comment on line 2189 mentions a race where a thunk might be updated by another thread. This same race condition could occur here when copying the value after forcing.

The value might be modified by another thread between forceValue and the assignment. Consider using atomic operations or proper synchronization:

     forceValue(*vExpr, noPos);
-
-    v = *vExpr;
+    // Use memcpy to avoid issues with concurrent value updates
+    memcpy(&v, vExpr, sizeof(Value));
src/libexpr/parallel-eval.cc (2)

257-257: Inconsistent error context message.

The error context differs from the suggested fix in the past review comment.

-    state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.parallel");
+    state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.parallel");

The current message is correct and matches the convention used elsewhere in the codebase.


270-281: Remove or implement the experimental feature gate.

The FIXME comment at line 271 indicates this should be gated behind an experimental feature, but it's currently always enabled when registered with .experimentalFeature = Xp::ParallelEval.

Either remove the FIXME comment since the feature is already gated, or clarify what additional gating is needed:

-// FIXME: gate this behind an experimental feature.
 static RegisterPrimOp r_parallel({
     .name = "__parallel",
🧹 Nitpick comments (47)
src/libstore/include/nix/store/remote-store.hh (1)

24-26: Default jump to 64 connections: aligns with parallel eval, but consider CPU-aware cap and doc note

  • This materially changes behavior (1 → 64). Good for multithreaded eval, but may oversubscribe small machines or CI runners and increase daemon FD/memory pressure.
  • Suggest a CPU-aware default with a ceiling to keep behavior sane across hosts, while preserving the ability to override via nix.conf.

Apply this diff within the initializer to pick a dynamic default (cap at 64, reasonable floor), keeping the option overrideable:

-    const Setting<int> maxConnections{
-        this, 64, "max-connections", "Maximum number of concurrent connections to the Nix daemon."};
+    const Setting<int> maxConnections{
+        this,
+        [] {
+            unsigned c = std::thread::hardware_concurrency();
+            int d = c ? static_cast<int>(c) * 2 : 8; // fallback for unknown core count
+            if (d < 4) d = 4;
+            if (d > 64) d = 64;
+            return d;
+        }(),
+        "max-connections",
+        "Maximum number of concurrent connections to the Nix daemon."};

Additional includes required at the top of the file:

#include <thread>

Follow-ups:

  • Document the new default in nix.conf docs/manpage and release notes as a behavior change (“per RemoteStore instance”).
  • If you see daemon load spikes in CI, consider lowering the cap (e.g., 32) or introducing a per-host override for builders with tight limits.
packaging/dependencies.nix (1)

88-88: Boost: verify Thread’s transitive libs are built
Ensure Boost.Thread’s dependent libraries (System, Chrono, Atomic) are present after building with extraB2Args = ["--with-thread"]. After running

nix build .#boost
fd -HI 'libboost_{thread,system,chrono,atomic}*' $(nix eval --raw .#boost.outPath)

verify that libboost_thread*, libboost_system*, libboost_chrono*, and libboost_atomic* all exist. If any are missing, extend extraB2Args as:

         "--with-thread"
+        "--with-system"
+        "--with-chrono"
+        "--with-atomic"
tests/functional/misc.sh (1)

25-26: Make error-location assertions resilient and quote variables

Avoid brittle exact columns and unquoted expansions.

Apply:

-e
-echo $eval_arg_res | grep "at «string»:1:12:"
-echo $eval_arg_res | grep "infinite recursion encountered"
+printf '%s\n' "$eval_arg_res" | grep -E 'at «string»:1:[0-9]+:'
+printf '%s\n' "$eval_arg_res" | grep -F "infinite recursion encountered"

-e
-echo $eval_stdin_res | grep "at «stdin»:1:12:"
-echo $eval_stdin_res | grep "infinite recursion encountered"
+printf '%s\n' "$eval_stdin_res" | grep -E 'at «stdin»:1:[0-9]+:'
+printf '%s\n' "$eval_stdin_res" | grep -F "infinite recursion encountered"

Also applies to: 29-30

tests/functional/flakes/show.sh (1)

62-62: Harden the assertion against blank lines/colors.

Counting raw lines is brittle if formatting changes. Filter empty lines and force no color.

-[[ $(nix flake show --all-systems --legacy | wc -l) = 1 ]]
+[[ $(nix --color never flake show --all-systems --legacy | sed '/^[[:space:]]*$/d' | wc -l) = 1 ]]

Optional (if jq is available): assert via JSON instead of display formatting.

nix flake show --json --all-systems --legacy | jq -e 'keys==["description"]'
src/libstore/globals.cc (1)

153-162: Consider marking noexcept (if getMaxCPU() is non-throwing).

Minor polish; communicates intent and may enable small optimizations.

src/libutil/include/nix/util/pos-table.hh (1)

54-63: Add missing standard headers explicitly.

This header uses std::map (and std::prev in resolve()) but doesn’t include (and ). Avoid relying on transitive includes.

Add:

  • #include
  • #include
  • #include
src/libutil/include/nix/util/util.hh (1)

221-229: Generalize key type and consider ref-returning variant to avoid copies.

Current getOptional copies mapped_type and only accepts key_type; heavy types pay a copy cost.

Minimal generalization:

-template<class T>
-std::optional<typename T::mapped_type> getOptional(const T & map, const typename T::key_type & key)
+template<class T, typename K>
+std::optional<typename T::mapped_type> getOptional(const T & map, const K & key)
{
    auto i = map.find(key);
    if (i == map.end())
        return std::nullopt;
    return {i->second};
}

Optional: add a zero-copy helper alongside (keeps current API intact):

template<class T, typename K>
std::optional<std::reference_wrapper<const typename T::mapped_type>>
getOptionalRef(const T & map, const K & key)
{
    auto i = map.find(key);
    if (i == map.end()) return std::nullopt;
    return std::cref(i->second);
}
src/libexpr/include/nix/expr/eval-settings.hh (1)

388-392: Consider making eval stack size configurable.

Hard-coding 64 MiB may not fit all platforms/workloads. Expose as a Setting (with this as default) or env knob for tunability.

src/libutil/mounted-source-accessor.cc (1)

62-69: Consider extracting the lock scope for clarity.

While the current implementation is correct, the scoping block on lines 62-69 could be more explicit about its purpose. This is a minor readability suggestion.

Consider adding a comment to clarify the lock scope:

-            {
+            { // Acquire read lock for mounts lookup
                 auto mounts(mounts_.readLock());
src/libutil/experimental-features.cc (1)

318-320: Consider expanding the feature description.

The description for the ParallelEval feature is quite minimal compared to other features. Given this is a significant performance enhancement, users would benefit from more details about what it enables and any potential implications.

Consider providing a more comprehensive description:

         .description = R"(
-            Enable built-in functions for parallel evaluation.
+            Enable built-in functions for parallel evaluation.
+            
+            This feature allows Nix to evaluate expressions in parallel using multiple
+            threads, potentially improving performance for large evaluations. The number
+            of threads can be configured using the `eval-cores` setting.
+            
+            Note: This is an experimental feature and may have stability implications.
         )",
.github/workflows/build.yml (5)

128-134: Matrix entry “eval-cores = 24” is mutually exclusive with lazy-trees settings.

Right now, each job tests either lazy-trees or eval-cores, but never both together. If the intent is to test eval-cores under both lazy-trees true/false, consider a 2D matrix (lazy_trees x eval_cores) and composing NIX_CONFIG from both, or add combined entries like "lazy-trees = true\neval-cores = 24".

Example change:

 strategy:
   matrix:
-    nix_config:
-      - "lazy-trees = true"
-      - "lazy-trees = false"
-      - "eval-cores = 24"
+    lazy_trees: [true, false]
+    eval_cores: [0, 24]
     glob:
       - "[0-d]*"
       - "[e-l]*"
       - "[m]*"
       - "[n-r]*"
       - "[s-z]*"

and in the step env:

-  NIX_CONFIG: ${{ matrix.nix_config }}
+  NIX_CONFIG: |
+    lazy-trees = ${{ matrix.lazy_trees }}
+    eval-cores = ${{ matrix.eval_cores }}

156-161: Re-enable dynamic PARALLEL to keep overall CI throughput.

Hard-coding PARALLEL to -P 1 may slow the suite when eval-cores isn’t configured. If that was unintentional, restore the conditional variant.

-          PARALLEL: '-P 1'
+          PARALLEL: ${{ contains(matrix.nix_config, 'eval-cores') && '-P 1' || '-P 50%' }}

166-169: Shellcheck: prefer -n over ! -z and avoid assigning in export.

Small bash hygiene fixes; keeps actionlint/shellcheck green.

-          if [ ! -z "${NSC_CACHE_PATH:-}" ]; then
+          if [ -n "${NSC_CACHE_PATH:-}" ]; then
             mkdir -p "${NSC_CACHE_PATH}/nix/xdg-cache"
-            export XDG_CACHE_HOME="${NSC_CACHE_PATH}/nix/xdg-cache"
+            XDG_CACHE_HOME="${NSC_CACHE_PATH}/nix/xdg-cache"
+            export XDG_CACHE_HOME
           fi

170-173: Shellcheck: avoid command substitution inside export (SC2155).

Split assignment and export for PATH.

-          export PATH=$(pwd)/new-nix/bin:$PATH
+          PATH="$(pwd)/new-nix/bin:$PATH"
+          export PATH

179-183: Optional: stricter bash mode with a controlled retry.

set -euo pipefail can surface early failures; keep the retry by disabling -e just for the flaky block.

-          set -x
+          set -xeuo pipefail
           echo "PARALLEL: $PARALLEL"
           echo "NIX_CONFIG: $NIX_CONFIG"
@@
-          if ! flake-regressions/eval-all.sh; then
+          set +e
+          flake-regressions/eval-all.sh
+          rc=$?
+          set -e
+          if [ $rc -ne 0 ]; then
             echo "Some failed, trying again"
             printf "\n\n\n\n\n\n\n\n"
             NIX_REMOTE=/tmp/nix flake-regressions/eval-all.sh
           fi
src/libexpr/include/nix/expr/counter.hh (2)

15-28: Prefer explicit atomic loads; avoid implicit conversion from std::atomic.

Use inner.load(memory_order_relaxed) for reads to be clear and avoid any ambiguity with deprecated implicit conversions.

-    operator value_type() const noexcept
-    {
-        return inner;
-    }
+    operator value_type() const noexcept { return inner.load(std::memory_order_relaxed); }
@@
-    value_type load() const noexcept
-    {
-        return inner;
-    }
+    value_type load() const noexcept { return inner.load(std::memory_order_relaxed); }

59-59: Use standard alignas for portability.

attribute((aligned)) is GCC/Clang-specific. Prefer alignas(64).

-} __attribute__((aligned(64))); // cache line alignment to prevent false sharing
+} alignas(64); // cache line alignment to prevent false sharing
src/libexpr/include/nix/expr/value.hh (5)

440-473: Concurrency doc and protocol look good; minor nit on wording.

Great to see finish() centralizing the p1-before-p0 invariant with release semantics. The comment block is clear and addresses the awaited/pending states.

Two nits:

  • “use `acquire semantics” -> remove the stray asterisks for consistency with “release”.
  • Consider explicitly mentioning that readers must use acquire loads on p0 (as you already do) before reading p1.

524-543: finish(): handle unexpected prior states explicitly (defensive).

If p0_ decodes to pdSingleDWord/pdPairOfPointers/etc., silently doing nothing hides bugs. Logging once (rate-limited) or asserting in debug builds helps catch misuse.

-        if (pd == pdPending)
-            // Nothing to do; no thread is waiting on this thunk.
-            ;
-        else if (pd == pdAwaited)
+        if (pd == pdPending) {
+            // No waiters.
+        } else if (pd == pdAwaited) {
             // Slow path: wake up the threads that are waiting on this
             // thunk.
             notifyWaiters();
-        else if (pd == pdThunk) {
+        } else if (pd == pdThunk) {
             printError("BAD FINISH %x", this);
             unreachable();
+        } else {
+            // Unexpected: finishing over a non-thunk state.
+            debug("finish(): overwrote state %d", (int)pd);
         }

574-586: setThunkPayload(): consider release semantics on publishing the thunk header.

Even though this is initial construction, using a release store on p0 (or a fence) future-proofs against any re-ordering surprises on less common architectures or if construction paths evolve.

-        p0.store(static_cast<int>(pdThunk) | firstFieldPayload, std::memory_order_relaxed);
+        p0.store(static_cast<int>(pdThunk) | firstFieldPayload, std::memory_order_release);

1280-1323: Accessor methods for thunk/app noted as racy — follow-up to remove or guard.

You’ve marked thunk() and app() as racy. Consider guarding their use under asserts that the value is a finished thunk representation or delete them once call sites are migrated.


1028-1033: isFinished(): optional fast-path.

The FIXME notes a possible optimization. A small fast-path check on p0’s primary discriminator (without decoding full type) would avoid a switch in hot paths. Not urgent, but worth a TODO link.

src/libexpr/include/nix/expr/symbol-table.hh (1)

35-35: Centralize cache line alignment into a platform‐configurable constant

Current code hard-codes __attribute__((aligned(64))) in three places:

  • src/libexpr/parallel-eval.cc:175
  • src/libexpr/include/nix/expr/counter.hh:59
  • src/libexpr/include/nix/expr/symbol-table.hh:35

Assuming a 64-byte cache line is common on x86_64 but may be suboptimal on other architectures (e.g. 32-byte on ARM). Define a single CACHE_LINE_SIZE (or use std::hardware_destructive_interference_size) and replace each hard-coded 64 with alignas(CACHE_LINE_SIZE) to make cache‐line alignment configurable per platform.

src/libexpr/symbol-table.cc (3)

8-11: Document MAP_NORESERVE fallback behavior

The fallback definition of MAP_NORESERVE to 0 when undefined could lead to different memory allocation behavior across platforms. Consider adding a comment explaining the implications.

 #ifndef MAP_NORESERVE
+// MAP_NORESERVE is not available on all platforms (e.g., macOS).
+// When set to 0, memory is reserved immediately rather than on-demand.
 #  define MAP_NORESERVE 0
 #endif

12-18: Consider fallback for mmap failure on large allocations

The arena requests 1GB of virtual memory (1 << 30 from the header). On systems with limited virtual memory or restrictive overcommit settings, this could fail even with MAP_NORESERVE.

Consider implementing a fallback mechanism or making the arena size configurable:

 static void * allocateLazyMemory(size_t maxSize)
 {
     auto p = mmap(nullptr, maxSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
-    if (p == MAP_FAILED)
+    if (p == MAP_FAILED) {
+        // Try without MAP_NORESERVE as a fallback
+        p = mmap(nullptr, maxSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+        if (p == MAP_FAILED)
             throw SysError("allocating arena using mmap");
+    }
     return p;
 }

45-61: Extract alignment computation into a shared helper

The constructor’s expression ((rawSize + alignment – 1)/alignment)*alignment and the dump method’s (n + (n%8 ? 8-(n%8) : 0)) both perform the same rounding up to SymbolTable::alignment. Factor this into a single helper to avoid drift and ease future changes:

+static size_t alignSize(size_t rawSize, size_t alignment) {
+    return ((rawSize + alignment - 1) / alignment) * alignment;
+}-    auto size = ((rawSize + SymbolTable::alignment - 1) / SymbolTable::alignment) * SymbolTable::alignment;
+    auto size = alignSize(rawSize, SymbolTable::alignment);
src/libexpr/print.cc (2)

515-519: Optional: colorize 'failed' consistently and/or reuse a shared helper.

Match styling with other diagnostic prints (e.g., ANSI_RED) and avoid duplicating the literal across files.

 void printFailed(Value & v)
 {
-    output << "«failed»";
+    if (options.ansiColors) output << ANSI_RED;
+    output << "«failed»";
+    if (options.ansiColors) output << ANSI_NORMAL;
 }

636-638: Remove unused local ‘seen’ shadowing the member.

This local variable is unused and shadows the optional member ‘seen’, which is confusing.

-        ValuesSeen seen;
         print(v, 0);
src/libexpr/include/nix/expr/nixexpr.hh (1)

93-93: Switching to Counter for nrExprs: confirm cheap disabled-path.

Assuming Counter guards increments behind an enable flag. If not, consider gating the hot-path increment to keep parser perf stable when counting is disabled.

Apply if Counter exposes an enabled() check:

-    nrExprs++;
+    if (nrExprs.enabled()) nrExprs++;
src/libexpr-tests/value/print.cc (2)

13-18: Pass Value as const reference.

The helper doesn’t mutate v; prefer const Value & to tighten the contract.

-    void test(Value & v, std::string_view expected, A... args)
+    void test(const Value & v, std::string_view expected, A... args)

624-633: Avoid mutating the same Bindings backing vAttrs (aliasing hazard).

Reusing the same builder after finish() likely mutates the bindings that vAttrs points to. Build vAttrs2 with a fresh builder to keep tests independent.

-    builder.insert(state.symbols.create("three"), &vThree);
-    Value vAttrs2;
-    vAttrs2.mkAttrs(builder.finish());
+    BindingsBuilder builder3(state, state.allocBindings(10));
+    builder3.insert(state.symbols.create("one"),  &vOne);
+    builder3.insert(state.symbols.create("two"),  &vTwo);
+    builder3.insert(state.symbols.create("three"), &vThree);
+    Value vAttrs2;
+    vAttrs2.mkAttrs(builder3.finish());
src/libexpr/primops.cc (1)

5101-5160: Unconditionally constructing currentTime/currentSystem: confirm pure-eval behavior.

Values are created even in pure-eval but gated from exposure via impureOnly. Please confirm:

  • In pure-eval, neither symbol appears in builtins.
  • No tests depend on their absence vs presence in the environment snapshot.

If you want to avoid doing time(0) work when impure-only, guard it:

-    v.mkInt(time(0));
+    if (!settings.pureEval && !settings.restrictEval) v.mkInt(time(0));
src/libexpr/eval-gc.cc (3)

36-68: Consider improving error handling in memory parsing

The getFreeMem() function silently falls back to defaults when parsing fails. Consider logging warnings when memory information cannot be obtained or parsed incorrectly.

 static size_t getFreeMem()
 {
     /* On Linux, use the `MemAvailable` or `MemFree` fields from
        /proc/cpuinfo. */
 #  ifdef __linux__
     {
         std::unordered_map<std::string, std::string> fields;
         for (auto & line :
              tokenizeString<std::vector<std::string>>(readFile(std::filesystem::path("/proc/meminfo")), "\n")) {
             auto colon = line.find(':');
             if (colon == line.npos)
                 continue;
             fields.emplace(line.substr(0, colon), trim(line.substr(colon + 1)));
         }
 
         auto i = fields.find("MemAvailable");
         if (i == fields.end())
             i = fields.find("MemFree");
         if (i != fields.end()) {
             auto kb = tokenizeString<std::vector<std::string>>(i->second, " ");
-            if (kb.size() == 2 && kb[1] == "kB")
-                return string2Int<size_t>(kb[0]).value_or(0) * 1024;
+            if (kb.size() == 2 && kb[1] == "kB") {
+                auto mem = string2Int<size_t>(kb[0]);
+                if (mem.has_value())
+                    return mem.value() * 1024;
+                debug("Failed to parse memory value from /proc/meminfo");
+            }
         }
+        debug("Could not find MemAvailable or MemFree in /proc/meminfo");
     }
 #  endif
 
     /* On non-Linux systems, conservatively assume that 25% of memory is free. */
     long pageSize = sysconf(_SC_PAGESIZE);
     long pages = sysconf(_SC_PHYS_PAGES);
     if (pageSize != -1)
         return (pageSize * pages) / 4;
     return 0;
 }

119-121: Consider adding assertions for stack pointer validity

After adjusting the stack pointer to osStackLo, consider adding debug assertions to verify the adjusted pointer is valid and within expected bounds.

     if (sp >= osStackHi || sp < osStackLo) // sp is outside the os stack
         sp = osStackLo;
+#ifndef NDEBUG
+    assert(sp >= osStackLo && sp < osStackHi && "Adjusted stack pointer should be within bounds");
+#endif

156-173: Consider making heap sizing parameters configurable

The heap sizing logic uses hardcoded values (32MB min, 4GB max, 50% of free memory). Consider making these configurable via environment variables or settings for better flexibility in different deployment scenarios.

Would you like me to help create a configuration system for these heap sizing parameters?

src/nix/search.cc (2)

113-117: Consider explaining the priority logic

The priority assignment logic (checking for "Packages" in the attribute name) could benefit from a comment explaining why packages get lower priority (0 vs 2).

                         work.emplace_back(
                             [cursor2, attrPath2, visit]() { visit(*cursor2, attrPath2, false); },
+                            // Give "Packages" attributes lower priority to process other attributes first
                             std::string_view(state->symbols[attr]).find("Packages") != std::string_view::npos ? 0 : 2);

167-175: Consider batching console output for better performance

Writing to the console per-result in parallel threads could cause contention and jumbled output. Consider accumulating results and printing them in batches.

Instead of immediate console output, consider collecting results and printing them after futures.finishAll() or implementing a dedicated output queue with a single writer thread.

src/libexpr/include/nix/expr/eval-inline.hh (1)

106-107: Consider strengthening memory ordering for CAS

The compare_exchange uses acquire ordering for both success and failure cases. Consider if acq_rel for success would provide better guarantees for the subsequent operations on the thunk.

-            if (!p0.compare_exchange_strong(p0_, pdPending, std::memory_order_acquire, std::memory_order_acquire)) {
+            if (!p0.compare_exchange_strong(p0_, pdPending, std::memory_order_acq_rel, std::memory_order_acquire)) {
src/libexpr/value-to-json.cc (2)

44-142: Consider adding bounded parallelism for deeply nested structures.

The recursive traversal correctly handles all value types with parallel spawning. However, for deeply nested structures, unbounded parallelism could lead to excessive memory usage from queued tasks.

Consider adding a depth limit or task queue size limit to prevent resource exhaustion:

-    auto spawn = [&](auto work) {
+    size_t depth = 0;
+    auto spawn = [&](auto work, size_t currentDepth = 0) {
-        if (doParallel) {
+        if (doParallel && currentDepth < 100) {  // Limit parallel depth
             futures.spawn(0, [work{std::move(work)}]() { work(); });
         } else {
             work();
         }
     };

96-106: Add error recovery for parallel attribute evaluation.

While error traces are properly added, there's no mechanism to cancel other spawned tasks when one fails. This could lead to unnecessary computation.

Consider implementing early termination when errors occur in parallel tasks to avoid wasted computation on siblings that will ultimately be discarded.

src/nix/flake.cc (1)

519-528: Document and replace magic priority levels in futures.spawn

Add documentation for priority values (0–3) in parallel-eval.hh and replace raw literals (e.g., 1 in flake.cc:519-528) with a descriptive enum or named constants to clarify the task ordering in futures.spawn calls.

src/libexpr/eval.cc (3)

493-495: Document why raw memcpy is necessary here.

The comment mentions that operator = fails on thunks, but since this occurs after value construction, it would be helpful to explain why a simple assignment cannot be used even for non-thunk values.

 Value * EvalState::addConstant(const std::string & name, Value & v, Constant info)
 {
     Value * v2 = allocValue();
-    // Do a raw copy since `operator =` barfs on thunks.
+    // Do a raw copy since `operator =` barfs on thunks. This is safe because
+    // we're copying the entire Value structure including its internal storage.
     memcpy((char *) v2, (char *) &v, sizeof(Value));

1540-1540: Thread-local variable should be documented.

The thread-local callDepth variable is introduced without documentation about its purpose or usage pattern.

+// Track recursion depth per thread to detect infinite recursion in parallel evaluation
 thread_local size_t EvalState::callDepth = 0;

2813-2815: Consider handling nFailed case in switch statement.

The new nFailed value type is marked as unreachable in forceValue() results, but it would be safer to handle it explicitly.

-    // Cannot be returned by forceValue().
-    case nThunk:
-    case nFailed:
-        unreachable();
+    case nThunk:
+        unreachable(); // Cannot be returned by forceValue()
+    case nFailed:
+        error<AssertionError>("cannot compare failed values").debugThrow();
src/libexpr/parallel-eval.cc (3)

101-126: Potential integer overflow in priority key calculation.

The key calculation (uint64_t(item.second) << 48) could silently truncate if item.second (uint8_t) multiplied by the shift exceeds expected bounds.

-            auto key = (uint64_t(item.second) << 48) | dist(rd);
+            // Priority in upper 16 bits, random value in lower 48 bits
+            auto key = (static_cast<uint64_t>(item.second) << 48) | dist(rd);

175-175: Consider making cache line size configurable.

The hard-coded cache line alignment of 64 bytes may not be optimal for all architectures (some use 128 bytes).

-} __attribute__((aligned(64))); // cache line alignment to prevent false sharing
+} __attribute__((aligned(std::hardware_destructive_interference_size))); // cache line alignment to prevent false sharing

Note: std::hardware_destructive_interference_size requires C++17 and may not be available on all compilers. Consider using a compile-time constant that can be configured per platform.


221-224: Error message could be more informative.

When detecting infinite recursion with a single eval core, the error message could explain why this happens in parallel evaluation.

-        state.error<InfiniteRecursionError>("infinite recursion encountered")
+        state.error<InfiniteRecursionError>("infinite recursion encountered (parallel evaluation requires multiple cores to resolve circular dependencies)")
📜 Review details

Configuration used: CodeRabbit UI

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 09b2a9c and 3d8127c.

📒 Files selected for processing (45)
  • .github/workflows/build.yml (2 hunks)
  • packaging/dependencies.nix (1 hunks)
  • src/libexpr-c/nix_api_value.cc (1 hunks)
  • src/libexpr-c/nix_api_value.h (1 hunks)
  • src/libexpr-tests/value/print.cc (2 hunks)
  • src/libexpr-tests/value/value.cc (0 hunks)
  • src/libexpr/eval-gc.cc (3 hunks)
  • src/libexpr/eval.cc (27 hunks)
  • src/libexpr/include/nix/expr/counter.hh (1 hunks)
  • src/libexpr/include/nix/expr/eval-inline.hh (3 hunks)
  • src/libexpr/include/nix/expr/eval-settings.hh (1 hunks)
  • src/libexpr/include/nix/expr/eval.hh (10 hunks)
  • src/libexpr/include/nix/expr/meson.build (2 hunks)
  • src/libexpr/include/nix/expr/nixexpr.hh (2 hunks)
  • src/libexpr/include/nix/expr/parallel-eval.hh (1 hunks)
  • src/libexpr/include/nix/expr/symbol-table.hh (7 hunks)
  • src/libexpr/include/nix/expr/value.hh (17 hunks)
  • src/libexpr/meson.build (2 hunks)
  • src/libexpr/nixexpr.cc (1 hunks)
  • src/libexpr/parallel-eval.cc (1 hunks)
  • src/libexpr/primops.cc (5 hunks)
  • src/libexpr/primops/fetchClosure.cc (1 hunks)
  • src/libexpr/print-ambiguous.cc (1 hunks)
  • src/libexpr/print.cc (3 hunks)
  • src/libexpr/symbol-table.cc (1 hunks)
  • src/libexpr/value-to-json.cc (2 hunks)
  • src/libexpr/value-to-xml.cc (1 hunks)
  • src/libfetchers/filtering-source-accessor.cc (3 hunks)
  • src/libflake/flake.cc (1 hunks)
  • src/libstore/globals.cc (1 hunks)
  • src/libstore/include/nix/store/globals.hh (1 hunks)
  • src/libstore/include/nix/store/remote-store.hh (1 hunks)
  • src/libutil/experimental-features.cc (2 hunks)
  • src/libutil/include/nix/util/experimental-features.hh (1 hunks)
  • src/libutil/include/nix/util/pos-table.hh (2 hunks)
  • src/libutil/include/nix/util/util.hh (1 hunks)
  • src/libutil/mounted-source-accessor.cc (3 hunks)
  • src/nix/flake.cc (14 hunks)
  • src/nix/main.cc (2 hunks)
  • src/nix/search.cc (4 hunks)
  • tests/functional/flakes/show.sh (1 hunks)
  • tests/functional/lang/eval-fail-blackhole.err.exp (1 hunks)
  • tests/functional/lang/eval-fail-recursion.err.exp (1 hunks)
  • tests/functional/lang/eval-fail-scope-5.err.exp (1 hunks)
  • tests/functional/misc.sh (1 hunks)
💤 Files with no reviewable changes (1)
  • src/libexpr-tests/value/value.cc
🧰 Additional context used
🧬 Code graph analysis (16)
src/libutil/include/nix/util/pos-table.hh (1)
src/libutil/pos-table.cc (3)
  • p (9-49)
  • p (9-9)
  • it (27-27)
src/libutil/mounted-source-accessor.cc (2)
src/libfetchers/filtering-source-accessor.cc (2)
  • path (92-95)
  • path (92-92)
src/libutil/include/nix/util/mounted-source-accessor.hh (2)
  • mountPoint (9-9)
  • mountPoint (15-15)
src/libfetchers/filtering-source-accessor.cc (1)
src/libfetchers/include/nix/fetchers/filtering-source-accessor.hh (15)
  • path (35-35)
  • path (37-37)
  • path (39-39)
  • path (41-41)
  • path (43-43)
  • path (45-45)
  • path (47-47)
  • path (49-49)
  • path (51-51)
  • path (53-53)
  • path (59-59)
  • path (64-64)
  • path (96-96)
  • path (98-98)
  • prefix (76-76)
src/libexpr/symbol-table.cc (2)
src/libexpr/include/nix/expr/value.hh (8)
  • p (1199-1199)
  • s (1143-1146)
  • s (1143-1143)
  • s (1148-1148)
  • s (1150-1150)
  • s (1152-1152)
  • v (766-777)
  • v (766-766)
src/libexpr/include/nix/expr/symbol-table.hh (15)
  • ContiguousArena (37-37)
  • bytes (39-39)
  • s (229-229)
  • s (240-244)
  • s (240-240)
  • s (281-284)
  • s (281-281)
  • arena (251-254)
  • Symbol (56-59)
  • Symbol (56-56)
  • Symbol (62-65)
  • SymbolStr (114-117)
  • SymbolStr (119-119)
  • key (167-170)
  • key (167-167)
src/libexpr/include/nix/expr/parallel-eval.hh (1)
src/libexpr/parallel-eval.cc (7)
  • Executor (15-23)
  • Executor (25-39)
  • FutureVector (128-135)
  • spawn (101-126)
  • spawn (101-101)
  • spawn (137-143)
  • spawn (137-137)
src/libexpr/print.cc (1)
src/libexpr/include/nix/expr/value.hh (2)
  • v (766-777)
  • v (766-766)
src/libexpr/primops.cc (2)
src/libstore/store-registration.cc (1)
  • make_ref (59-59)
src/libexpr/include/nix/expr/eval.hh (17)
  • v (536-539)
  • v (536-536)
  • v (541-541)
  • v (547-547)
  • v (552-552)
  • v (553-553)
  • v (554-554)
  • v (556-556)
  • v (561-561)
  • v (565-565)
  • v (566-566)
  • v (567-572)
  • v (573-573)
  • v (592-592)
  • v (757-757)
  • v (842-842)
  • settings (715-715)
src/libexpr/value-to-json.cc (5)
src/libexpr/include/nix/expr/value.hh (17)
  • state (156-157)
  • state (169-169)
  • state (174-181)
  • state (793-793)
  • state (802-802)
  • state (1026-1026)
  • pos (1241-1241)
  • i (859-862)
  • i (859-859)
  • a (1167-1170)
  • a (1167-1167)
  • e (1184-1187)
  • e (1184-1184)
  • e (1194-1197)
  • e (1194-1194)
  • e (1211-1214)
  • e (1211-1211)
src/libexpr/include/nix/expr/parallel-eval.hh (2)
  • state (54-54)
  • work (78-78)
src/libexpr/eval.cc (4)
  • state (1130-1153)
  • state (1130-1130)
  • copyContext (2313-2318)
  • copyContext (2313-2313)
src/libexpr/include/nix/expr/eval.hh (13)
  • pos (594-595)
  • pos (611-618)
  • pos (641-641)
  • pos (646-646)
  • pos (651-655)
  • pos (672-672)
  • pos (786-786)
  • pos (932-932)
  • context (620-620)
  • context (916-916)
  • copyContext (183-186)
  • e (520-520)
  • elem (513-513)
src/libexpr/parallel-eval.cc (4)
  • spawn (101-126)
  • spawn (101-101)
  • spawn (137-143)
  • spawn (137-137)
src/libexpr/eval-gc.cc (2)
src/libutil/file-system.cc (7)
  • readFile (251-264)
  • readFile (251-251)
  • readFile (266-269)
  • readFile (266-266)
  • readFile (271-298)
  • readFile (271-271)
  • path (850-850)
src/libutil/unix/file-descriptor.cc (2)
  • readFile (32-39)
  • readFile (32-32)
src/nix/main.cc (2)
src/libutil/current-process.cc (2)
  • setStackSize (63-81)
  • setStackSize (63-63)
src/libmain/shared.cc (2)
  • handleExceptions (314-355)
  • handleExceptions (314-314)
src/libexpr/parallel-eval.cc (3)
src/libexpr/include/nix/expr/parallel-eval.hh (7)
  • evalSettings (48-48)
  • Executor (50-50)
  • Executor (52-52)
  • state (54-54)
  • items (58-58)
  • FutureVector (74-74)
  • work (78-78)
src/libexpr/include/nix/expr/value.hh (24)
  • state (156-157)
  • state (169-169)
  • state (174-181)
  • state (793-793)
  • state (802-802)
  • state (1026-1026)
  • n (205-208)
  • n (205-205)
  • n (1128-1131)
  • n (1128-1128)
  • n (1133-1136)
  • n (1133-1133)
  • n (1216-1219)
  • n (1216-1216)
  • v (766-777)
  • v (766-766)
  • waitOnThunk (814-814)
  • p0_ (524-542)
  • p0_ (524-524)
  • p0 (788-791)
  • pd (598-621)
  • static_cast (514-517)
  • notifyWaiters (811-811)
  • pos (1241-1241)
src/libexpr/include/nix/expr/eval.hh (22)
  • v (536-539)
  • v (536-536)
  • v (541-541)
  • v (547-547)
  • v (552-552)
  • v (553-553)
  • v (554-554)
  • v (556-556)
  • v (561-561)
  • v (565-565)
  • v (566-566)
  • v (567-572)
  • v (573-573)
  • v (592-592)
  • pos (594-595)
  • pos (611-618)
  • pos (641-641)
  • pos (646-646)
  • pos (651-655)
  • pos (672-672)
  • pos (786-786)
  • pos (932-932)
src/libexpr/include/nix/expr/eval-inline.hh (2)
src/libexpr/include/nix/expr/value.hh (16)
  • state (156-157)
  • state (169-169)
  • state (174-181)
  • state (793-793)
  • state (802-802)
  • state (1026-1026)
  • pos (1241-1241)
  • p0_ (524-542)
  • p0_ (524-524)
  • p0 (788-791)
  • pd (598-621)
  • static_cast (514-517)
  • p1 (781-785)
  • waitOnThunk (814-814)
  • untagPointer (509-512)
  • untagPointer (509-509)
src/libexpr/parallel-eval.cc (2)
  • waitOnThunk (186-245)
  • waitOnThunk (186-186)
src/nix/flake.cc (3)
src/libstore/async-path-writer.cc (1)
  • futures (116-124)
src/libexpr/include/nix/expr/parallel-eval.hh (2)
  • state (54-54)
  • evalSettings (48-48)
src/libcmd/repl.cc (2)
  • s (103-103)
  • s (104-104)
src/libexpr/include/nix/expr/symbol-table.hh (2)
src/libexpr/include/nix/expr/value.hh (14)
  • s (1143-1146)
  • s (1143-1143)
  • s (1148-1148)
  • s (1150-1150)
  • s (1152-1152)
  • p (1199-1199)
  • n (205-208)
  • n (205-205)
  • n (1128-1131)
  • n (1128-1128)
  • n (1133-1136)
  • n (1133-1133)
  • n (1216-1219)
  • n (1216-1216)
src/libexpr/symbol-table.cc (1)
  • SymbolStr (45-61)
src/libexpr/eval.cc (4)
src/libexpr/include/nix/expr/value.hh (49)
  • isFinished (85-88)
  • isFinished (85-85)
  • isFinished (1029-1032)
  • isa (1004-1007)
  • isa (1004-1004)
  • isa (1038-1041)
  • isa (1043-1046)
  • isa (1055-1058)
  • isa (1060-1063)
  • isa (1065-1068)
  • isa (1070-1073)
  • isa (1123-1126)
  • isa (1226-1229)
  • isa (1231-1234)
  • isa (1236-1239)
  • v (766-777)
  • v (766-766)
  • path (689-693)
  • path (689-689)
  • path (710-713)
  • path (710-710)
  • path (745-748)
  • path (745-745)
  • path (1154-1154)
  • path (1155-1155)
  • state (156-157)
  • state (169-169)
  • state (174-181)
  • state (793-793)
  • state (802-802)
  • state (1026-1026)
  • e (1184-1187)
  • e (1184-1184)
  • e (1194-1197)
  • e (1194-1194)
  • e (1211-1214)
  • e (1211-1211)
  • i (859-862)
  • i (859-859)
  • pos (1241-1241)
  • s (1143-1146)
  • s (1143-1143)
  • s (1148-1148)
  • s (1150-1150)
  • s (1152-1152)
  • string (683-687)
  • string (683-683)
  • string (740-743)
  • string (740-740)
src/libexpr/include/nix/expr/eval.hh (48)
  • v (536-539)
  • v (536-536)
  • v (541-541)
  • v (547-547)
  • v (552-552)
  • v (553-553)
  • v (554-554)
  • v (556-556)
  • v (561-561)
  • v (565-565)
  • v (566-566)
  • v (567-572)
  • v (573-573)
  • v (592-592)
  • env (526-526)
  • env (527-527)
  • env (528-528)
  • env (761-761)
  • path (428-428)
  • path (433-433)
  • path (442-442)
  • path (447-447)
  • path (480-480)
  • path (481-481)
  • path (496-496)
  • path (503-503)
  • e (520-520)
  • expr (333-341)
  • expr (333-333)
  • fun (803-803)
  • fun (805-805)
  • fun (807-811)
  • fun (807-807)
  • fun (989-989)
  • args (817-817)
  • pos (594-595)
  • pos (611-618)
  • pos (641-641)
  • pos (646-646)
  • pos (651-655)
  • pos (672-672)
  • pos (786-786)
  • pos (932-932)
  • s (486-486)
  • s (487-487)
  • s (601-601)
  • table (205-218)
  • table (205-205)
src/libexpr/include/nix/expr/counter.hh (5)
  • Counter (13-13)
  • enabled (30-33)
  • enabled (35-38)
  • enabled (40-43)
  • enabled (45-48)
src/libexpr/eval-cache.cc (2)
  • forceValue (392-424)
  • forceValue (392-392)
src/libexpr/include/nix/expr/value.hh (2)
src/libexpr/parallel-eval.cc (4)
  • notifyWaiters (248-253)
  • notifyWaiters (248-248)
  • waitOnThunk (186-245)
  • waitOnThunk (186-186)
src/libexpr/include/nix/expr/eval.hh (24)
  • v (536-539)
  • v (536-536)
  • v (541-541)
  • v (547-547)
  • v (552-552)
  • v (553-553)
  • v (554-554)
  • v (556-556)
  • v (561-561)
  • v (565-565)
  • v (566-566)
  • v (567-572)
  • v (573-573)
  • v (592-592)
  • v (757-757)
  • v (842-842)
  • pos (594-595)
  • pos (611-618)
  • pos (641-641)
  • pos (646-646)
  • pos (651-655)
  • pos (672-672)
  • pos (786-786)
  • pos (932-932)
🪛 actionlint (1.7.7)
.github/workflows/build.yml

162-162: shellcheck reported issue in this script: SC2236:style:4:6: Use -n instead of ! -z

(shellcheck)


162-162: shellcheck reported issue in this script: SC2155:warning:9:8: Declare and assign separately to avoid masking return values

(shellcheck)

⏰ 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). (4)
  • GitHub Check: build_x86_64-linux / build
  • GitHub Check: build_aarch64-darwin / build
  • GitHub Check: build_aarch64-linux / build
  • GitHub Check: build_x86_64-darwin / build

Comment on lines +2188 to 2189
// FIXME: race, thunk might be updated by another thread
auto dts = debugRepl && i.value->isThunk() ? makeDebugTraceStacker(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document the race condition and its implications.

The FIXME comment mentions a race where a thunk might be updated by another thread, but doesn't explain the implications or how it should be addressed.

-                    // FIXME: race, thunk might be updated by another thread
+                    // FIXME: race condition - the thunk might be forced by another thread
+                    // between the isThunk() check and accessing thunk().expr/env.
+                    // This could lead to accessing invalid memory. Need proper synchronization.
📝 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
// FIXME: race, thunk might be updated by another thread
auto dts = debugRepl && i.value->isThunk() ? makeDebugTraceStacker(
// FIXME: race condition - the thunk might be forced by another thread
// between the isThunk() check and accessing thunk().expr/env.
// This could lead to accessing invalid memory. Need proper synchronization.
auto dts = debugRepl && i.value->isThunk() ? makeDebugTraceStacker(
🤖 Prompt for AI Agents
In src/libexpr/eval.cc around lines 2188-2189, the FIXME about a race where a
thunk may be updated by another thread must be expanded into a concrete comment:
describe the race scenario (concurrent evaluation/updates of the thunk leading
to inconsistent or partial debugTraceStacker state, lost updates, or observing
an intermediate value), enumerate the possible implications for debugging (wrong
stack traces, crashes, or missed updates), and list acceptable mitigations
(acquire the thunk's mutex or use an atomic/snapshot read before creating the
debug trace stacker, or document that callers must hold a particular lock), then
add a clear TODO specifying the preferred mitigation strategy and any follow-up
tasks (e.g., add locking, convert thunk fields to atomics, or restrict
single-threaded access), and reference relevant code locations or threading
invariants to guide the actual fix.

Comment on lines +69 to 76
auto state(state_.readLock());
const auto idx = p.id - 1;
/* we want the last key <= idx, so we'll take prev(first key > idx).
this is guaranteed to never rewind origin.begin because the first
key is always 0. */
const auto pastOrigin = origins.upper_bound(idx);
/* We want the last key <= idx, so we'll take prev(first key >
idx). This is guaranteed to never rewind origin.begin
because the first key is always 0. */
const auto pastOrigin = state->origins.upper_bound(idx);
return &std::prev(pastOrigin)->second;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential UB in resolve() when origins is empty (prev(begin)).

Guard empty map and idx before calling prev(). Otherwise this dereferences before begin.

-        auto state(state_.readLock());
-        const auto idx = p.id - 1;
+        auto state(state_.readLock());
+        const auto idx = p.id - 1;
+        if (state->origins.empty()) return nullptr;
         /* We want the last key <= idx, so we'll take prev(first key >
            idx). This is guaranteed to never rewind origin.begin
            because the first key is always 0. */
-        const auto pastOrigin = state->origins.upper_bound(idx);
-        return &std::prev(pastOrigin)->second;
+        const auto pastOrigin = state->origins.upper_bound(idx);
+        if (pastOrigin == state->origins.begin()) return nullptr;
+        return &std::prev(pastOrigin)->second;
📝 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
auto state(state_.readLock());
const auto idx = p.id - 1;
/* we want the last key <= idx, so we'll take prev(first key > idx).
this is guaranteed to never rewind origin.begin because the first
key is always 0. */
const auto pastOrigin = origins.upper_bound(idx);
/* We want the last key <= idx, so we'll take prev(first key >
idx). This is guaranteed to never rewind origin.begin
because the first key is always 0. */
const auto pastOrigin = state->origins.upper_bound(idx);
return &std::prev(pastOrigin)->second;
}
auto state(state_.readLock());
const auto idx = p.id - 1;
if (state->origins.empty()) return nullptr;
/* We want the last key <= idx, so we'll take prev(first key >
idx). This is guaranteed to never rewind origin.begin
because the first key is always 0. */
const auto pastOrigin = state->origins.upper_bound(idx);
if (pastOrigin == state->origins.begin()) return nullptr;
return &std::prev(pastOrigin)->second;
}
🤖 Prompt for AI Agents
In src/libutil/include/nix/util/pos-table.hh around lines 69-76, resolve() can
call std::prev(pastOrigin) unsafely if origins is empty or pastOrigin equals
begin; add guards: if state->origins.empty() return a safe failure (e.g. nullptr
or throw) immediately, then compute pastOrigin and if pastOrigin ==
state->origins.begin() return &pastOrigin->second else return
&std::prev(pastOrigin)->second so prev() is never called on begin().

Copy link
Copy Markdown

@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: 2

♻️ Duplicate comments (3)
src/libexpr/eval-gc.cc (1)

99-106: Restore pthread_attr_get_np fallback; current #error breaks BSD/non-glibc builds

The FreeBSD/non-glibc path is hard-erroring. Implement the pthread_attr_get_np branch with proper error checks.

Apply:

-#    ifdef HAVE_PTHREAD_GETATTR_NP
-    if (pthread_getattr_np(pthread_id, &pattr))
-        throw Error("fixupBoehmStackPointer: pthread_getattr_np failed");
-#    else
-#      error "Need  `pthread_attr_get_np`"
-#    endif
+#    ifdef HAVE_PTHREAD_GETATTR_NP
+    if (pthread_getattr_np(pthread_id, &pattr))
+        throw Error("fixupBoehmStackPointer: pthread_getattr_np failed");
+#    elif defined(HAVE_PTHREAD_ATTR_GET_NP)
+    if (pthread_attr_get_np(pthread_id, &pattr))
+        throw Error("fixupBoehmStackPointer: pthread_attr_get_np failed");
+#    else
+#      error "Need pthread_getattr_np or pthread_attr_get_np"
+#    endif
src/libexpr/include/nix/expr/symbol-table.hh (2)

240-244: Add bounds and alignment checks in operator[] and launder the pointer

Prevent out-of-bounds access and misaligned loads; also use std::launder for safety after placement construction.

     SymbolStr operator[](Symbol s) const
     {
-        assert(s.id);
-        return SymbolStr(*reinterpret_cast<const SymbolValue *>(arena.data + s.id));
+        assert(s.id);
+        // Basic integrity checks
+        assert(s.id < arena.size); // offset within arena
+        assert((s.id % alignment) == 0); // start of an aligned record
+        assert(s.id + sizeof(SymbolValue) <= arena.size); // header fits
+        auto p = std::launder(reinterpret_cast<const SymbolValue *>(arena.data + s.id));
+        return SymbolStr(*p);
     }

256-273: Fix dump() record-advance logic to account for header in alignment calculation

Current padding uses only the string length; it should include the Value header to remain aligned. Otherwise, iteration can desynchronize and hit asserts or skip symbols.

     void dump(T callback) const
     {
         std::string_view left{arena.data, arena.size};
         left = left.substr(alignment);
         while (true) {
             if (left.empty())
                 break;
             left = left.substr(sizeof(Value));
             auto p = left.find('\0');
             assert(p != left.npos);
             auto sym = left.substr(0, p);
             callback(sym);
             // skip alignment padding
-            auto n = sym.size() + 1;
-            left = left.substr(n + (n % alignment ? alignment - (n % alignment) : 0));
+            auto n = sizeof(Value) + sym.size() + 1; // header + string + NUL
+            auto alignedSize = ((n + alignment - 1) / alignment) * alignment;
+            left = left.substr(alignedSize - sizeof(Value));
         }
     }
🧹 Nitpick comments (11)
src/libexpr/eval-gc.cc (4)

38-40: Fix comment: /proc/meminfo, not /proc/cpuinfo

The comment references cpuinfo but the code reads meminfo.

Apply:

-    /* On Linux, use the `MemAvailable` or `MemFree` fields from
-       /proc/cpuinfo. */
+    /* On Linux, use the `MemAvailable` or `MemFree` fields from
+       /proc/meminfo. */

41-59: Harden Linux meminfo parsing against read failures

readFile("/proc/meminfo") can throw; let’s fall back instead of propagating.

Apply:

-    {
+    try {
         std::unordered_map<std::string, std::string> fields;
         for (auto & line :
              tokenizeString<std::vector<std::string>>(readFile(std::filesystem::path("/proc/meminfo")), "\n")) {
             auto colon = line.find(':');
             if (colon == line.npos)
                 continue;
             fields.emplace(line.substr(0, colon), trim(line.substr(colon + 1)));
         }

         auto i = fields.find("MemAvailable");
         if (i == fields.end())
             i = fields.find("MemFree");
         if (i != fields.end()) {
             auto kb = tokenizeString<std::vector<std::string>>(i->second, " ");
             if (kb.size() == 2 && kb[1] == "kB")
                 return string2Int<size_t>(kb[0]).value_or(0) * 1024;
         }
-    }
+    } catch (const std::exception &) {
+        /* Fall through to generic sysconf-based fallback below. */
+    }

151-159: Comment/code mismatch (80% vs 50%)

Comment says 80% but code uses ~50% of free RAM.

Apply one of:

  • Update comment to 50%:
-    /* Set the initial heap size to something fairly big (80% of
+    /* Set the initial heap size to something fairly big (50% of
        free RAM, up to a maximum of 4 GiB) so that in most cases
  • Or change code to 80% (if intended).

164-166: Avoid float math; use integer halving

free * 0.5 promotes to double and back. Use integer division for precision.

Apply:

-        auto free = getFreeMem();
-        size = std::max(size, std::min((size_t) (free * 0.5), maxSize));
+        auto free = getFreeMem();
+        auto halfFree = free / 2;
+        size = std::max(size, std::min(halfFree, maxSize));
src/libexpr/include/nix/expr/symbol-table.hh (7)

27-40: Make arena.data writable without const_casts and keep the pointer itself const

Construction writes through data; using const here forces const_cast elsewhere. Prefer a const pointer to mutable memory.

-    const char * data;
+    char * const data;

Follow-up: drop const_casts at call sites (e.g., SymbolStr::SymbolStr in symbol-table.cc).


32-36: Confirm cache-line isolation intent is achieved

aligned(64) on the atomic ensures its start is 64B-aligned, but only if the compiler inserts padding before it. Given preceding fields, this is likely fine, but please verify with static_asserts or sizeof/offsetof in a unit test.

Example check:

  • Expect offsetof(ContiguousArena, size) % 64 == 0.
  • Optionally add alignas(64) to the struct and reorder members if needed.

101-109: Key stores arena by reference; ensure lifetime and thread-safety are clear

Key captures ContiguousArena& and is used inside a concurrent container; document that arena outlives the set and is append-only. If not guaranteed, consider a pointer.


139-149: size()/empty() are O(n) due to std::string_view length computation

This is fine if not hot; otherwise consider storing the length next to the string to avoid repeated scans.


219-225: Constructor reserves ID 0; also assert arena invariants

Add cheap asserts to catch early misuse.

     SymbolTable()
         : arena(1 << 30)
     {
         // Reserve symbol ID 0 and ensure alignment of the first allocation.
         arena.allocate(alignment);
+        assert((uintptr_t)arena.data % alignment == 0);
+        assert(arena.size == alignment);
     }

251-254: Atomic load semantics for totalSize()

If used for metrics only, relaxed is fine and cheaper; otherwise use acquire. Make it explicit.

-    size_t totalSize() const
-    {
-        return arena.size;
-    }
+    size_t totalSize() const
+    {
+        return arena.size.load(std::memory_order_relaxed);
+    }

22-24: Consistent access: consider using Value APIs or document the aliasing

Returning a string_view by reinterpreting (this + 1) is correct for your layout but bypasses Value’s accessor; add a comment asserting that mkString stored the same pointer and that the trailing bytes remain immutable.

📜 Review details

Configuration used: CodeRabbit UI

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 3d8127c and 3425c5d.

📒 Files selected for processing (4)
  • src/libexpr/eval-gc.cc (3 hunks)
  • src/libexpr/include/nix/expr/counter.hh (1 hunks)
  • src/libexpr/include/nix/expr/symbol-table.hh (7 hunks)
  • src/libexpr/meson.build (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/libexpr/include/nix/expr/counter.hh
  • src/libexpr/meson.build
🧰 Additional context used
🧬 Code graph analysis (2)
src/libexpr/eval-gc.cc (2)
src/libutil/file-system.cc (7)
  • readFile (251-264)
  • readFile (251-251)
  • readFile (266-269)
  • readFile (266-266)
  • readFile (271-298)
  • readFile (271-271)
  • path (850-850)
src/libutil/unix/file-descriptor.cc (2)
  • readFile (32-39)
  • readFile (32-32)
src/libexpr/include/nix/expr/symbol-table.hh (2)
src/libexpr/include/nix/expr/value.hh (14)
  • s (1143-1146)
  • s (1143-1143)
  • s (1148-1148)
  • s (1150-1150)
  • s (1152-1152)
  • p (1199-1199)
  • n (205-208)
  • n (205-205)
  • n (1128-1131)
  • n (1128-1128)
  • n (1133-1136)
  • n (1133-1133)
  • n (1216-1219)
  • n (1216-1216)
src/libexpr/symbol-table.cc (1)
  • SymbolStr (45-61)
⏰ 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). (4)
  • GitHub Check: build_aarch64-darwin / build
  • GitHub Check: build_x86_64-darwin / build
  • GitHub Check: build_x86_64-linux / build
  • GitHub Check: build_aarch64-linux / build
🔇 Additional comments (2)
src/libexpr/eval-gc.cc (2)

82-84: Confirm assumption: no GC roots in coroutines

Assuming no GC roots live on coroutine stacks is risky. Please confirm or document invariants/tests ensuring this remains true under parallel eval.


148-150: SP corrector hook: LGTM

Hooking and asserting the corrector is installed looks good.

Copy link
Copy Markdown

@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: 2

♻️ Duplicate comments (4)
src/libexpr/include/nix/expr/value.hh (4)

440-542: Complex concurrency design requires careful analysis.

The atomic p0 and non-atomic p1 design with the ordering constraint ("p1 must be written before p0") is a sophisticated approach to lock-free value updates. However, this creates several concerns:

  1. Race condition potential: The requirement that "p1 must always be updated before p0" (Line 459) must be strictly enforced across all code paths.

  2. Memory ordering: The use of memory_order_release for p0 writes and memory_order_acquire for reads is correct, but the implementation must be consistent.

  3. Debug output in production: Lines 539-541 contain debug output that may not be appropriate for production builds.

Apply this fix for the debug output:

-        else if (pd == pdThunk) {
-            printError("BAD FINISH %x", this);
-            unreachable();
-        }
+        else if (pd == pdThunk)
+            unreachable();

1308-1312: Race condition warning for thunk accessor.

The comment "FIXME: remove this since reading it is racy" correctly identifies that accessing thunk data is unsafe in a multithreaded context. This method should be marked deprecated or removed.

Consider deprecating this method or adding runtime checks:

+    [[deprecated("Reading thunk data is racy in multithreaded evaluation")]]
     ClosureThunk thunk() const noexcept

1319-1323: Race condition warning for app accessor.

The comment "FIXME: remove this since reading it is racy" correctly identifies the same threading issue as with the thunk accessor. This should also be addressed.


781-791: Testing-only blackhole function should be restricted.

The mkBlackhole() function is marked as "only used for testing" but is in the public interface. This could be accidentally used in production code.

📜 Review details

Configuration used: CodeRabbit UI

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 3425c5d and 2a001a8.

📒 Files selected for processing (1)
  • src/libexpr/include/nix/expr/value.hh (17 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/libexpr/include/nix/expr/value.hh (2)
src/libexpr/parallel-eval.cc (4)
  • notifyWaiters (248-253)
  • notifyWaiters (248-248)
  • waitOnThunk (186-245)
  • waitOnThunk (186-186)
src/libexpr/include/nix/expr/eval.hh (24)
  • v (536-539)
  • v (536-536)
  • v (541-541)
  • v (547-547)
  • v (552-552)
  • v (553-553)
  • v (554-554)
  • v (556-556)
  • v (561-561)
  • v (565-565)
  • v (566-566)
  • v (567-572)
  • v (573-573)
  • v (592-592)
  • v (757-757)
  • v (842-842)
  • pos (594-595)
  • pos (611-618)
  • pos (641-641)
  • pos (646-646)
  • pos (651-655)
  • pos (672-672)
  • pos (786-786)
  • pos (932-932)
⏰ 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). (4)
  • GitHub Check: build_aarch64-darwin / build
  • GitHub Check: build_aarch64-linux / build
  • GitHub Check: build_x86_64-darwin / build
  • GitHub Check: build_x86_64-linux / build
🔇 Additional comments (16)
src/libexpr/include/nix/expr/value.hh (16)

336-336: LGTM! Added Failed payload type mapping.

The macro definition correctly includes the Failed payload type in the storage system.


544-572: LGTM! Proper memory ordering in payload setters.

The payload setting functions correctly use the finish() method to ensure proper memory ordering. The bit manipulation and alignment assertions provide good safety checks.


587-621: LGTM! Efficient internal type decoding.

The getInternalType() function efficiently decodes the bit-packed discriminator information. The switch statement correctly handles all primary discriminator cases.


623-641: LGTM! Clean macro-based payload accessors.

The macro generates appropriate getter/setter pairs for pointer-based payloads, maintaining the correct memory ordering through the established payload setter functions.


643-698: LGTM! Consistent payload access patterns.

The storage getters correctly extract data from p1 for single-word payloads and use proper bit manipulation for pointer-based payloads.


695-698: LGTM! Failed payload accessor.

The Failed pointer getter is implemented consistently with other pointer-based payload types.


750-753: LGTM! Failed payload setter.

The setter correctly uses setSingleDWordPayload for storing the Failed pointer.


793-807: LGTM! Forward declarations for multithreaded operations.

The forward declarations for force, waitOnThunk, and notifyWaiters establish the interface for the multithreaded evaluation system.


810-814: LGTM! Template specialization declarations.

The explicit template specializations ensure that the multithreaded operations are properly instantiated for the target pointer size.


1028-1032: LGTM! Efficient finished check.

The isFinished() method provides a clean interface to check if a value is in weak-head normal form.


1048-1052: LGTM! Updated blackhole detection.

The isBlackhole() implementation correctly identifies pending and awaited states as blackhole conditions in the new state machine.


1070-1073: LGTM! Failed value predicate.

The isFailed() method provides a clean interface to check for failed values.


1079-1116: LGTM! Updated type mapping with failure support.

The type() method correctly maps the new tFailed internal type to nFailed and handles the new pending/awaited states as thunk types.


1221-1224: LGTM! Failed value constructor.

The mkFailed() method correctly creates a Failed value with the current exception. The use of std::current_exception() is appropriate for capturing the active exception.


1244-1251: LGTM! Updated trivial value detection.

The isTrivial() documentation correctly reflects that finished values are considered trivial in the new evaluation model.


1335-1338: LGTM! Failed value accessor.

The failed() method provides clean access to the Failed payload, consistent with other payload accessors.

Copy link
Copy Markdown

@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: 3

♻️ Duplicate comments (1)
src/libexpr/include/nix/expr/value.hh (1)

572-585: Release-store in setThunkPayload is correct.

This now preserves the “p1-before-p0” ordering without spurious wakeups for awaited states. Thanks for addressing the earlier fence concern.

🧹 Nitpick comments (3)
src/libexpr/include/nix/expr/eval.hh (1)

377-381: Good move to SharedSync for caches; consider similar treatment for remaining maps.

lookupPathResolved, primOpCalls, and attrSelects are still marked non-thread-safe. Consider SharedSync or per-thread aggregation with periodic merge to avoid lock contention.

Also applies to: 392-393, 398-398

src/libexpr/include/nix/expr/value.hh (2)

1217-1221: Failed payload: confirm GC ownership and lifetime.

mkFailed() allocates with new on a gc-derived type. Please confirm this is traced by the GC in both Boehm and non-Boehm builds and that any printers/visitors handle nFailed without dereferencing a null ex.

Optionally add a brief docstring to ValueType::nFailed and Value::failed() clarifying semantics.

Also applies to: 1331-1335


1044-1049: Expose racy accessors only where safe.

isBlackhole() is fine. The “FIXME: remove this since reading it is racy” for thunk()/app() remains—consider #ifdef UNIT_TESTS or moving to a test-only header to prevent accidental use.

Also applies to: 1304-1309, 1315-1320

📜 Review details

Configuration used: CodeRabbit UI

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 2a001a8 and 92788a0.

📒 Files selected for processing (4)
  • src/libexpr/eval-gc.cc (3 hunks)
  • src/libexpr/include/nix/expr/eval.hh (10 hunks)
  • src/libexpr/include/nix/expr/symbol-table.hh (7 hunks)
  • src/libexpr/include/nix/expr/value.hh (17 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/libexpr/include/nix/expr/symbol-table.hh
  • src/libexpr/eval-gc.cc
🧰 Additional context used
🧬 Code graph analysis (2)
src/libexpr/include/nix/expr/value.hh (2)
src/libexpr/parallel-eval.cc (4)
  • notifyWaiters (248-253)
  • notifyWaiters (248-248)
  • waitOnThunk (186-245)
  • waitOnThunk (186-186)
src/libexpr/include/nix/expr/eval.hh (24)
  • v (537-540)
  • v (537-537)
  • v (542-542)
  • v (548-548)
  • v (553-553)
  • v (554-554)
  • v (555-555)
  • v (557-557)
  • v (562-562)
  • v (566-566)
  • v (567-567)
  • v (568-573)
  • v (574-574)
  • v (593-593)
  • v (758-758)
  • v (843-843)
  • pos (595-596)
  • pos (612-619)
  • pos (642-642)
  • pos (647-647)
  • pos (652-656)
  • pos (673-673)
  • pos (787-787)
  • pos (933-933)
src/libexpr/include/nix/expr/eval.hh (2)
src/libexpr/primops.cc (2)
  • struct State (4312-4315)
  • re (4310-4334)
src/libexpr/eval.cc (3)
  • EvalState (378-380)
  • EvalState (187-375)
  • eval (1758-1784)
⏰ 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). (3)
  • GitHub Check: build_aarch64-darwin / build
  • GitHub Check: build_x86_64-darwin / build
  • GitHub Check: build_x86_64-linux / build
🔇 Additional comments (3)
src/libexpr/include/nix/expr/eval.hh (3)

193-193: RegexCache ownership change looks good; verify all call sites.

Return type moved to ref<RegexCache> and the member matches. Please confirm all makeRegexCache() callers were updated and no lingering shared_ptr assumptions remain.

Also applies to: 408-408


537-540: Inline forceValue forwarding is correct.


780-781: Per-thread callDepth via thread_local is appropriate.

Comment on lines +227 to +228
ref<Executor> executor;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Ensure executor is always non-null (avoid nullptr deref).

waitOnThunk() uses state.executor->evalCores; if executor is unset you'll crash. Enforce non-null in the constructor (initialize a single-threaded default) or guard all uses.

Would you like a patch that wires a default Executor{evalCores=1} in EvalState’s ctor?


Initialize executor in EvalState’s constructor
Ensure executor is never null by assigning a default single-threaded Executor (e.g. with evalCores=1) in the EvalState ctor to avoid null-pointer deref in waitOnThunk().

🤖 Prompt for AI Agents
In src/libexpr/include/nix/expr/eval.hh around lines 227-228, EvalState's member
ref<Executor> executor is not initialized; update the EvalState constructor to
create and assign a default single-threaded Executor (e.g. construct Executor
with evalCores=1) so executor is never null and waitOnThunk() cannot dereference
a null pointer. Instantiate the Executor with appropriate ownership semantics
and assign it to the executor field in the ctor, ensuring any required
headers/params are provided.

When useMaster is true, startMaster() acquires the state lock, then
calls isMasterRunning(), which calls addCommonSSHOpts(), which tries
to acquire the state lock again, causing a deadlock.

The solution is to move tmpDir out of the state. It doesn't need to be
there in the first place because it never changes.
Copy link
Copy Markdown

@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 (2)
src/libstore/ssh.cc (2)

118-125: Control master check ignores custom -S path; can skip starting our master and then use a non-existent socket

isMasterRunning() doesn’t pass the control socket path, but startMaster() later returns state->socketPath and callers use “-S ”. If a user-level master exists (default ControlPath), you’ll return our tmpDir path even though no master is listening there.

Apply these changes to scope the check to our socket:

-bool SSHMaster::isMasterRunning()
+bool SSHMaster::isMasterRunning(Path controlPath)
 {
     Strings args = {"-O", "check", hostnameAndUser};
     addCommonSSHOpts(args);
+    if (!controlPath.empty())
+        args.insert(args.end(), {"-S", controlPath});

And in the header:

-    bool isMasterRunning();
+    bool isMasterRunning(Path controlPath = "");

And in startMaster (see below).


249-253: Pass the socket path when checking master state

Tie the check to the socket we’ll actually use.

-    if (isMasterRunning())
+    if (isMasterRunning(state->socketPath))
         return state->socketPath;
🧹 Nitpick comments (2)
src/libstore/include/nix/store/ssh.hh (1)

30-31: Make tmpDir immutable and document lifetime/thread-safety

tmpDir is effectively write-once; marking it const prevents accidental reassignment and clarifies intent. Also, because Connections don’t hold a ref to tmpDir, verify SSHMaster outlives any in-flight ssh child using files under tmpDir.

-    ref<AutoDelete> tmpDir;
+    const ref<AutoDelete> tmpDir;

If you want to make usage clearer and avoid repeated recomputation of paths, consider adding:

  • a cached known-hosts file path (e.g., Path knownHostsFile;)
  • a cached control socket path (e.g., Path controlSocket;)
src/libstore/ssh.cc (1)

241-242: Use consistent path joining; avoid deref/cast of ref

Dereferencing tmpDir and C-style casting to Path is brittle. Use the same path API you used above.

-    state->socketPath = (Path) *tmpDir + "/ssh.sock";
+    state->socketPath = (tmpDir->path() / "ssh.sock").string();
📜 Review details

Configuration used: CodeRabbit UI

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 92788a0 and 4b34658.

📒 Files selected for processing (2)
  • src/libstore/include/nix/store/ssh.hh (2 hunks)
  • src/libstore/ssh.cc (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/libstore/ssh.cc (2)
src/libutil/include/nix/util/file-system.hh (1)
  • createTempDir (330-330)
src/libstore/include/nix/store/ssh.hh (2)
  • args (42-42)
  • getNixSshOpts (12-12)
⏰ 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). (4)
  • GitHub Check: build_x86_64-darwin / build
  • GitHub Check: build_aarch64-linux / build
  • GitHub Check: build_x86_64-linux / build
  • GitHub Check: build_aarch64-darwin / build
🔇 Additional comments (2)
src/libstore/include/nix/store/ssh.hh (1)

4-4: Include looks correct and necessary

Adding ref.hh is appropriate for the new ref<...> member.

src/libstore/ssh.cc (1)

87-88: Good: secure temp dir creation

Creating the tmp dir at construction with 0700 is correct and avoids races.

Comment on lines 97 to 103
if (!keyFile.empty())
args.insert(args.end(), {"-i", keyFile});
if (!sshPublicHostKey.empty()) {
std::filesystem::path fileName = state->tmpDir->path() / "host-key";
std::filesystem::path fileName = tmpDir->path() / "host-key";
writeFile(fileName.string(), authority.host + " " + sshPublicHostKey + "\n");
args.insert(args.end(), {"-oUserKnownHostsFile=" + fileName.string()});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Known_hosts line is malformed (missing key type and base64; wrong host format)

OpenSSH’s known_hosts requires “host keytype base64”. You’re writing raw bytes without a key type, and you don’t bracket non-default ports ([host]:port). This will cause host key verification to fail and may be racy if multiple threads rewrite the same file.

Apply this fix (defaults to ssh-ed25519; ideally plumb the real type):

-    if (!sshPublicHostKey.empty()) {
-        std::filesystem::path fileName = tmpDir->path() / "host-key";
-        writeFile(fileName.string(), authority.host + " " + sshPublicHostKey + "\n");
-        args.insert(args.end(), {"-oUserKnownHostsFile=" + fileName.string()});
-    }
+    if (!sshPublicHostKey.empty()) {
+        std::filesystem::path fileName = tmpDir->path() / "known_hosts";
+        // Use "[host]:port" when a non-default port is set.
+        std::string hostForKnownHosts =
+            authority.port ? fmt("[%s]:%d", authority.host, *authority.port)
+                           : std::string(authority.host);
+        // TODO: pass the real key type instead of defaulting.
+        const char *keyType = "ssh-ed25519";
+        writeFile(
+            fileName.string(),
+            hostForKnownHosts + " " + keyType + " " + base64::encode(sshPublicHostKey) + "\n");
+        args.insert(args.end(), {"-oUserKnownHostsFile=" + fileName.string()});
+    }

Optionally, write the file once (e.g., at construction) to avoid concurrent rewrites. I can send a follow-up patch using std::once_flag.

📝 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
if (!keyFile.empty())
args.insert(args.end(), {"-i", keyFile});
if (!sshPublicHostKey.empty()) {
std::filesystem::path fileName = state->tmpDir->path() / "host-key";
std::filesystem::path fileName = tmpDir->path() / "host-key";
writeFile(fileName.string(), authority.host + " " + sshPublicHostKey + "\n");
args.insert(args.end(), {"-oUserKnownHostsFile=" + fileName.string()});
}
if (!keyFile.empty())
args.insert(args.end(), {"-i", keyFile});
if (!sshPublicHostKey.empty()) {
std::filesystem::path fileName = tmpDir->path() / "known_hosts";
// Use "[host]:port" when a non-default port is set.
std::string hostForKnownHosts =
authority.port ? fmt("[%s]:%d", authority.host, *authority.port)
: std::string(authority.host);
// TODO: pass the real key type instead of defaulting.
const char *keyType = "ssh-ed25519";
writeFile(
fileName.string(),
hostForKnownHosts + " " + keyType + " " + base64::encode(sshPublicHostKey) + "\n");
args.insert(args.end(), {"-oUserKnownHostsFile=" + fileName.string()});
}
🤖 Prompt for AI Agents
In src/libstore/ssh.cc around lines 97 to 103, the known_hosts line is
malformed: you currently write raw key bytes without a key type or base64 and
you don’t bracket hosts with non-default ports, plus the file may be rewritten
concurrently; fix by formatting the entry as "host keytype base64key" (default
keytype to "ssh-ed25519" if the real type isn’t available), base64-encode
sshPublicHostKey before writing, bracket host as "[host]:port" when a
non-default port is used, and ensure the file is created/written only once
(e.g., move writeFile to construction or guard with a once_flag) so multiple
threads don’t race rewriting it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flake-regression-test Run the flake regressions test suite on this PR upload to s3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants