Merge pull request #9582 from pennae/misc-opts

a packet of small optimizations

(cherry picked from commit ee439734e924eb337a869ff2e48aff8b989198bc)
Change-Id: I125d870710750a32a0dece48f39a3e9132b0d023
This commit is contained in:
eldritch horrors 2024-03-04 07:32:31 +01:00
parent 076844e386
commit dd180911d8
18 changed files with 144 additions and 72 deletions

View file

@ -12,9 +12,9 @@ namespace nix {
bool MY_TYPE ::operator COMPARATOR (const MY_TYPE & other) const \ bool MY_TYPE ::operator COMPARATOR (const MY_TYPE & other) const \
{ \ { \
const MY_TYPE* me = this; \ const MY_TYPE* me = this; \
auto fields1 = std::make_tuple<const CHILD_TYPE &, const FIELD_TYPE &>(*me->drvPath, me->FIELD); \ auto fields1 = std::tie(*me->drvPath, me->FIELD); \
me = &other; \ me = &other; \
auto fields2 = std::make_tuple<const CHILD_TYPE &, const FIELD_TYPE &>(*me->drvPath, me->FIELD); \ auto fields2 = std::tie(*me->drvPath, me->FIELD); \
return fields1 COMPARATOR fields2; \ return fields1 COMPARATOR fields2; \
} }
#define CMP(CHILD_TYPE, MY_TYPE, FIELD) \ #define CMP(CHILD_TYPE, MY_TYPE, FIELD) \

View file

@ -47,7 +47,7 @@ Value * InstallableFlake::getFlakeOutputs(EvalState & state, const flake::Locked
auto aOutputs = vFlake->attrs->get(state.symbols.create("outputs")); auto aOutputs = vFlake->attrs->get(state.symbols.create("outputs"));
assert(aOutputs); assert(aOutputs);
state.forceValue(*aOutputs->value, [&]() { return aOutputs->value->determinePos(noPos); }); state.forceValue(*aOutputs->value, aOutputs->value->determinePos(noPos));
return aOutputs->value; return aOutputs->value;
} }

View file

@ -885,7 +885,7 @@ void NixRepl::evalString(std::string s, Value & v)
{ {
Expr * e = parseString(s); Expr * e = parseString(s);
e->eval(*state, *env, v); e->eval(*state, *env, v);
state->forceValue(v, [&]() { return v.determinePos(noPos); }); state->forceValue(v, v.determinePos(noPos));
} }
@ -904,7 +904,7 @@ std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int m
str.flush(); str.flush();
checkInterrupt(); checkInterrupt();
state->forceValue(v, [&]() { return v.determinePos(noPos); }); state->forceValue(v, v.determinePos(noPos));
switch (v.type()) { switch (v.type()) {

View file

@ -83,13 +83,6 @@ Env & EvalState::allocEnv(size_t size)
[[gnu::always_inline]] [[gnu::always_inline]]
void EvalState::forceValue(Value & v, const PosIdx pos) void EvalState::forceValue(Value & v, const PosIdx pos)
{
forceValue(v, [&]() { return pos; });
}
template<typename Callable>
void EvalState::forceValue(Value & v, Callable getPos)
{ {
if (v.isThunk()) { if (v.isThunk()) {
Env * env = v.thunk.env; Env * env = v.thunk.env;
@ -100,15 +93,12 @@ void EvalState::forceValue(Value & v, Callable getPos)
expr->eval(*this, *env, v); expr->eval(*this, *env, v);
} catch (...) { } catch (...) {
v.mkThunk(env, expr); v.mkThunk(env, expr);
tryFixupBlackHolePos(v, pos);
throw; throw;
} }
} }
else if (v.isApp()) { else if (v.isApp())
PosIdx pos = getPos();
callFunction(*v.app.left, *v.app.right, v, pos); callFunction(*v.app.left, *v.app.right, v, pos);
}
else if (v.isBlackhole())
error("infinite recursion encountered").atPos(getPos()).template debugThrow<EvalError>();
} }

View file

@ -158,7 +158,17 @@ void Value::print(const SymbolTable &symbols, std::ostream &str,
break; break;
case tThunk: case tThunk:
case tApp: case tApp:
str << "<CODE>"; if (!isBlackhole()) {
str << "<CODE>";
} else {
// Although we know for sure that it's going to be an infinite recursion
// when this value is accessed _in the current context_, it's likely
// that the user will misinterpret a simpler «infinite recursion» output
// as a definitive statement about the value, while in fact it may be
// a valid value after `builtins.trace` and perhaps some other steps
// have completed.
str << "«potential infinite recursion»";
}
break; break;
case tLambda: case tLambda:
str << "<LAMBDA>"; str << "<LAMBDA>";
@ -175,15 +185,6 @@ void Value::print(const SymbolTable &symbols, std::ostream &str,
case tFloat: case tFloat:
str << fpoint; str << fpoint;
break; break;
case tBlackhole:
// Although we know for sure that it's going to be an infinite recursion
// when this value is accessed _in the current context_, it's likely
// that the user will misinterpret a simpler «infinite recursion» output
// as a definitive statement about the value, while in fact it may be
// a valid value after `builtins.trace` and perhaps some other steps
// have completed.
str << "«potential infinite recursion»";
break;
default: default:
printError("Nix evaluator internal error: Value::print(): invalid value type %1%", internalType); printError("Nix evaluator internal error: Value::print(): invalid value type %1%", internalType);
abort(); abort();
@ -251,9 +252,8 @@ std::string showType(const Value & v)
case tPrimOpApp: case tPrimOpApp:
return fmt("the partially applied built-in function '%s'", std::string(getPrimOp(v)->primOp->name)); return fmt("the partially applied built-in function '%s'", std::string(getPrimOp(v)->primOp->name));
case tExternal: return v.external->showType(); case tExternal: return v.external->showType();
case tThunk: return "a thunk"; case tThunk: return v.isBlackhole() ? "a black hole" : "a thunk";
case tApp: return "a function application"; case tApp: return "a function application";
case tBlackhole: return "a black hole";
default: default:
return std::string(showType(v.type())); return std::string(showType(v.type()));
} }
@ -1666,15 +1666,15 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
return; return;
} else { } else {
/* We have all the arguments, so call the primop. */ /* We have all the arguments, so call the primop. */
auto name = vCur.primOp->name; auto * fn = vCur.primOp;
nrPrimOpCalls++; nrPrimOpCalls++;
if (countCalls) primOpCalls[name]++; if (countCalls) primOpCalls[fn->name]++;
try { try {
vCur.primOp->fun(*this, vCur.determinePos(noPos), args, vCur); fn->fun(*this, vCur.determinePos(noPos), args, vCur);
} catch (Error & e) { } catch (Error & e) {
addErrorTrace(e, pos, "while calling the '%1%' builtin", name); addErrorTrace(e, pos, "while calling the '%1%' builtin", fn->name);
throw; throw;
} }
@ -1711,18 +1711,18 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
for (size_t i = 0; i < argsLeft; ++i) for (size_t i = 0; i < argsLeft; ++i)
vArgs[argsDone + i] = args[i]; vArgs[argsDone + i] = args[i];
auto name = primOp->primOp->name; auto fn = primOp->primOp;
nrPrimOpCalls++; nrPrimOpCalls++;
if (countCalls) primOpCalls[name]++; if (countCalls) primOpCalls[fn->name]++;
try { try {
// TODO: // TODO:
// 1. Unify this and above code. Heavily redundant. // 1. Unify this and above code. Heavily redundant.
// 2. Create a fake env (arg1, arg2, etc.) and a fake expr (arg1: arg2: etc: builtins.name arg1 arg2 etc) // 2. Create a fake env (arg1, arg2, etc.) and a fake expr (arg1: arg2: etc: builtins.name arg1 arg2 etc)
// so the debugger allows to inspect the wrong parameters passed to the builtin. // so the debugger allows to inspect the wrong parameters passed to the builtin.
primOp->primOp->fun(*this, vCur.determinePos(noPos), vArgs, vCur); fn->fun(*this, vCur.determinePos(noPos), vArgs, vCur);
} catch (Error & e) { } catch (Error & e) {
addErrorTrace(e, pos, "while calling the '%1%' builtin", name); addErrorTrace(e, pos, "while calling the '%1%' builtin", fn->name);
throw; throw;
} }
@ -2076,6 +2076,29 @@ void ExprPos::eval(EvalState & state, Env & env, Value & v)
} }
void ExprBlackHole::eval(EvalState & state, Env & env, Value & v)
{
state.error("infinite recursion encountered")
.debugThrow<InfiniteRecursionError>();
}
// always force this to be separate, otherwise forceValue may inline it and take
// a massive perf hit
[[gnu::noinline]]
void EvalState::tryFixupBlackHolePos(Value & v, PosIdx pos)
{
if (!v.isBlackhole())
return;
auto e = std::current_exception();
try {
std::rethrow_exception(e);
} catch (InfiniteRecursionError & e) {
e.err.errPos = positions[pos];
} catch (...) {
}
}
void EvalState::forceValueDeep(Value & v) void EvalState::forceValueDeep(Value & v)
{ {
std::set<const Value *> seen; std::set<const Value *> seen;
@ -2085,7 +2108,7 @@ void EvalState::forceValueDeep(Value & v)
recurse = [&](Value & v) { recurse = [&](Value & v) {
if (!seen.insert(&v).second) return; if (!seen.insert(&v).second) return;
forceValue(v, [&]() { return v.determinePos(noPos); }); forceValue(v, v.determinePos(noPos));
if (v.type() == nAttrs) { if (v.type() == nAttrs) {
for (auto & i : *v.attrs) for (auto & i : *v.attrs)

View file

@ -467,8 +467,7 @@ public:
*/ */
inline void forceValue(Value & v, const PosIdx pos); inline void forceValue(Value & v, const PosIdx pos);
template <typename Callable> void tryFixupBlackHolePos(Value & v, PosIdx pos);
inline void forceValue(Value & v, Callable getPos);
/** /**
* Force a value, then recursively force list elements and * Force a value, then recursively force list elements and

View file

@ -199,7 +199,7 @@ StringSet DrvInfo::queryMetaNames()
bool DrvInfo::checkMeta(Value & v) bool DrvInfo::checkMeta(Value & v)
{ {
state->forceValue(v, [&]() { return v.determinePos(noPos); }); state->forceValue(v, v.determinePos(noPos));
if (v.type() == nList) { if (v.type() == nList) {
for (auto elem : v.listItems()) for (auto elem : v.listItems())
if (!checkMeta(*elem)) return false; if (!checkMeta(*elem)) return false;
@ -305,7 +305,7 @@ static bool getDerivation(EvalState & state, Value & v,
bool ignoreAssertionFailures) bool ignoreAssertionFailures)
{ {
try { try {
state.forceValue(v, [&]() { return v.determinePos(noPos); }); state.forceValue(v, v.determinePos(noPos));
if (!state.isDerivation(v)) return true; if (!state.isDerivation(v)) return true;
/* Remove spurious duplicates (e.g., a set like `rec { x = /* Remove spurious duplicates (e.g., a set like `rec { x =

View file

@ -1,4 +1,5 @@
%option reentrant bison-bridge bison-locations %option reentrant bison-bridge bison-locations
%option align
%option noyywrap %option noyywrap
%option never-interactive %option never-interactive
%option stack %option stack
@ -35,9 +36,6 @@ static inline PosIdx makeCurPos(const YYLTYPE & loc, ParseData * data)
#define CUR_POS makeCurPos(*yylloc, data) #define CUR_POS makeCurPos(*yylloc, data)
// backup to recover from yyless(0)
thread_local YYLTYPE prev_yylloc;
static void initLoc(YYLTYPE * loc) static void initLoc(YYLTYPE * loc)
{ {
loc->first_line = loc->last_line = 1; loc->first_line = loc->last_line = 1;
@ -46,7 +44,7 @@ static void initLoc(YYLTYPE * loc)
static void adjustLoc(YYLTYPE * loc, const char * s, size_t len) static void adjustLoc(YYLTYPE * loc, const char * s, size_t len)
{ {
prev_yylloc = *loc; loc->stash();
loc->first_line = loc->last_line; loc->first_line = loc->last_line;
loc->first_column = loc->last_column; loc->first_column = loc->last_column;
@ -230,7 +228,7 @@ or { return OR_KW; }
{HPATH_START}\$\{ { {HPATH_START}\$\{ {
PUSH_STATE(PATH_START); PUSH_STATE(PATH_START);
yyless(0); yyless(0);
*yylloc = prev_yylloc; yylloc->unstash();
} }
<PATH_START>{PATH_SEG} { <PATH_START>{PATH_SEG} {
@ -286,7 +284,7 @@ or { return OR_KW; }
context (it may be ')', ';', or something of that sort) */ context (it may be ')', ';', or something of that sort) */
POP_STATE(); POP_STATE();
yyless(0); yyless(0);
*yylloc = prev_yylloc; yylloc->unstash();
return PATH_END; return PATH_END;
} }

View file

@ -9,6 +9,8 @@
namespace nix { namespace nix {
ExprBlackHole eBlackHole;
struct PosAdapter : AbstractPos struct PosAdapter : AbstractPos
{ {
Pos::Origin origin; Pos::Origin origin;

View file

@ -22,6 +22,13 @@ MakeError(UndefinedVarError, Error);
MakeError(MissingArgumentError, EvalError); MakeError(MissingArgumentError, EvalError);
MakeError(RestrictedPathError, Error); MakeError(RestrictedPathError, Error);
class InfiniteRecursionError : public EvalError
{
friend class EvalState;
public:
using EvalError::EvalError;
};
/** /**
* Position objects. * Position objects.
*/ */
@ -450,6 +457,16 @@ struct ExprPos : Expr
COMMON_METHODS COMMON_METHODS
}; };
/* only used to mark thunks as black holes. */
struct ExprBlackHole : Expr
{
void show(const SymbolTable & symbols, std::ostream & str) const override {}
void eval(EvalState & state, Env & env, Value & v) override;
void bindVars(EvalState & es, const std::shared_ptr<const StaticEnv> & env) override {}
};
extern ExprBlackHole eBlackHole;
/* Static environments are used to map variable names onto (level, /* Static environments are used to map variable names onto (level,
displacement) pairs used to obtain the value of the variable at displacement) pairs used to obtain the value of the variable at

View file

@ -27,6 +27,31 @@
namespace nix { namespace nix {
#define YYLTYPE ::nix::ParserLocation
struct ParserLocation
{
int first_line, first_column;
int last_line, last_column;
// backup to recover from yyless(0)
int stashed_first_line, stashed_first_column;
int stashed_last_line, stashed_last_column;
void stash() {
stashed_first_line = first_line;
stashed_first_column = first_column;
stashed_last_line = last_line;
stashed_last_column = last_column;
}
void unstash() {
first_line = stashed_first_line;
first_column = stashed_first_column;
last_line = stashed_last_line;
last_column = stashed_last_column;
}
};
struct ParseData struct ParseData
{ {
EvalState & state; EvalState & state;

View file

@ -31,7 +31,6 @@ typedef enum {
tThunk, tThunk,
tApp, tApp,
tLambda, tLambda,
tBlackhole,
tPrimOp, tPrimOp,
tPrimOpApp, tPrimOpApp,
tExternal, tExternal,
@ -61,6 +60,7 @@ class Bindings;
struct Env; struct Env;
struct Expr; struct Expr;
struct ExprLambda; struct ExprLambda;
struct ExprBlackHole;
struct PrimOp; struct PrimOp;
class Symbol; class Symbol;
class PosIdx; class PosIdx;
@ -151,7 +151,7 @@ public:
// type() == nThunk // type() == nThunk
inline bool isThunk() const { return internalType == tThunk; }; inline bool isThunk() const { return internalType == tThunk; };
inline bool isApp() const { return internalType == tApp; }; inline bool isApp() const { return internalType == tApp; };
inline bool isBlackhole() const { return internalType == tBlackhole; }; inline bool isBlackhole() const;
// type() == nFunction // type() == nFunction
inline bool isLambda() const { return internalType == tLambda; }; inline bool isLambda() const { return internalType == tLambda; };
@ -236,7 +236,7 @@ public:
case tLambda: case tPrimOp: case tPrimOpApp: return nFunction; case tLambda: case tPrimOp: case tPrimOpApp: return nFunction;
case tExternal: return nExternal; case tExternal: return nExternal;
case tFloat: return nFloat; case tFloat: return nFloat;
case tThunk: case tApp: case tBlackhole: return nThunk; case tThunk: case tApp: return nThunk;
} }
if (invalidIsThunk) if (invalidIsThunk)
return nThunk; return nThunk;
@ -343,11 +343,7 @@ public:
lambda.fun = f; lambda.fun = f;
} }
inline void mkBlackhole() inline void mkBlackhole();
{
internalType = tBlackhole;
// Value will be overridden anyways
}
void mkPrimOp(PrimOp * p); void mkPrimOp(PrimOp * p);
@ -443,6 +439,20 @@ public:
}; };
extern ExprBlackHole eBlackHole;
bool Value::isBlackhole() const
{
return internalType == tThunk && thunk.expr == (Expr*) &eBlackHole;
}
void Value::mkBlackhole()
{
internalType = tThunk;
thunk.expr = (Expr*) &eBlackHole;
}
#if HAVE_BOEHMGC #if HAVE_BOEHMGC
typedef std::vector<Value *, traceable_allocator<Value *>> ValueVector; typedef std::vector<Value *, traceable_allocator<Value *>> ValueVector;
typedef std::map<Symbol, Value *, std::less<Symbol>, traceable_allocator<std::pair<const Symbol, Value *>>> ValueMap; typedef std::map<Symbol, Value *, std::less<Symbol>, traceable_allocator<std::pair<const Symbol, Value *>>> ValueMap;

View file

@ -11,9 +11,9 @@ namespace nix {
bool MY_TYPE ::operator COMPARATOR (const MY_TYPE & other) const \ bool MY_TYPE ::operator COMPARATOR (const MY_TYPE & other) const \
{ \ { \
const MY_TYPE* me = this; \ const MY_TYPE* me = this; \
auto fields1 = std::make_tuple<const CHILD_TYPE &, const FIELD_TYPE &>(*me->drvPath, me->FIELD); \ auto fields1 = std::tie(*me->drvPath, me->FIELD); \
me = &other; \ me = &other; \
auto fields2 = std::make_tuple<const CHILD_TYPE &, const FIELD_TYPE &>(*me->drvPath, me->FIELD); \ auto fields2 = std::tie(*me->drvPath, me->FIELD); \
return fields1 COMPARATOR fields2; \ return fields1 COMPARATOR fields2; \
} }
#define CMP(CHILD_TYPE, MY_TYPE, FIELD) \ #define CMP(CHILD_TYPE, MY_TYPE, FIELD) \
@ -21,13 +21,9 @@ namespace nix {
CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, !=) \ CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, !=) \
CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, <) CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, <)
#define FIELD_TYPE std::string
CMP(SingleDerivedPath, SingleDerivedPathBuilt, output) CMP(SingleDerivedPath, SingleDerivedPathBuilt, output)
#undef FIELD_TYPE
#define FIELD_TYPE OutputsSpec
CMP(SingleDerivedPath, DerivedPathBuilt, outputs) CMP(SingleDerivedPath, DerivedPathBuilt, outputs)
#undef FIELD_TYPE
#undef CMP #undef CMP
#undef CMP_ONE #undef CMP_ONE

View file

@ -15,6 +15,8 @@
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <memory>
#include <new>
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/select.h> #include <sys/select.h>
@ -1286,7 +1288,11 @@ StorePath LocalStore::addToStoreFromDump(Source & source0, std::string_view name
path. */ path. */
bool inMemory = false; bool inMemory = false;
std::string dump; struct Free {
void operator()(void* v) { free(v); }
};
std::unique_ptr<char, Free> dumpBuffer(nullptr);
std::string_view dump;
/* Fill out buffer, and decide whether we are working strictly in /* Fill out buffer, and decide whether we are working strictly in
memory based on whether we break out because the buffer is full memory based on whether we break out because the buffer is full
@ -1295,13 +1301,18 @@ StorePath LocalStore::addToStoreFromDump(Source & source0, std::string_view name
auto oldSize = dump.size(); auto oldSize = dump.size();
constexpr size_t chunkSize = 65536; constexpr size_t chunkSize = 65536;
auto want = std::min(chunkSize, settings.narBufferSize - oldSize); auto want = std::min(chunkSize, settings.narBufferSize - oldSize);
dump.resize(oldSize + want); if (auto tmp = realloc(dumpBuffer.get(), oldSize + want)) {
dumpBuffer.release();
dumpBuffer.reset((char*) tmp);
} else {
throw std::bad_alloc();
}
auto got = 0; auto got = 0;
Finally cleanup([&]() { Finally cleanup([&]() {
dump.resize(oldSize + got); dump = {dumpBuffer.get(), dump.size() + got};
}); });
try { try {
got = source.read(dump.data() + oldSize, want); got = source.read(dumpBuffer.get() + oldSize, want);
} catch (EndOfFile &) { } catch (EndOfFile &) {
inMemory = true; inMemory = true;
break; break;
@ -1327,7 +1338,8 @@ StorePath LocalStore::addToStoreFromDump(Source & source0, std::string_view name
else else
writeFile(tempPath, bothSource); writeFile(tempPath, bothSource);
dump.clear(); dumpBuffer.reset();
dump = {};
} }
auto [hash, size] = hashSink->finish(); auto [hash, size] = hashSink->finish();

View file

@ -13,9 +13,9 @@
#define GENERATE_ONE_CMP(PRE, QUAL, COMPARATOR, MY_TYPE, ...) \ #define GENERATE_ONE_CMP(PRE, QUAL, COMPARATOR, MY_TYPE, ...) \
PRE bool QUAL operator COMPARATOR(const MY_TYPE & other) const { \ PRE bool QUAL operator COMPARATOR(const MY_TYPE & other) const { \
__VA_OPT__(const MY_TYPE * me = this;) \ __VA_OPT__(const MY_TYPE * me = this;) \
auto fields1 = std::make_tuple( __VA_ARGS__ ); \ auto fields1 = std::tie( __VA_ARGS__ ); \
__VA_OPT__(me = &other;) \ __VA_OPT__(me = &other;) \
auto fields2 = std::make_tuple( __VA_ARGS__ ); \ auto fields2 = std::tie( __VA_ARGS__ ); \
return fields1 COMPARATOR fields2; \ return fields1 COMPARATOR fields2; \
} }
#define GENERATE_EQUAL(prefix, qualification, my_type, args...) \ #define GENERATE_EQUAL(prefix, qualification, my_type, args...) \

View file

@ -346,7 +346,7 @@ static void main_nix_build(int argc, char * * argv)
takesNixShellAttr(vRoot) ? *autoArgsWithInNixShell : *autoArgs, takesNixShellAttr(vRoot) ? *autoArgsWithInNixShell : *autoArgs,
vRoot vRoot
).first); ).first);
state->forceValue(v, [&]() { return v.determinePos(noPos); }); state->forceValue(v, v.determinePos(noPos));
getDerivations( getDerivations(
*state, *state,
v, v,

View file

@ -129,7 +129,7 @@ bool createUserEnv(EvalState & state, DrvInfos & elems,
/* Evaluate it. */ /* Evaluate it. */
debug("evaluating user environment builder"); debug("evaluating user environment builder");
state.forceValue(topLevel, [&]() { return topLevel.determinePos(noPos); }); state.forceValue(topLevel, topLevel.determinePos(noPos));
NixStringContext context; NixStringContext context;
Attr & aDrvPath(*topLevel.attrs->find(state.sDrvPath)); Attr & aDrvPath(*topLevel.attrs->find(state.sDrvPath));
auto topLevelDrv = state.coerceToStorePath(aDrvPath.pos, *aDrvPath.value, context, ""); auto topLevelDrv = state.coerceToStorePath(aDrvPath.pos, *aDrvPath.value, context, "");

View file

@ -41,7 +41,7 @@ void processExpr(EvalState & state, const Strings & attrPaths,
for (auto & i : attrPaths) { for (auto & i : attrPaths) {
Value & v(*findAlongAttrPath(state, i, autoArgs, vRoot).first); Value & v(*findAlongAttrPath(state, i, autoArgs, vRoot).first);
state.forceValue(v, [&]() { return v.determinePos(noPos); }); state.forceValue(v, v.determinePos(noPos));
NixStringContext context; NixStringContext context;
if (evalOnly) { if (evalOnly) {