Skip to content

Conversation

@Radvendii
Copy link
Contributor

@Radvendii Radvendii commented Oct 27, 2025

Motivation

For big-picture motivation, see the tracking issue

This PR moves the ExprLambda data into our allocator

  • saves 32 bytes per ExprLambda which has Formals ({ foo, bar }: rather than x: )
  • before:
    • 8 bytes: Formals *
    • 4 bytes: padding in ExprLambda before formals
    • 24 bytes: std::vector
    • 8 bytes: ellipsis bool + padding
  • after:
    • 2 bytes: nFormals
    • 2 bytes: ellipsis bool + hasFormals bool + padding
    • 8 bytes: Formal *

And everything else stays the same.

Alternative Design

Presumably we were using a Formals * here rather than Formals directly so that we don't need to take up the extra space in the common case that there are no formals. One effect of using Formals * rather than Formals directly has been that we don't need to take up the extra space to store a Formals in the common case that there are no formals.

This PR does not make that case any worse, because we can fit the extra data (ellipses + hasFormals + nFormals) in the space that was padding before.

However, we could also choose to put all formals in one array and index into it, reducing the 8 byte Formal * to a 4 byte index.

IMO this is an optimization to look into in the future, when we start putting things in indexable vectors. (e.g. Exprs)

Context


Add 👍 to pull requests you find important.

The Nix maintainer team uses a GitHub project board to schedule and track reviews.

@Radvendii Radvendii requested a review from edolstra as a code owner October 27, 2025 20:53
@github-actions github-actions bot added the new-cli Relating to the "nix" command label Oct 27, 2025
@Radvendii Radvendii mentioned this pull request Oct 27, 2025
29 tasks
Comment on lines +487 to +489
uint16_t nFormals;
Formal * formalsStart;
Copy link
Member

@Ericson2314 Ericson2314 Oct 27, 2025

Choose a reason for hiding this comment

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

Suggested change
uint16_t nFormals;
Formal * formalsStart;
std::span<Formal> formals;

Can we do that?

Copy link
Member

Choose a reason for hiding this comment

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

oh we can't because padding. Fair enough. Add comment then?

Copy link
Member

Choose a reason for hiding this comment

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

oh and maybe make the underlying fields private too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That change would add 8 bytes to the struct.

  • std::span uses a uint32_t for the size (2 more bytes)
  • 6 bytes of padding

Copy link
Member

Choose a reason for hiding this comment

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

Yeah the private fields to preserve packing replaces the original suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In general I'm not a big fan of private fields, but I know I'm in the minority here and it doesn't matter to me that much.

However, we haven't been making the fields private in any of the other Expr subtypes. I'd rather change that for all the relevant Expr sub-types in a separate MR, rather than have it be inconsistent.

Copy link
Member

@Ericson2314 Ericson2314 Oct 28, 2025

Choose a reason for hiding this comment

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

I will say from a language design perspective, putting private information in the public header / interface file is disgusting, and C++ should feel bad about it. Does that match your feelings about private fields? That said, I do think abstract data types are fine in principle, and since we want to manually control layout while not foisting these encoding details on other code, I think private fields are the best tool we have in C++ for that job.

I see you have fixed it with bool hasFormals. I think we should have

struct FormalsSpan{
    std::span<Formal> formals;
    bool ellipsis;
}

and then

[[gnu::always_inline]] std::optional<std::span<FormalsSpan>> getOptFormals() const

This will also enforce that the original bool ellipsis is never used if hasFormals is false, which further ensures correctness.

And overall the less trivial getOptFormals now becomes, the more I think the private field are worth it.

Does this sound OK?

@github-actions github-actions bot added the with-tests Issues related to testing. PRs with tests have some priority label Oct 27, 2025
@Radvendii Radvendii force-pushed the exprlambda-alloc branch 2 times, most recently from 83cf0a9 to 3d61d0e Compare October 27, 2025 22:16
e.g. (foo@{}: 1) { a = 3; } should error, but wasn't with the previous
commit
@coderabbitai

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

1499-1499: Prefer nFormals (or unconditional size) to simplify and avoid extra branching

