-
-
Couldn't load subscription status.
- Fork 1.8k
libexpr: store ExprLambda data in Expr::alloc #14384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
| uint16_t nFormals; | ||
| Formal * formalsStart; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| uint16_t nFormals; | |
| Formal * formalsStart; | |
| std::span<Formal> formals; |
Can we do that?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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::spanuses auint32_tfor the size (2 more bytes)- 6 bytes of padding
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() constThis 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?
83cf0a9 to
3d61d0e
Compare
3d61d0e to
80068f9
Compare
e.g. (foo@{}: 1) { a = 3; } should error, but wasn't with the previous
commit
80068f9 to
5db63f3
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this 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 branchingUse 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: cachefunand 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: Cachefun/formals; confirm sort invariant held bygetFormals().
- 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 verifygetFormals()preserves that ordering post-refactor, or add a defensiveassertin debug builds. Based on learnings.src/libexpr/nixexpr.cc (2)
156-179: Minor: cachefunand reusegetFormalsLexicographic(...)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: cachefun/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/loopLeverage 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 clarityCurrent 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
hasFormalconstructs a span viagetFormals(). It’s fine, but oncegetFormals()returns a const span (see next comment), preferconst auto formals = getFormals();to prevent accidental mutation in future edits.
528-536: getFormalsLexicographic copies then sorts — OKThis 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 mutationReturning
std::span<Formal>from aconstmethod 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
📒 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
hasFormalsdata 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 correctSwitching 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 senseSwitching 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 correctlyPassing
state.mem.exprs.allocandFormals{}aligns with the new allocator-awareExprLambdaAPI.
505-506: Constructor callsite updated correctly (ANSI variant)Same as above; looks good.
src/libexpr/eval.cc (7)
1505-1506: LGTMBranching on hasFormals before forcing attrs is correct and preserves previous behavior.
1523-1541: LGTMIterating via getFormals() and binding defaults vs actuals preserves semantics.
1546-1566: LGTMUnexpected-arg check now uses hasFormal(...) and suggestions; matches prior logic with the new API.
1750-1753: LGTMEarly exit when not a lambda with formals maintains autocall semantics.
1757-1763: LGTMEllipsis handling (“pass-through” of all args) looks consistent with previous behavior.
1765-1781: LGTMFiltering automatic arguments to only declared formals via getFormals() is correct.
1755-1756: Review comment is incorrect—no changes neededThe current code is already properly typed. Both operands to
std::maxareuint32_t:
fun.lambda().fun->nFormalsisuint16_t(cast touint32_t)args.size()returnsuint32_t(Bindings::size_type is defined asuint32_t)No type mismatch exists, and adding an explicit template parameter or redundant cast is unnecessary.
src/libexpr/parser.y (7)
134-135: LGTMSwitching formals/formal_set to value semantics matches the allocator-backed refactor.
182-185: LGTMConstructor now takes allocator and symbol directly; doc position preserved.
188-190: LGTMValidating by reference before moving the Formals into ExprLambda is correct and avoids use-after-move.
197-198: LGTMvalidateFormals with arg + pos before move is correct for the
formal_set @ idcase.
204-206: LGTMSame as above for the
id @ formal_setcase; move semantics are used appropriately.
497-502: LGTMformal_set productions now move the accumulated list and set ellipsis; defaults are clear.
506-509: LGTMformals accumulation via std::move avoids copies; minimal actions and clear intent.
| 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); | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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) {}; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -150Length 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.hhLength 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 -20Length 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 2Length 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 10Length 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 5Length 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 -40Length 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 5Length 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.hhLength 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 -20Length 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 5Length 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 -40Length of output: 2409
🏁 Script executed:
#!/bin/bash
# Verify correct rg syntax for cpp files
rg -n "\.ellipsis" src/libexpr/ | grep -v "formals\.ellipsis" | head -10Length 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 -30Length 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.
Motivation
For big-picture motivation, see the tracking issue
This PR moves the ExprLambda data into our allocator
{ foo, bar }:rather thanx:)Formals *formalsFormal *And everything else stays the same.
Alternative Design
Presumably we were using aOne effect of usingFormals *here rather thanFormalsdirectly so that we don't need to take up the extra space in the common case that there are no formals.Formals *rather thanFormalsdirectly has been that we don't need to take up the extra space to store aFormalsin 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.