Use the compact field already on ExprLambda, or rely on getFormals().size() being 0 when absent.

- auto size = (!lambda.arg ? 0 : 1) + (lambda.hasFormals ? lambda.getFormals().size() : 0);
+ size_t size = (lambda.arg ? 1 : 0) + lambda.nFormals;

If you prefer the earlier suggestion: (!lambda.arg ? 0 : 1) + lambda.getFormals().size(); is fine too.

🧹 Nitpick comments (10)
src/libexpr/value-to-xml.cc (1)

148-156: Minor cleanup: cache fun and avoid repeated derefs/calls in this block.

Purely readability/微 perf. Example:

-        if (v.lambda().fun->hasFormals) {
+        auto fun = v.lambda().fun;
+        if (fun->hasFormals) {
             XMLAttrs attrs;
-            if (v.lambda().fun->arg)
-                attrs["name"] = state.symbols[v.lambda().fun->arg];
-            if (v.lambda().fun->ellipsis)
+            if (fun->arg)
+                attrs["name"] = state.symbols[fun->arg];
+            if (fun->ellipsis)
                 attrs["ellipsis"] = "1";
             XMLOpenElement _(doc, "attrspat", attrs);
-            for (auto & i : v.lambda().fun->getFormalsLexicographic(state.symbols))
+            for (auto & i : fun->getFormalsLexicographic(state.symbols))
                 doc.writeEmptyElement("attr", singletonAttrs("name", state.symbols[i.name]));
         } else
-            doc.writeEmptyElement("varpat", singletonAttrs("name", state.symbols[v.lambda().fun->arg]));
+            doc.writeEmptyElement("varpat", singletonAttrs("name", state.symbols[fun->arg]));
src/libexpr/primops.cc (1)

3366-3381: Cache fun/formals; confirm sort invariant held by getFormals().

  • Reads well and matches the new API. To reduce repeated calls/derefs:
-    if (!args[0]->lambda().fun->hasFormals) {
+    auto fun = args[0]->lambda().fun;
+    if (!fun->hasFormals) {
         v.mkAttrs(&Bindings::emptyBindings);
         return;
     }
-
-    const auto & formals = args[0]->lambda().fun->getFormals();
+    const auto formals = fun->getFormals();
  • The comment below assumes formals are sorted by (name, pos). Please verify getFormals() preserves that ordering post-refactor, or add a defensive assert in debug builds. Based on learnings.
src/libexpr/nixexpr.cc (2)

156-179: Minor: cache fun and reuse getFormalsLexicographic(...) result.

Keeps code compact and avoids repeated derefs:

-    if (hasFormals) {
+    auto fun = this; // inside ExprLambda, or `auto fun = this;` is implicit; if outside, `auto fun = v.lambda().fun;`
+    if (fun->hasFormals) {
         str << "{ ";
         bool first = true;
-        for (auto & i : getFormalsLexicographic(symbols)) {
+        const auto lexFormals = fun->getFormalsLexicographic(symbols);
+        for (auto & i : lexFormals) {
             …
         }
-        if (ellipsis) {
+        if (fun->ellipsis) {
             …
         }
         str << " }";
         if (arg)
             str << " @ ";
     }

454-471: Bind vars: compute env size and iterate using cached formals.

Avoids multiple getFormals() calls and repeated derefs:

-    auto newEnv = std::make_shared<StaticEnv>(nullptr, env, (hasFormals ? getFormals().size() : 0) + (!arg ? 0 : 1));
+    const bool haveFormals = hasFormals;
+    const auto formals = haveFormals ? getFormals() : decltype(getFormals()){};
+    auto newEnv = std::make_shared<StaticEnv>(nullptr, env, (haveFormals ? formals.size() : 0) + (arg ? 1 : 0));

     Displacement displ = 0;
     if (arg) newEnv->vars.emplace_back(arg, displ++);
-    if (hasFormals) {
-        for (auto & i : getFormals())
+    if (haveFormals) {
+        for (auto & i : formals)
             newEnv->vars.emplace_back(i.name, displ++);
         newEnv->sort();
-        for (auto & i : getFormals())
+        for (auto & i : formals)
             if (i.def) i.def->bindVars(es, newEnv);
     }
src/libflake/flake.cc (1)

284-291: Tiny readability tweak: cache fun/formals.

Current logic is correct. Optional cleanups:

-        if (outputs->value->isLambda() && outputs->value->lambda().fun->hasFormals) {
-            for (auto & formal : outputs->value->lambda().fun->getFormals()) {
+        if (outputs->value->isLambda()) {
+            auto fun = outputs->value->lambda().fun;
+            if (fun->hasFormals) {
+                const auto formals = fun->getFormals();
+                for (auto & formal : formals) {
                     if (formal.name != state.s.self)
                         flake.inputs.emplace(
                             state.symbols[formal.name],
                             FlakeInput{.ref = parseFlakeRef(state.fetchSettings, std::string(state.symbols[formal.name]))});
-            }
+                }
+            }
src/nix/nix-build/nix-build.cc (1)

419-426: Use hasFormal(Symbol) and avoid string compares/loop

Leverage the new API and Symbol equality for clarity and speed.

-            if (v.type() == nFunction && v.lambda().fun->hasFormals) {
-                for (auto & i : v.lambda().fun->getFormals()) {
-                    if (state->symbols[i.name] == "inNixShell") {
-                        add = true;
-                        break;
-                    }
-                }
-            }
+            if (v.type() == nFunction && v.lambda().fun->hasFormals) {
+                const auto sInNixShell = state->symbols.create("inNixShell");
+                if (v.lambda().fun->hasFormal(sInNixShell)) add = true;
+            }
src/libexpr/include/nix/expr/parser-state.hh (1)

218-237: Duplicate detection OK; consider adjacent_find for clarity

Current sort + index loop is correct. Optionally:

-    std::optional<std::pair<Symbol, PosIdx>> duplicate;
-    for (size_t i = 0; i + 1 < formals.formals.size(); i++) {
-        if (formals.formals[i].name != formals.formals[i + 1].name)
-            continue;
-        std::pair thisDup{formals.formals[i].name, formals.formals[i + 1].pos};
-        duplicate = std::min(thisDup, duplicate.value_or(thisDup));
-    }
+    std::optional<std::pair<Symbol, PosIdx>> duplicate;
+    if (auto it = std::adjacent_find(
+            formals.formals.begin(), formals.formals.end(),
+            [](const Formal &a, const Formal &b){ return a.name == b.name; });
+        it != formals.formals.end()) {
+        duplicate = std::pair{it->name, std::next(it)->pos};
+    }
src/libexpr/include/nix/expr/nixexpr.hh (3)

517-523: Minor: can avoid temporary span copy

hasFormal constructs a span via getFormals(). It’s fine, but once getFormals() returns a const span (see next comment), prefer const auto formals = getFormals(); to prevent accidental mutation in future edits.


528-536: getFormalsLexicographic copies then sorts — OK

This keeps the internal order untouched and matches prior behavior. Consider reserving capacity explicitly (minor):

-    std::vector<Formal> result(getFormals().begin(), getFormals().end());
+    std::vector<Formal> result;
+    auto fs = getFormals();
+    result.reserve(fs.size());
+    result.insert(result.end(), fs.begin(), fs.end());

543-548: Expose a const span to prevent accidental mutation

Returning std::span<Formal> from a const method allows callers to mutate internals inadvertently.

-    std::span<Formal> getFormals() const
+    std::span<const Formal> getFormals() const
     {
         assert(hasFormals);
         return {formalsStart, nFormals};
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd0d006 and 5db63f3.

📒 Files selected for processing (14)
  • src/libexpr-tests/primops.cc (1 hunks)
  • src/libexpr-tests/value/print.cc (2 hunks)
  • src/libexpr/eval.cc (4 hunks)
  • src/libexpr/include/nix/expr/nixexpr.hh (1 hunks)
  • src/libexpr/include/nix/expr/parser-state.hh (2 hunks)
  • src/libexpr/nixexpr.cc (3 hunks)
  • src/libexpr/parser.y (3 hunks)
  • src/libexpr/primops.cc (1 hunks)
  • src/libexpr/value-to-xml.cc (1 hunks)
  • src/libflake/flake.cc (1 hunks)
  • src/nix/flake.cc (1 hunks)
  • src/nix/nix-build/nix-build.cc (1 hunks)
  • tests/functional/lang/eval-fail-empty-formals.err.exp (1 hunks)
  • tests/functional/lang/eval-fail-empty-formals.nix (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/libexpr/include/nix/expr/parser-state.hh (1)
src/libexpr/include/nix/expr/nixexpr.hh (4)
  • arg (472-477)
  • arg (472-472)
  • arg (517-523)
  • arg (517-517)
src/libexpr/include/nix/expr/nixexpr.hh (2)
src/libexpr/include/nix/expr/parser-state.hh (2)
  • pos (97-97)
  • formals (96-96)
src/libexpr/include/nix/expr/symbol-table.hh (11)
  • Symbol (47-50)
  • Symbol (47-47)
  • Symbol (53-56)
  • Symbol (53-53)
  • symbols (288-295)
  • symbols (288-288)
  • a (198-202)
  • a (198-198)
  • a (204-207)
  • a (204-204)
  • a (210-210)
src/libexpr/nixexpr.cc (1)
src/libexpr/include/nix/expr/nixexpr.hh (12)
  • symbols (116-116)
  • symbols (147-147)
  • symbols (440-440)
  • symbols (528-536)
  • symbols (528-528)
  • symbols (582-582)
  • symbols (753-753)
  • symbols (753-753)
  • arg (472-477)
  • arg (472-472)
  • arg (517-523)
  • arg (517-517)
src/libexpr/eval.cc (1)
src/libexpr/include/nix/expr/eval.hh (22)
  • args (911-911)
  • fun (897-897)
  • fun (899-899)
  • fun (901-905)
  • fun (901-901)
  • fun (1056-1056)
  • v (652-652)
  • v (654-654)
  • v (660-660)
  • v (665-665)
  • v (666-666)
  • v (667-667)
  • v (669-669)
  • v (674-674)
  • v (678-678)
  • v (679-679)
  • v (680-685)
  • v (686-686)
  • v (705-705)
  • v (852-852)
  • v (928-928)
  • v (929-929)
⏰ 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: tests on macos
  • GitHub Check: tests on ubuntu
  • GitHub Check: tests on ubuntu (with sanitizers / coverage)
🔇 Additional comments (21)
tests/functional/lang/eval-fail-empty-formals.nix (1)

1-1: Test addition LGTM.

Covers empty-formals pattern with unexpected arg.

src/libexpr-tests/primops.cc (1)

770-775: LGTM.

Update to hasFormals data member aligns with API changes.

tests/functional/lang/eval-fail-empty-formals.err.exp (1)

1-12: Expected error output LGTM.

Matches the added test; two-stage context + specific argument error looks correct.

src/nix/flake.cc (1)

476-477: API update to hasFormals looks correct

Switching to the boolean member preserves the overlay check semantics. No issues spotted here.

src/libexpr/include/nix/expr/parser-state.hh (1)

96-97: In‑place validation API makes sense

Switching to void validateFormals(Formals & …) eliminates allocation/return churn. Callers must pass an lvalue; verify all call sites were adjusted.

src/libexpr-tests/value/print.cc (2)

115-116: Constructor callsite updated correctly

Passing state.mem.exprs.alloc and Formals{} aligns with the new allocator-aware ExprLambda API.


505-506: Constructor callsite updated correctly (ANSI variant)

Same as above; looks good.

src/libexpr/eval.cc (7)

1505-1506: LGTM

Branching on hasFormals before forcing attrs is correct and preserves previous behavior.


1523-1541: LGTM

Iterating via getFormals() and binding defaults vs actuals preserves semantics.


1546-1566: LGTM

Unexpected-arg check now uses hasFormal(...) and suggestions; matches prior logic with the new API.


1750-1753: LGTM

Early exit when not a lambda with formals maintains autocall semantics.


1757-1763: LGTM

Ellipsis handling (“pass-through” of all args) looks consistent with previous behavior.


1765-1781: LGTM

Filtering automatic arguments to only declared formals via getFormals() is correct.


1755-1756: Review comment is incorrect—no changes needed

The current code is already properly typed. Both operands to std::max are uint32_t:

  • fun.lambda().fun->nFormals is uint16_t (cast to uint32_t)
  • args.size() returns uint32_t (Bindings::size_type is defined as uint32_t)

No type mismatch exists, and adding an explicit template parameter or redundant cast is unnecessary.

src/libexpr/parser.y (7)

134-135: LGTM

Switching formals/formal_set to value semantics matches the allocator-backed refactor.


182-185: LGTM

Constructor now takes allocator and symbol directly; doc position preserved.


188-190: LGTM

Validating by reference before moving the Formals into ExprLambda is correct and avoids use-after-move.


197-198: LGTM

validateFormals with arg + pos before move is correct for the formal_set @ id case.


204-206: LGTM

Same as above for the id @ formal_set case; move semantics are used appropriately.


497-502: LGTM

formal_set productions now move the accumulated list and set ellipsis; defaults are clear.


506-509: LGTM

formals accumulation via std::move avoids copies; minimal actions and clear intent.

Comment on lines +494 to 506
ExprLambda(
std::pmr::polymorphic_allocator<char> & alloc, PosIdx pos, Symbol arg, const Formals & formals, Expr * body)
: pos(pos)
, arg(arg)
, formals(formals)
, body(body) {};
, ellipsis(formals.ellipsis)
, hasFormals(true)
, nFormals(formals.formals.size())
, formalsStart(alloc.allocate_object<Formal>(nFormals))
, body(body)
{
std::ranges::copy(formals.formals, formalsStart);
};

Copy link
Contributor

@coderabbitai coderabbitai bot Oct 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

UB: copying into uninitialized storage from allocate_object

alloc.allocate_object<Formal>(nFormals) returns raw storage. std::ranges::copy assigns into uninitialized memory → undefined behavior, even for trivial types.

Apply placement construction via uninitialized algorithms:

+#include <memory> // for std::uninitialized_copy-        , formalsStart(alloc.allocate_object<Formal>(nFormals))
+        , formalsStart(alloc.allocate_object<Formal>(nFormals))
         , body(body)
     {
-        std::ranges::copy(formals.formals, formalsStart);
+        std::uninitialized_copy(formals.formals.begin(), formals.formals.end(), formalsStart);
     };

Alternatively, loop with std::construct_at(formalsStart + i, formals.formals[i]).

📝 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
ExprLambda(
std::pmr::polymorphic_allocator<char> & alloc, PosIdx pos, Symbol arg, const Formals & formals, Expr * body)
: pos(pos)
, arg(arg)
, formals(formals)
, body(body) {};
, ellipsis(formals.ellipsis)
, hasFormals(true)
, nFormals(formals.formals.size())
, formalsStart(alloc.allocate_object<Formal>(nFormals))
, body(body)
{
std::ranges::copy(formals.formals, formalsStart);
};
ExprLambda(
std::pmr::polymorphic_allocator<char> & alloc, PosIdx pos, Symbol arg, const Formals & formals, Expr * body)
: pos(pos)
, arg(arg)
, ellipsis(formals.ellipsis)
, hasFormals(true)
, nFormals(formals.formals.size())
, formalsStart(alloc.allocate_object<Formal>(nFormals))
, body(body)
{
std::uninitialized_copy(formals.formals.begin(), formals.formals.end(), formalsStart);
};
🤖 Prompt for AI Agents
In src/libexpr/include/nix/expr/nixexpr.hh around lines 494 to 506, the
constructor uses alloc.allocate_object<Formal>(nFormals) to get raw storage and
then calls std::ranges::copy to copy objects into that uninitialized memory
(UB). Replace the raw copy with placement-construction: either use
std::uninitialized_copy_n(formals.formals.data(), nFormals, formalsStart) (or
allocator_traits::construct in a loop), or explicitly loop i from 0..nFormals-1
and call std::construct_at(formalsStart + i, formals.formals[i]) so each Formal
is properly constructed into the allocated storage.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not really an issue since that type will be implicit_lifetime when it becomes a widely supported thing in compilers.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

C++20 C++23 implicit-lifetime types standard support

💡 Result:

Short answer:

  • The language rule allowing certain operations to implicitly create/start lifetimes of "implicit-lifetime types" was introduced in C++20 (with later CWG defect corrections applied retroactively). [1][2]
  • Library/type-trait support (std::is_implicit_lifetime and the feature-test macro __cpp_lib_is_implicit_lifetime = 202302L) was added in C++23. [3][4]

Sources:
[1] cppreference — ImplicitLifetimeType (language notes, DR/CWG info).
[2] cppreference — notes on implicit object creation / C++20 behavior.
[3] cppreference — std::is_implicit_lifetime (shows C++23 / 202302L).
[4] Clang/GCC/MSVC status notes mentioning P2674R1 / builtin/trait support for implicit-lifetime types.


@xokdvium, you're absolutely right—implicit-lifetime types were introduced in C++20 (with retroactive defect corrections), and the standard library trait std::is_implicit_lifetime was added in C++23. If Formal qualifies as an implicit-lifetime type (which it likely does as a simple aggregate), then operations like std::ranges::copy can implicitly start object lifetimes in the allocated storage under C++20 semantics, making the UB concern moot.

I understand your position that this is acceptable for your codebase's C++ standard target and compiler support expectations. Thanks for the clarification!

   (\___/)
   (='.'=)  *notes for later*
   (")_(")

✏️ Learnings added
Learnt from: xokdvium
PR: NixOS/nix#14384
File: src/libexpr/include/nix/expr/nixexpr.hh:494-506
Timestamp: 2025-10-28T18:50:29.831Z
Learning: In the NixOS/nix codebase (C++ codebase), when reviewing code that copies simple aggregate types into allocated storage using operations like std::ranges::copy, understand that the maintainers accept relying on C++20 implicit-lifetime type semantics. They consider types like Formal (simple aggregates) to be implicit-lifetime types where copy operations implicitly start object lifetimes, so traditional placement-new or std::uninitialized_copy concerns don't apply. Don't flag this pattern as UB.

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

Comment on lines +507 to +513
ExprLambda(std::pmr::polymorphic_allocator<char> & alloc, PosIdx pos, Symbol arg, Expr * body)
: pos(pos)
, formals(formals)
, body(body)
, arg(arg)
, hasFormals(false)
, formalsStart(nullptr)
, body(body) {};

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Uninitialized members in no‑formals constructor

ellipsis and nFormals are not initialized when hasFormals == false. Any read (even guarded imperfectly elsewhere) risks UB.

Initialize all related fields:

-    ExprLambda(std::pmr::polymorphic_allocator<char> & alloc, PosIdx pos, Symbol arg, Expr * body)
-        : pos(pos)
-        , arg(arg)
-        , hasFormals(false)
-        , formalsStart(nullptr)
-        , body(body) {};
+    ExprLambda(std::pmr::polymorphic_allocator<char> & /*alloc*/, PosIdx pos, Symbol arg, Expr * body)
+        : pos(pos)
+        , arg(arg)
+        , ellipsis(false)
+        , hasFormals(false)
+        , nFormals(0)
+        , formalsStart(nullptr)
+        , body(body) {};

Optional: add a debug check in the other ctor to ensure formals.formals.size() <= std::numeric_limits<uint16_t>::max().


🏁 Script executed:

#!/bin/bash
# Find and examine the ExprLambda class definition
cat src/libexpr/include/nix/expr/nixexpr.hh | head -600 | tail -150

Length of output: 3822


🏁 Script executed:

#!/bin/bash
# Search for class ExprLambda definition to understand full structure
rg -n "class ExprLambda" -A 100 src/libexpr/include/nix/expr/nixexpr.hh

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for all member variables in ExprLambda
rg -n "ellipsis|nFormals|hasFormals" src/libexpr/include/nix/expr/nixexpr.hh | head -20

Length of output: 393


🏁 Script executed:

#!/bin/bash
# Search for unguarded accesses to ellipsis or nFormals in ExprLambda context
rg -n "\.ellipsis|\.nFormals" src/libexpr/include/nix/expr/nixexpr.hh -B 2 -A 2

Length of output: 164


🏁 Script executed:

#!/bin/bash
# Search for DocComment to see if it has default constructor
rg -n "struct DocComment|class DocComment" src/libexpr/include/nix/expr/ -A 10

Length of output: 696


🏁 Script executed:

#!/bin/bash
# Check if name field is used in ExprLambda and whether it needs initialization
rg -n "\.name\s*=" src/libexpr/include/nix/expr/nixexpr.hh | grep -i "ExprLambda\|Formal" -A 5 -B 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for setName method and name field initialization
rg -n "setName|name\s*=" src/libexpr/include/nix/expr/nixexpr.hh -B 2 -A 2 | head -40

Length of output: 822


🏁 Script executed:

#!/bin/bash
# Find PosIdx definition to see if it has default constructor
rg -n "struct PosIdx|class PosIdx" src/libexpr/include/nix/ -A 5

Length of output: 692


🏁 Script executed:

#!/bin/bash
# Check if name is used in any method of ExprLambda without initialization
rg -n "void setName" -A 15 src/libexpr/include/nix/expr/nixexpr.hh

Length of output: 1299


🏁 Script executed:

#!/bin/bash
# Search for actual reads of ellipsis field in evaluator/runtime code
rg -n "\.ellipsis" src/libexpr/ --type=cc --type=hh | grep -v "formals\\.ellipsis" | head -20

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check constructor 2 usage - verify alloc parameter is truly unused
cat src/libexpr/include/nix/expr/nixexpr.hh | sed -n '507,513p'

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Search for code that creates ExprLambda with no-formals constructor to see if name/docComment are set later
rg -n "ExprLambda.*alloc.*pos.*arg.*body" src/libexpr/ -B 2 -A 5

Length of output: 581


🏁 Script executed:

#!/bin/bash
# Check what Symbol default is (if any)
rg -n "class Symbol|struct Symbol" src/libexpr/include/nix/ -A 10 | head -40

Length of output: 2409


🏁 Script executed:

#!/bin/bash
# Verify correct rg syntax for cpp files
rg -n "\.ellipsis" src/libexpr/ | grep -v "formals\.ellipsis" | head -10

Length of output: 648


🏁 Script executed:

#!/bin/bash
# Final verification: check if name/docComment fields are set before use
rg -n "lambda\.name|lambda\.docComment" src/libexpr/eval.cc -B 3 -A 1 | head -30

Length of output: 1377


Initialize uninitialized members in no-formals ExprLambda constructor

The no-formals constructor (lines 507-513) leaves ellipsis and nFormals uninitialized. ellipsis is read unguarded in eval.cc:1545 (if (!lambda.ellipsis && ...)), creating undefined behavior risk. Initialize both fields to safe defaults:

-    ExprLambda(std::pmr::polymorphic_allocator<char> & alloc, PosIdx pos, Symbol arg, Expr * body)
-        : pos(pos)
-        , arg(arg)
-        , hasFormals(false)
-        , formalsStart(nullptr)
-        , body(body) {};
+    ExprLambda(std::pmr::polymorphic_allocator<char> & /*alloc*/, PosIdx pos, Symbol arg, Expr * body)
+        : pos(pos)
+        , arg(arg)
+        , ellipsis(false)
+        , hasFormals(false)
+        , nFormals(0)
+        , formalsStart(nullptr)
+        , body(body) {};

The alloc parameter is unused in this constructor variant and can be commented out.

🤖 Prompt for AI Agents
In src/libexpr/include/nix/expr/nixexpr.hh around lines 507 to 513, the
ExprLambda constructor that builds a lambda with no formals leaves members
ellipsis and nFormals uninitialized and also doesn't use the alloc parameter;
initialize ellipsis to false and nFormals to 0 in this constructor and either
remove or mark the alloc parameter as unused (e.g., comment it out or cast to
void) so there is no UB when code later reads lambda.ellipsis.

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

Labels

new-cli Relating to the "nix" command with-tests Issues related to testing. PRs with tests have some priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants