Files
lix/lix/libexpr/eval.cc
T
skye c40afdea73 libexpr/eval-expr: extract makeThunk function that constructs and increments counters
Motivated by needing to remove a Value default construction from
`ExprVar::maybeThunk`, so part of #744

Change-Id: Ia12b5b019e49055ebd7ffa0cc39a59c06a6a6964
2026-04-22 08:37:17 -04:00

2284 lines
72 KiB
C++

#include "lix/libexpr/eval.hh"
#include "lix/libexpr/eval-settings.hh"
#include "lix/libstore/path.hh"
#include "lix/libutil/archive.hh"
#include "lix/libutil/ansicolor.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/current-process.hh"
#include "lix/libutil/deprecated-features.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/english.hh"
#include "lix/libutil/fmt.hh"
#include "lix/libexpr/primops.hh"
#include "lix/libexpr/print-options.hh"
#include "lix/libmain/shared.hh"
#include "lix/libutil/logging.hh"
#include "lix/libutil/suggestions.hh"
#include "lix/libutil/types.hh"
#include "lix/libstore/store-api.hh"
#include "lix/libstore/derivations.hh"
#include "lix/libexpr/gc-alloc.hh"
#include "lix/libstore/filetransfer.hh"
#include "lix/libexpr/function-trace.hh"
#include "lix/libstore/profiles.hh"
#include "lix/libexpr/print.hh"
#include "lix/libexpr/gc-small-vector.hh"
#include "lix/libfetchers/fetch-to-store.hh"
#include "lix/libexpr/flake/flakeref.hh"
#include "lix/libutil/exit.hh"
#include "lix/libutil/json.hh"
#include "symbol-table.hh"
#include "value.hh"
#include <algorithm>
#include <iostream>
#include <ostream>
#include <sstream>
#include <cstring>
#include <optional>
#include <string>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <fstream>
#include <functional>
#include <ranges>
#include <sys/resource.h>
#include <boost/container/small_vector.hpp>
// Ignore all internal signals boehm uses for parallel marking
// FIXME: Find out how to do this with LLDB for macOS!
#ifndef __APPLE__
[[gnu::section(".debug_gdb_scripts"), gnu::used, gnu::aligned(1)]]
// TODO: We should use `SECTION_SCRIPT_ID_PYTHON_TEXT` from
// `<gdb/section-scripts.h>` instead of hardcoding 4.
// But why isn't this header exported by GDB?
static const char printer_script[] =
"\4"
R"(lix-ignore-boehm-signals
import gdb
gdb.execute("handle SIGPWR SIGXCPU ignore")
)";
#endif
#if HAVE_BOEHMGC
#define GC_INCLUDE_NEW
#include <gc/gc.h>
#include <gc/gc_cpp.h>
#endif
namespace nix {
RootValue allocRootValue(Value v)
{
return std::allocate_shared<Value>(TraceableAllocator<Value>(), v);
}
// Pretty print types for assertion errors
std::ostream & operator << (std::ostream & os, const ValueType t) {
os << showType(t);
return os;
}
std::string printValue(EvalState & state, Value & v)
{
std::ostringstream out;
v.print(state, out);
return out.str();
}
std::string_view showType(ValueType type, bool withArticle)
{
#define WA(a, w) withArticle ? a " " w : w
switch (type) {
case nInt: return WA("an", "integer");
case nBool: return WA("a", "Boolean");
case nString: return WA("a", "string");
case nPath: return WA("a", "path");
case nNull: return "null";
case nAttrs: return WA("a", "set");
case nList: return WA("a", "list");
case nFunction: return WA("a", "function");
case nExternal: return WA("an", "external value");
case nFloat: return WA("a", "float");
case nThunk: return WA("a", "thunk");
}
abort();
}
std::string showType(const Value & v)
{
// Allow selecting a subset of enum values
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
switch (v.internalType()) {
case tString:
if (v.string().isPath()) {
return "a path";
} else {
return v.string().context ? "a string with context" : "a string";
}
case tAuxiliary:
#pragma GCC diagnostic push
#pragma GCC diagnostic error "-Wswitch-enum"
switch (v.auxiliary()->type()) {
case Value::Acb::tExternal:
return v.external()->showType();
case Value::Acb::tFloat:
case Value::Acb::tNull:
case Value::Acb::tLambda:
case Value::Acb::tInt:
return std::string(showType(v.type()));
case Value::Acb::tPrimOp:
return fmt("the built-in function '%s'", v.primOp()->name);
}
#pragma GCC diagnostic pop
case tThunk:
return v.isBlackhole() ? "a black hole" : "a thunk";
case tApp:
if (v.isPrimOpApp()) {
return fmt(
"the partially applied built-in function '%s'", v.app().target().primOp()->name
);
} else {
return "a function application";
}
default:
return std::string(showType(v.type()));
}
#pragma GCC diagnostic pop
}
#if HAVE_BOEHMGC
/* Called when the Boehm GC runs out of memory. */
static void * oomHandler(size_t requested)
{
/* Convert this to a proper C++ exception. */
throw std::bad_alloc();
}
#endif
Symbol getName(const AttrName & name, EvalState & state, Env & env)
{
if (name.symbol) {
return name.symbol;
} else {
Value nameValue = name.expr->eval(state, env);
state.forceStringNoCtx(nameValue, name.expr->getPos(), "while evaluating an attribute name");
return state.ctx.symbols.create(nameValue.str());
}
}
static bool libexprInitialised = false;
void initLibExpr()
{
if (libexprInitialised) return;
#if HAVE_BOEHMGC
/* Initialise the Boehm garbage collector. */
/* Don't look for interior pointers. This reduces the odds of
misdetection a bit. */
GC_set_all_interior_pointers(0);
for (int i = 1; i < 8; i++) {
GC_REGISTER_DISPLACEMENT(i);
}
/* We don't have any roots in data segments, so don't scan from
there. */
GC_set_no_dls(1);
GC_INIT();
// Enable parallel marking
GC_start_mark_threads();
GC_set_oom_fn(oomHandler);
/* Set the initial heap size to something fairly big (25% of
physical RAM, up to a maximum of 384 MiB) so that in most cases
we don't need to garbage collect at all. (Collection has a
fairly significant overhead.) The heap size can be overridden
through libgc's GC_INITIAL_HEAP_SIZE environment variable. We
should probably also provide a nix.conf setting for this. Note
that GC_expand_hp() causes a lot of virtual, but not physical
(resident) memory to be allocated. This might be a problem on
systems that don't overcommit. */
if (!getEnv("GC_INITIAL_HEAP_SIZE")) {
int64_t size = 32l * 1024 * 1024;
#if HAVE_SYSCONF && defined(_SC_PAGESIZE) && defined(_SC_PHYS_PAGES)
int64_t maxSize = 384l * 1024 * 1024;
int64_t pageSize = sysconf(_SC_PAGESIZE);
int64_t pages = sysconf(_SC_PHYS_PAGES);
if (pageSize != -1) {
size = (pageSize * pages) / 4; // 25% of RAM
}
if (size > maxSize) size = maxSize;
#endif
debug("setting initial heap size to %1% bytes", size);
GC_expand_hp(size);
}
#endif
fetchers::initLibFetchers();
libexprInitialised = true;
}
EvalMemory::EvalMemory()
{
assert(libexprInitialised);
#if HAVE_BOEHMGC
GC_add_roots(static_cast<void *>(gcCache), static_cast<void *>(gcCache + CACHES));
#endif
}
EvalMemory::~EvalMemory()
{
#if HAVE_BOEHMGC
GC_remove_roots(static_cast<void *>(gcCache), static_cast<void *>(gcCache + CACHES));
#endif
}
EvalBuiltins::EvalBuiltins(
EvalMemory & mem,
SymbolTable & symbols,
const SearchPath & searchPath,
const Path & storeDir,
size_t size
)
: mem(mem)
, symbols(symbols)
, env(mem.allocEnv(size))
, staticEnv{std::make_shared<StaticEnv>(nullptr, nullptr)}
{
createBaseEnv(searchPath, storeDir);
}
EvalPaths::EvalPaths(
AsyncIoRoot & aio,
const ref<Store> & store,
SearchPath searchPath,
EvalErrorContext & errors
)
: store(store)
, searchPath_(std::move(searchPath))
, errors(errors)
{
if (evalSettings.restrictEval || evalSettings.pureEval) {
allowedPaths = AllowedPath{.allowAllChildren = false};
for (auto & i : searchPath_.elements) {
auto r = aio.blockOn(resolveSearchPathPath(i.path));
if (!r) continue;
auto path = std::move(*r);
if (store->isInStore(path)) {
try {
StorePathSet closure;
aio.blockOn(store->computeFSClosure(store->toStorePath(path).first, closure));
for (auto & path : closure)
allowPath(path);
} catch (InvalidPath &) {
allowPath(path);
}
} else
allowPath(path);
}
}
#if LIX_MAJOR >= 3 || (LIX_MAJOR == 2 && LIX_MINOR >= 98)
#warning \
"The feature nix-path-shadow was deprecated in 2.95 with a warning, error in 2.96, and we should consider removing the bypass in 2.98"
#endif
if (!featureSettings.isEnabled(DeprecatedFeature::NixPathShadow)) {
for (auto & [prefix, path] : searchPath_.elements) {
// Match on the 'nix' prefix
if (prefix.s == "nix") {
throw EvalError(HintFmt(
"The prefix '%s' is reserved for internal use by Lix in the Nix search "
"path, its usage is deprecated and will be forbidden in the future.\n"
"Use %s to silence this error.\n"
"This is due to adding '%s=%s' in the Nix search path, either through the "
"environment variable '%s' or by passing the flag %s to the nix "
"invocation.",
"nix",
"--extra-deprecated-features nix-path-shadow",
prefix.s,
path.s,
"NIX_PATH",
"-I"
));
} else
// Match prefixless paths that contain a `nix` directory
if (auto res =
prefix.suffixIfPotentialMatch("nix").and_then([&](std::string_view s) {
return aio.blockOn(resolveSearchPathPath(path))
.and_then([&](std::string r) {
Path res = s.length() ? concatStrings(r, "/", s): r;
return pathExists(res) ? std::optional(res) : std::nullopt;
});
}))
{
throw EvalError(HintFmt(
"Shadowing '%s' by configuring the nix-path is deprecated and "
"will be forbidden in the future.\n"
"Use %s to silence this error.\n"
"This is due to adding '%s' to the nix-path without a prefix, "
"either by passing the flag '-I %s' to the nix invocation or by "
"adding this path to the environment variable '%s'.",
"<nix/...>",
"--extra-deprecated-features nix-path-shadow",
path.s,
path.s,
"NIX_PATH"
));
}
}
}
}
Evaluator::Evaluator(
AsyncIoRoot & aio,
const SearchPath & _searchPath,
ref<Store> store,
std::shared_ptr<Store> buildStore,
std::function<ReplExitStatus(EvalState & es, ValMap const & extraEnv)> debugRepl
)
: paths(aio, store, [&] {
SearchPath searchPath;
if (!evalSettings.pureEval) {
for (auto & i : _searchPath.elements)
searchPath.elements.emplace_back(SearchPath::Elem {i});
for (auto & i : evalSettings.nixPath.get())
searchPath.elements.emplace_back(SearchPath::Elem::parse(i));
}
return searchPath;
}(), errors)
, builtins(mem, symbols, paths.searchPath(), store->config().storeDir)
, repair(NoRepair)
, store(store)
, buildStore(buildStore ? ref<Store>::unsafeFromPtr(buildStore) : store)
, debug{
debugRepl ? std::make_unique<DebugState>(
positions,
symbols,
[this, debugRepl](const ValMap & extraEnv, NeverAsync) {
return activeEval
? debugRepl(*activeEval, extraEnv)
: ReplExitStatus::Continue;
})
: nullptr
}
, errors{positions, debug.get()}
{
stats.countCalls = getEnv("NIX_COUNT_CALLS").value_or("0") != "0";
static_assert(sizeof(Env) <= 16, "environment must be <= 16 bytes");
}
box_ptr<EvalState> Evaluator::begin(AsyncIoRoot & aio)
{
assert(!activeEval);
// Increase the default stack size for the evaluator and for
// libstdc++'s std::regex.
ensureStackSizeAtLeast(64ul * 1024 * 1024);
return box_ptr<EvalState>::unsafeFromNonnull(
std::unique_ptr<EvalState>(new EvalState(aio, *this))
);
}
EvalState::EvalState(AsyncIoRoot & aio, Evaluator & ctx) : ctx(ctx), aio(aio)
{
ctx.activeEval = this;
}
EvalState::~EvalState()
{
ctx.activeEval = nullptr;
}
void EvalPaths::allowPath(const Path & path)
{
if (!allowedPaths) {
return;
}
CanonPath p(path);
auto * level = &*allowedPaths;
for (const auto & entry : p) {
level = &level->children.emplace(std::piecewise_construct, std::tuple(entry), std::tuple())
.first->second;
}
level->allowAllChildren = true;
}
void EvalPaths::allowPath(const StorePath & storePath)
{
if (allowedPaths)
allowPath(store->toRealPath(storePath));
}
Value EvalPaths::allowAndSetStorePathString(const StorePath & storePath)
{
allowPath(storePath);
return mkStorePathString(storePath);
}
CheckedSourcePath EvalPaths::checkSourcePath(const SourcePath & path_)
{
if (!allowedPaths) return auto(path_).unsafeIntoChecked();
auto i = resolvedPaths.find(path_.canonical().abs());
if (i != resolvedPaths.end())
return i->second;
/* First canonicalize the path without symlinks, so we make sure an
* attacker can't append ../../... to a path that would be in allowedPaths
* and thus leak symlink targets.
*/
const CanonPath abspath{path_.canonical().abs()};
if (abspath.abs().starts_with(corepkgsPrefix)) {
return SourcePath(std::move(abspath)).unsafeIntoChecked();
}
/* Resolve symlinks. This is mostly restricted copy of canonPath with
resolveSymlinks=true, because we need access to intermediat paths. */
debug("checking access to '%s'", abspath);
/* Count the number of times we follow a symlink and stop at some
arbitrary (but high) limit to prevent infinite loops. */
unsigned int followCount = 0, maxFollow = 1024;
std::optional<CanonPath> componentsBacking;
std::vector<std::string_view> components(abspath.begin(), abspath.end());
retry:
if (++followCount >= maxFollow) {
throw Error("infinite symlink recursion in path '%1%'", path_);
}
// TODO: tests for this stuff
const auto * level = &*allowedPaths;
CheckedSourcePath current = SourcePath(CanonPath::root).unsafeIntoChecked();
for (auto ct = components.begin(); ct != components.end(); ct++) {
auto & p = *ct;
// an empty level means all subpaths are allowed, propagate this forwards
// by setting level=nullptr for the subsequent checks. a symlink will set
// level to the "VFS" root and restart the check with a the resolved path
if (level) {
if (level->allowAllChildren) {
level = nullptr;
} else if (auto it = level->children.find(p); it != level->children.end()) {
level = &it->second;
} else {
goto failed;
}
}
auto next = (current + p).unsafeIntoChecked();
auto st = next.maybeLstat();
// resolve symlinks, treating nonexistant components like regular directories.
// this mirrors canonPath behavior and is necessary for `builtins.pathExists`.
if (st && st->type == InputAccessor::tSymlink) {
auto target = next.readLink();
auto levelResolved = target.starts_with("/")
? CanonPath(target)
: CanonPath(current.canonical().abs() + "/" + target);
for (ct++; ct != components.end(); ct++) {
levelResolved.push(*ct);
}
components = {levelResolved.begin(), levelResolved.end()};
componentsBacking = std::move(levelResolved);
followCount += 1;
goto retry;
}
current = std::move(next);
}
// Downstream users (e.g. `builtins.readDir` or `builtins.path`) will want to descend.
if (level && !level->allowAllChildren) {
goto failed;
}
resolvedPaths.insert_or_assign(path_.canonical().abs(), current);
return current;
failed:
auto modeInformation = evalSettings.pureEval
? "in pure eval mode (use '--impure' to override)"
: "in restricted mode";
throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", abspath, modeInformation);
}
void EvalPaths::checkURI(const std::string & uri)
{
if (!evalSettings.restrictEval) return;
/* 'uri' should be equal to a prefix, or in a subdirectory of a
prefix. Thus, the prefix https://github.co does not permit
access to https://github.com. Note: this allows 'http://' and
'https://' as prefixes for any http/https URI. */
for (auto & prefix : evalSettings.allowedUris.get())
if (uri == prefix ||
(uri.size() > prefix.size()
&& prefix.size() > 0
&& uri.starts_with(prefix)
&& (prefix[prefix.size() - 1] == '/' || uri[prefix.size()] == '/')))
return;
/* If the URI is a path, then check it against allowedPaths as
well. */
if (uri.starts_with("/")) {
checkSourcePath(CanonPath(uri));
return;
}
if (uri.starts_with("file://")) {
checkSourcePath(CanonPath(std::string(uri, 7)));
return;
}
throw RestrictedPathError("access to URI '%s' is forbidden in restricted mode", uri);
}
Path EvalPaths::toRealPath(const Path & path, const NixStringContext & context)
{
// FIXME: check whether 'path' is in 'context'.
return
!context.empty() && store->isInStore(path)
? store->toRealPath(path)
: path;
}
void EvalBuiltins::addConstant(const std::string & name, const Value & v, Constant info)
{
auto name2 = name.substr(0, 2) == "__" ? name.substr(2) : name;
constantInfos.push_back({name2, info});
if (!(evalSettings.pureEval && info.impureOnly)) {
/* Check the type, if possible.
We might know the type of a thunk in advance, so be allowed
to just write it down in that case. */
if (auto gotType = v.type(true); gotType != nThunk) {
assert(info.type == gotType);
}
/* Install value the base environment. */
staticEnv->vars.insert_or_assign(symbols.create(name), baseEnvDispl);
env.values[baseEnvDispl++] = v;
env.values[0].attrs()->push_back(Attr(symbols.create(name2), v));
}
}
std::ostream & operator<<(std::ostream & output, const PrimOp & primOp)
{
output << "primop " << primOp.name;
return output;
}
void EvalBuiltins::addPrimOp(PrimOpDetails primOp)
{
/* Hack to make constants lazy: turn them into a application of
the primop to a dummy value. */
if (primOp.arity == 0) {
primOp.arity = 1;
Value vPrimOp{NewValueAs::primop, *new PrimOp(primOp)};
Value v{NewValueAs::app, mem, vPrimOp, vPrimOp};
addConstant(
vPrimOp.primOp()->name,
v,
{
.type = nFunction,
.doc = vPrimOp.primOp()->doc,
}
);
}
auto envName = symbols.create(primOp.name);
if (primOp.name.starts_with("__"))
primOp.name = primOp.name.substr(2);
Value v{NewValueAs::primop, *new PrimOp(std::move(primOp))};
staticEnv->vars.insert_or_assign(auto(envName), baseEnvDispl);
env.values[baseEnvDispl++] = v;
env.values[0].attrs()->push_back(Attr(symbols.create(v.primOp()->name), v));
}
Value & EvalBuiltins::get(const std::string & name)
{
return env.values[0].attrs()->get(symbols.create(name))->value;
}
std::optional<EvalBuiltins::Doc> EvalBuiltins::getDoc(Value & v)
{
if (v.isPrimOp()) {
auto v2 = &v;
if (auto * doc = v2->primOp()->doc)
return Doc {
.pos = {},
.name = v2->primOp()->name,
.arity = v2->primOp()->arity,
.args = v2->primOp()->args,
.doc = doc,
};
}
return {};
}
static std::set<std::string_view> sortedBindingNames(const SymbolTable & st, const StaticEnv & se)
{
std::set<std::string_view> bindings;
for (auto [symbol, displ] : se.vars)
bindings.emplace(st[symbol]);
return bindings;
}
// just for the current level of StaticEnv, not the whole chain.
void printStaticEnvBindings(const SymbolTable & st, const StaticEnv & se)
{
std::cout << ANSI_MAGENTA;
for (auto & i : sortedBindingNames(st, se))
std::cout << i << " ";
std::cout << ANSI_NORMAL;
std::cout << std::endl;
}
// just for the current level of Env, not the whole chain.
void printWithBindings(const SymbolTable & st, const Env & env)
{
if (env.values[0].type() == nAttrs) {
std::set<std::string_view> bindings;
for (const auto & attr : *env.values[0].attrs()) {
bindings.emplace(st[attr.name]);
}
std::cout << "with: ";
std::cout << ANSI_MAGENTA;
for (auto & i : bindings)
std::cout << i << " ";
std::cout << ANSI_NORMAL;
std::cout << std::endl;
}
}
void printEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & env, int lvl)
{
std::cout << "Env level " << lvl << std::endl;
if (se.up && env.up) {
std::cout << "static: ";
printStaticEnvBindings(st, se);
if (se.isWith)
printWithBindings(st, env);
std::cout << std::endl;
printEnvBindings(st, *se.up, *env.up, ++lvl);
} else {
std::cout << ANSI_MAGENTA;
// for the top level, don't print the double underscore ones;
// they are in builtins.
for (auto & i : sortedBindingNames(st, se))
if (!i.starts_with("__"))
std::cout << i << " ";
std::cout << ANSI_NORMAL;
std::cout << std::endl;
if (se.isWith)
printWithBindings(st, env); // probably nothing there for the top level.
std::cout << std::endl;
}
}
void printEnvBindings(const EvalState &es, const Expr & expr, const Env & env)
{
// just print the names for now
auto se = es.ctx.debug->staticEnvFor(expr);
if (se)
printEnvBindings(es.ctx.symbols, *se, env, 0);
}
void mapStaticEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & env, ValMap & vm)
{
// add bindings for the next level up first, so that the bindings for this level
// override the higher levels.
// The top level bindings (builtins) are skipped since they are added for us by initEnv()
if (env.up && se.up) {
mapStaticEnvBindings(st, *se.up, *env.up, vm);
if (se.isWith && env.values[0].type() == nAttrs) {
// add 'with' bindings.
Bindings::iterator j = env.values[0].attrs()->begin();
while (j != env.values[0].attrs()->end()) {
vm.insert_or_assign(std::string(st[j->name]), j->value);
++j;
}
} else {
// iterate through staticenv bindings and add them.
for (auto & i : se.vars)
vm.insert_or_assign(std::string(st[i.first]), env.values[i.second]);
}
}
}
std::unique_ptr<ValMap> mapStaticEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & env)
{
auto vm = std::make_unique<ValMap>();
mapStaticEnvBindings(st, se, env, *vm);
return vm;
}
/**
* Sets `inDebugger` to true on construction and false on destruction.
*/
class DebuggerGuard {
bool & inDebugger;
public:
DebuggerGuard(bool & inDebugger) : inDebugger(inDebugger) {
inDebugger = true;
}
~DebuggerGuard() {
inDebugger = false;
}
};
void DebugState::onEvalError(
const EvalError * error, const Env & env, const Expr & expr, NeverAsync
)
{
// Make sure we have a debugger to run and we're not already in a debugger.
if (inDebugger)
return;
auto dts =
error && expr.getPos()
? addTrace(DebugTrace {
.pos = error->info().pos ? error->info().pos : positions[expr.getPos()],
.expr = expr,
.env = env,
.hint = error->info().msg,
.isError = true
})
: nullptr;
if (error)
{
printError("%s\n", Uncolored(error->what()));
if (trylevel > 0 && error->info().level != lvlInfo) {
if (evalSettings.ignoreExceptionsDuringTry) {
printError("This exception occurred in a 'tryEval' call, " ANSI_RED "despite the use of " ANSI_GREEN "--ignore-try" ANSI_RED " to attempt to skip these" ANSI_NORMAL ". This is probably a bug. We would appreciate if you report it along with what caused it at https://git.lix.systems/lix-project/lix/issues.\n");
} else {
printError("This exception occurred in a 'tryEval' call. Use " ANSI_GREEN "--ignore-try" ANSI_NORMAL " to skip these.\n");
}
}
}
auto se = staticEnvFor(expr);
if (se) {
auto vm = mapStaticEnvBindings(symbols, *se.get(), env);
DebuggerGuard _guard(inDebugger);
auto exitStatus = errorCallback(*vm, {});
switch (exitStatus) {
case ReplExitStatus::QuitAll:
if (error)
throw *error;
throw Exit(0);
case ReplExitStatus::Continue:
break;
default:
abort();
}
}
}
DebugState::TraceFrame DebugState::addTrace(DebugTrace t)
{
struct UnlinkDebugTrace
{
DebugState * state;
void operator()(DebugTrace * trace)
{
state->latestTrace = trace->parent;
delete trace;
}
};
t.parent = latestTrace.lock();
std::unique_ptr<DebugTrace, UnlinkDebugTrace> trace(
new auto(std::move(t)), UnlinkDebugTrace{.state = this}
);
std::shared_ptr<DebugTrace> entry(std::move(trace));
latestTrace = entry;
return TraceFrame{entry};
}
Value::List * EvalMemory::newList(size_t size)
{
auto list =
reinterpret_cast<Value::List *>(allocBytes(sizeof(Value::List) + size * sizeof(Value *)));
list->size = size;
stats.nrListElems += size;
return list;
}
Value Evaluator::evalLazily(Expr & e)
{
return e.makeThunk(*this, builtins.env);
}
Value EvalState::mkPos(PosIdx p)
{
auto origin = ctx.positions.originOf(p);
if (auto path = std::get_if<CheckedSourcePath>(&origin)) {
auto attrs = ctx.buildBindings(3);
attrs.insert(ctx.symbols.sym_file, {NewValueAs::string, path->to_string()});
auto [line, col] = makePositionThunks(*this, p);
attrs.insert(ctx.symbols.sym_line, line);
attrs.insert(ctx.symbols.sym_column, col);
return {NewValueAs::attrs, attrs};
} else
return Value::VNULL;
}
Value EvalPaths::mkStorePathString(const StorePath & p)
{
return {
NewValueAs::string,
store->printStorePath(p),
NixStringContext{
NixStringContextElem::Opaque{.path = p},
}
};
}
std::string EvalState::mkOutputStringRaw(
const StorePath & staticOutputPath)
{
return ctx.store->printStorePath(staticOutputPath);
}
Value EvalState::mkOutputString(const SingleDerivedPath::Built & b, const StorePath & staticOutputPath)
{
return {NewValueAs::string, mkOutputStringRaw(staticOutputPath), NixStringContext{b}};
}
std::string EvalState::mkSingleDerivedPathStringRaw(
const SingleDerivedPath & p)
{
return std::visit(overloaded {
[&](const SingleDerivedPath::Opaque & o) {
return ctx.store->printStorePath(o.path);
},
[&](const SingleDerivedPath::Built & b) {
auto drv = aio.blockOn(ctx.store->readDerivation(b.drvPath.path));
auto i = drv.outputs.find(b.output);
if (i == drv.outputs.end())
throw Error("derivation '%s' does not have output '%s'", b.drvPath.to_string(*ctx.store), b.output);
auto staticOutputPath = i->second.path(*ctx.store, drv.name, b.output);
return mkOutputStringRaw(staticOutputPath);
}
}, p.raw());
}
struct CachedEvalFile
{
Value result;
explicit CachedEvalFile(Value result): result(result) {}
};
Value EvalState::evalFile(const SourcePath & path_)
{
auto path = ctx.paths.checkSourcePath(path_);
if (auto i = ctx.caches.fileEval.find(path); i != ctx.caches.fileEval.end()) {
return i->second->result;
}
auto resolvedPath = ctx.paths.resolveExprPath(path);
if (auto i = ctx.caches.fileEval.find(resolvedPath); i != ctx.caches.fileEval.end()) {
return i->second->result;
}
debug("evaluating file '%1%'", resolvedPath);
Expr & e = ctx.parseExprFromFile(ctx.paths.checkSourcePath(resolvedPath));
try {
auto dts = ctx.debug
? makeDebugTraceStacker(
*this,
e,
ctx.builtins.env,
e.getPos() ? std::make_shared<Pos>(ctx.positions[e.getPos()]) : nullptr,
"while evaluating the file '%1%':", resolvedPath.to_string())
: nullptr;
Value v = eval(e);
auto cache = std::allocate_shared<CachedEvalFile>(TraceableAllocator<CachedEvalFile>(), v);
ctx.caches.fileEval[resolvedPath] = cache;
if (path != resolvedPath) {
ctx.caches.fileEval[path] = cache;
}
return v;
} catch (Error & e) {
e.addTrace(nullptr, "while evaluating the file '%1%':", resolvedPath.to_string());
throw;
}
}
void EvalState::resetFileCache()
{
ctx.caches.fileEval.clear();
}
Value EvalState::eval(Expr & e)
{
return e.eval(*this, ctx.builtins.env);
}
std::string showAttrPath(EvalState & state, Env & env, const AttrPath & attrPath)
{
std::ostringstream out;
bool first = true;
for (auto & i : attrPath) {
if (!first) out << '.'; else first = false;
try {
out << state.ctx.symbols[getName(i, state, env)];
} catch (Error & e) {
assert(!i.symbol);
out << "\"${...}\"";
}
}
return out.str();
}
struct FormalsMatch
{
std::vector<SymbolStr> missing;
std::vector<SymbolStr> unexpected;
std::set<std::string> unused;
};
/** Matchup an attribute argument set to a lambda's formal arguments,
* or return what arguments were required but not given, or given but not allowed.
*/
FormalsMatch matchupLambdaAttrs(EvalState & state, Env & env, Displacement & displ, AttrsPattern const & pattern, Bindings & attrs, SymbolTable & symbols)
{
size_t attrsUsed = 0;
FormalsMatch result;
for (auto const & formal : pattern.formals) {
// The attribute whose name matches the name of the formal we're matching up, if it exists.
Attr const * matchingArg = attrs.get(formal.name);
if (matchingArg) {
attrsUsed += 1;
env.values[displ] = matchingArg->value;
displ += 1;
// We're done here. Move on to the next formal.
continue;
}
// The argument for this formal wasn't given.
result.unused.emplace(symbols[formal.name]);
// If the formal has a default, use it.
if (formal.def) {
env.values[displ] = formal.def->maybeThunk(state, env);
displ += 1;
} else {
// Otherwise, let our caller know what was missing.
result.missing.push_back(symbols[formal.name]);
}
}
// Check for unexpected extra arguments.
if (!pattern.ellipsis && attrsUsed != attrs.size()) {
// Return the first unexpected argument.
for (Attr const & attr : attrs) {
if (!pattern.has(attr.name)) {
result.unexpected.push_back(symbols[attr.name]);
}
}
}
return result;
}
Env & SimplePattern::match(
ExprLambda & lambda, EvalState & state, Env & up, Value & arg, const PosIdx pos
)
{
Env & env2(state.ctx.mem.allocEnv(1));
env2.up = &up;
env2.values[0] = arg;
return env2;
}
Env & AttrsPattern::match(
ExprLambda & lambda, EvalState & state, Env & up, Value & arg, const PosIdx pos
)
{
auto & ctx = state.ctx;
Env & env2(ctx.mem.allocEnv(formals.size() + (name ? 1 : 0)));
env2.up = &up;
Displacement displ = 0;
try {
state.forceAttrs(
arg, lambda.pos, "while evaluating the value passed for the lambda argument"
);
} catch (Error & e) {
if (pos) e.addTrace(ctx.positions[pos], "from call site");
throw;
}
if (name) {
env2.values[displ++] = arg;
}
///* For each formal argument, get the actual argument. If
// there is no matching actual argument but the formal
// argument has a default, use the default. */
auto const formalsMatch =
matchupLambdaAttrs(state, env2, displ, *this, *arg.attrs(), ctx.symbols);
if (!formalsMatch.unexpected.empty() || !formalsMatch.missing.empty()) {
Suggestions sug; // empty suggestions -> no suggestions
if (!formalsMatch.unexpected.empty()) {
// suggestions only for the first unexpected argument
// TODO: suggestions for all unexpected arguments
sug = Suggestions::bestMatches(formalsMatch.unused, formalsMatch.unexpected.front());
}
auto argFmt = [](const SymbolStr & argument) { return HintFmt("'%s'", argument); };
[&](){
if (formalsMatch.unexpected.empty() && !formalsMatch.missing.empty()) {
return ctx.errors.make<TypeError>(
"function '%s' called without required argument%s %s", lambda.getName(ctx.symbols),
Uncolored((formalsMatch.missing.size() == 1) ? "" : "s"),
Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.missing))
);
} else if (!formalsMatch.unexpected.empty() && formalsMatch.missing.empty()) {
return ctx.errors.make<TypeError>(
"function '%s' called with unexpected argument%s %s", lambda.getName(ctx.symbols),
Uncolored((formalsMatch.unexpected.size() == 1) ? "" : "s"),
Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.unexpected))
);
} else {
return ctx.errors.make<TypeError>(
"function '%s' called without required argument%s %s and with unexpected argument%s %s", lambda.getName(ctx.symbols),
Uncolored((formalsMatch.missing.size() == 1) ? "" : "s"),
Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.missing)),
Uncolored((formalsMatch.unexpected.size() == 1) ? "" : "s"),
Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.unexpected))
);
}
}().atPos(lambda.pos)
.withTrace(pos, "from call site")
.withSuggestions(sug)
.withFrame(up, lambda)
.debugThrow();
}
return env2;
}
Value EvalState::callFunction(Value & fun, std::span<Value> args, const PosIdx pos)
{
if (callDepth > evalSettings.maxCallDepth)
ctx.errors.make<EvalError>("stack overflow; max-call-depth exceeded").atPos(pos).debugThrow();
MaintainCount _level(callDepth);
auto trace = evalSettings.traceFunctionCalls
? std::make_unique<FunctionCallTrace>(ctx.positions[pos])
: nullptr;
forceValue(fun, pos);
Value vCur(fun);
auto makeAppChain = [&]() -> Value {
if (vCur.isApp()) {
auto & app = vCur.app();
return {NewValueAs::app, ctx.mem, app.left(), app.args(), args};
} else {
return {NewValueAs::app, ctx.mem, vCur, args};
}
};
const Attr * functor;
while (args.size() > 0) {
if (vCur.isLambda()) {
ExprLambda & lambda(*vCur.lambda().fun);
Env & env2 = lambda.pattern->match(lambda, *this, *vCur.lambda().env(), args[0], pos);
ctx.stats.nrFunctionCalls++;
if (ctx.stats.countCalls) ctx.stats.addCall(lambda);
/* Evaluate the body. */
try {
vCur = lambda.body->eval(*this, env2);
} catch (Error & e) {
if (loggerSettings.showTrace.get()) {
e.addTrace(
ctx.positions[lambda.pos],
"while calling %s",
lambda.getQuotedName(ctx.symbols));
if (pos) e.addTrace(ctx.positions[pos], "from call site");
}
throw;
}
args = args.subspan(1);
}
else if (vCur.isPrimOp()) {
size_t argsLeft = vCur.primOp()->arity;
if (args.size() < argsLeft) {
/* We don't have enough arguments, so create a tPrimOpApp chain. */
return makeAppChain();
} else {
/* We have all the arguments, so call the primop. */
auto * fn = vCur.primOp();
ctx.stats.nrPrimOpCalls++;
if (ctx.stats.countCalls) ctx.stats.primOpCalls[fn->name]++;
try {
SmallVector<Value *, 4> pargs(argsLeft);
for (unsigned i = 0; i < argsLeft; i++) {
pargs[i] = &args[i];
}
vCur = fn->fun(*this, pargs.data());
} catch (ThrownError & e) {
// Distinguish between an error that simply happened while "throw"
// was being evaluated and an explicit thrown error.
if (fn->name == "throw" && !e.hasTrace()) {
e.addTrace(ctx.positions[pos], "caused by explicit %s", "throw");
} else {
e.addTrace(ctx.positions[pos], "while calling the '%s' builtin", fn->name);
}
throw;
} catch (Error & e) {
e.addTrace(ctx.positions[pos], "while calling the '%1%' builtin", fn->name);
throw;
}
args = args.subspan(argsLeft);
}
}
else if (vCur.isPrimOpApp()) {
auto & app = vCur.app();
auto prevArgs = app.args();
assert(!vCur.app().left().isApp());
/* Figure out the number of arguments still needed. */
Value primOp = app.target();
auto arity = primOp.primOp()->arity;
if (args.size() < arity - prevArgs.size()) {
/* We still don't have enough arguments, so extend the tPrimOpApp chain. */
return makeAppChain();
} else {
/* We have all the arguments, so call the primop with
the previous and new arguments. */
// max arity as of writing is 3. even 4 seems excessive though.
SmallVector<Value *, 4> vArgs;
for (auto & arg : prevArgs) {
vArgs.push_back(&arg);
}
while (vArgs.size() < arity) {
vArgs.push_back(&args[0]);
args = args.subspan(1);
}
auto fn = primOp.primOp();
ctx.stats.nrPrimOpCalls++;
if (ctx.stats.countCalls) ctx.stats.primOpCalls[fn->name]++;
try {
// TODO:
// 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)
// so the debugger allows to inspect the wrong parameters passed to the builtin.
vCur = fn->fun(*this, vArgs.data());
} catch (Error & e) {
e.addTrace(ctx.positions[pos], "while calling the '%1%' builtin", fn->name);
throw;
}
}
}
else if (vCur.type() == nAttrs && (functor = vCur.attrs()->get(ctx.symbols.sym___functor)))
{
/* 'vCur' may be allocated on the stack of the calling
function, but for functors we may keep a reference, so
heap-allocate a copy and use that instead. */
Value args2[] = {vCur, args[0]};
try {
vCur = callFunction(functor->value, args2, functor->pos);
} catch (Error & e) {
e.addTrace(ctx.positions[pos], "while calling a functor (an attribute set with a '__functor' attribute)");
throw;
}
args = args.subspan(1);
}
else {
ctx.errors
.make<TypeError>(
"attempt to call something which is not a function but %1%: %2%",
showType(vCur),
ValuePrinter(*this, vCur, errorPrintOptions)
)
.atPos(pos)
.debugThrow();
}
}
return vCur;
}
// Lifted out of callFunction() because it creates a temporary that
// prevents tail-call optimisation.
void EvalStatistics::addCall(ExprLambda & fun)
{
functionCalls[&fun]++;
}
Value EvalState::autoCallFunction(Bindings & args, Value & fun, PosIdx pos)
{
forceValue(fun, pos);
if (fun.type() == nAttrs) {
auto found = fun.attrs()->get(ctx.symbols.sym___functor);
if (found) {
Value v = callFunction(found->value, fun, pos);
forceValue(v, pos);
return autoCallFunction(args, v, pos);
}
}
if (!fun.isLambda()) {
return fun;
}
auto pattern = dynamic_cast<AttrsPattern *>(fun.lambda().fun->pattern.get());
if (!pattern) {
return fun;
}
auto attrs = ctx.buildBindings(std::max(static_cast<uint32_t>(pattern->formals.size()), args.size()));
if (pattern->ellipsis) {
// If the formals have an ellipsis (eg the function accepts extra args) pass
// all available automatic arguments (which includes arguments specified on
// the command line via --arg/--argstr)
for (auto & v : args)
attrs.insert(v);
} else {
// Otherwise, only pass the arguments that the function accepts
for (auto & i : pattern->formals) {
auto j = args.get(i.name);
if (j) {
attrs.insert(*j);
} else if (!i.def) {
ctx.errors
.make<MissingArgumentError>(
R"(cannot evaluate a function that has an argument without a value ('%1%')
Lix attempted to evaluate a function as a top level expression; in
this case it must have its arguments supplied either by default
values, or passed explicitly with '--arg' or '--argstr'. See
https://docs.lix.systems/manual/lix/stable/language/constructs.html#functions)",
ctx.symbols[i.name]
)
.atPos(i.pos)
.withFrame(*fun.lambda().env(), *fun.lambda().fun)
.debugThrow();
}
}
}
Value vAttrs{NewValueAs::attrs, attrs.finish()};
return callFunction(fun, vAttrs, pos);
}
Value EvalState::updateAttrs(const Value & v1, const Value & v2)
{
ctx.stats.nrOpUpdates++;
if (v1.attrs()->size() == 0) {
return v2;
}
if (v2.attrs()->size() == 0) {
return v1;
}
auto attrs = ctx.buildBindings(v1.attrs()->size() + v2.attrs()->size());
/* Merge the sets, preferring values from the second set. Make
sure to keep the resulting vector in sorted order. */
Bindings::iterator i = v1.attrs()->begin();
Bindings::iterator j = v2.attrs()->begin();
while (i != v1.attrs()->end() && j != v2.attrs()->end()) {
if (i->name == j->name) {
attrs.insert(*j);
++i;
++j;
} else if (i->name < j->name) {
attrs.insert(*i++);
} else {
attrs.insert(*j++);
}
}
while (i != v1.attrs()->end()) {
attrs.insert(*i++);
}
while (j != v2.attrs()->end()) {
attrs.insert(*j++);
}
Value v = {NewValueAs::attrs, attrs.alreadySorted()};
ctx.stats.nrOpUpdateValuesCopied += v.attrs()->size();
return v;
}
Value EvalState::concatLists(std::span<Value> lists, const PosIdx pos, std::string_view errorCtx)
{
ctx.stats.nrListConcats++;
Value * nonEmpty = 0;
size_t len = 0;
for (size_t n = 0; n < lists.size(); ++n) {
forceList(lists[n], pos, errorCtx);
auto l = lists[n].listSize();
len += l;
if (l) {
nonEmpty = &lists[n];
}
}
if (nonEmpty && len == nonEmpty->listSize()) {
return *nonEmpty;
}
auto list = ctx.mem.newList(len);
auto out = list->elems;
for (size_t n = 0, pos = 0; n < lists.size(); ++n) {
auto l = lists[n].listSize();
if (l) {
std::copy(lists[n].listItems().begin(), lists[n].listItems().end(), out + pos);
}
pos += l;
}
return {NewValueAs::list, list};
}
// 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.atPos(ctx.positions[pos]);
} catch (...) {
}
}
void EvalState::forceValueDeep(Value & v)
{
std::set<const Value *> seen;
std::function<void(Value & v)> recurse;
recurse = [&](Value & v) {
if (!seen.insert(&v).second) return;
forceValue(v, noPos);
if (v.type() == nAttrs) {
for (auto & i : *v.attrs())
try {
// If the value is a thunk, we're evaling. Otherwise no trace necessary.
auto dts = ctx.debug && i.value.isThunk()
? makeDebugTraceStacker(
*this,
*i.value.thunk().expr,
*i.value.thunk().env(),
ctx.positions[i.pos],
"while evaluating the attribute '%1%'",
ctx.symbols[i.name]
)
: nullptr;
recurse(i.value);
} catch (Error & e) {
e.addTrace(ctx.positions[i.pos], "while evaluating the attribute '%1%'", ctx.symbols[i.name]);
throw;
}
}
else if (v.isList()) {
for (auto & v2 : v.listItems()) {
recurse(v2);
}
}
};
recurse(v);
}
NixInt EvalState::forceInt(Value & v, const PosIdx pos, std::string_view errorCtx)
{
try {
forceValue(v, pos);
return checkInt(v);
} catch (Error & e) {
e.addTrace(ctx.positions[pos], errorCtx);
throw;
}
}
NixFloat EvalState::forceFloat(Value & v, const PosIdx pos, std::string_view errorCtx)
{
try {
forceValue(v, pos);
return checkFloat(v);
} catch (Error & e) {
e.addTrace(ctx.positions[pos], errorCtx);
throw;
}
}
bool EvalState::forceBool(Value & v, const PosIdx pos, std::string_view errorCtx)
{
try {
forceValue(v, pos);
return checkBool(v);
} catch (Error & e) {
e.addTrace(ctx.positions[pos], errorCtx);
throw;
}
}
bool EvalState::isFunctor(Value & fun)
{
return fun.type() == nAttrs && fun.attrs()->get(ctx.symbols.sym___functor);
}
void EvalState::forceFunction(Value & v, const PosIdx pos, std::string_view errorCtx)
{
try {
forceValue(v, pos);
if (v.type() != nFunction && !isFunctor(v))
ctx.errors.make<TypeError>(
"expected a function but found %1%: %2%",
showType(v),
ValuePrinter(*this, v, errorPrintOptions)
).atPos(pos).debugThrow();
} catch (Error & e) {
e.addTrace(ctx.positions[pos], errorCtx);
throw;
}
}
std::string_view EvalState::forceString(Value & v, const PosIdx pos, std::string_view errorCtx)
{
try {
forceValue(v, pos);
if (v.type() != nString)
ctx.errors.make<TypeError>(
"expected a string but found %1%: %2%",
showType(v),
ValuePrinter(*this, v, errorPrintOptions)
).atPos(pos).debugThrow();
return v.str();
} catch (Error & e) {
e.addTrace(ctx.positions[pos], errorCtx);
throw;
}
}
void copyContext(const Value & v, NixStringContext & context)
{
if (v.string().context)
for (const char * * p = v.string().context; *p; ++p)
context.insert(NixStringContextElem::parse(*p));
}
std::string_view EvalState::forceString(Value & v, NixStringContext & context, const PosIdx pos, std::string_view errorCtx)
{
auto s = forceString(v, pos, errorCtx);
copyContext(v, context);
return s;
}
std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos, std::string_view errorCtx)
{
auto s = forceString(v, pos, errorCtx);
if (v.string().context) {
ctx.errors
.make<EvalError>(
"the string '%1%' is not allowed to refer to a store path (such as '%2%')",
v.str(),
v.string().context[0]
)
.withTrace(pos, errorCtx)
.debugThrow();
}
return s;
}
bool EvalState::isDerivation(Value & v)
{
if (v.type() != nAttrs) return false;
auto i = v.attrs()->get(ctx.symbols.sym_type);
if (!i) {
return false;
}
forceValue(i->value, i->pos);
if (i->value.type() != nString) {
return false;
}
return i->value.str() == "derivation";
}
std::optional<std::string> EvalState::tryAttrsToString(const PosIdx pos, Value & v,
NixStringContext & context, StringCoercionMode mode, bool copyToStore)
{
auto i = v.attrs()->get(ctx.symbols.sym___toString);
if (i) {
try {
Value v1 = callFunction(i->value, v, i->pos);
return coerceToString(pos, v1, context,
"while evaluating the result of the `__toString` attribute",
mode, copyToStore).toOwned();
} catch (EvalError & e) {
e.addTrace(ctx.positions[pos], "while converting a set to string");
throw;
}
}
return {};
}
BackedStringView EvalState::coerceToString(
const PosIdx pos,
Value & v,
NixStringContext & context,
std::string_view errorCtx,
StringCoercionMode mode,
bool copyToStore,
bool canonicalizePath)
{
forceValue(v, pos);
if (v.type() == nString) {
copyContext(v, context);
return v.str();
}
if (v.type() == nPath) {
return !canonicalizePath && !copyToStore
// FIXME: hack to preserve path literals that end in a slash, as in /foo/${x}.
? std::string(v.string().content->str())
: (copyToStore
? ctx.store->printStorePath(
aio.blockOn(ctx.paths.copyPathToStore(context, v.path(), ctx.repair))
.unwrap()
)
: v.path().to_string());
}
if (v.type() == nAttrs) {
auto maybeString = tryAttrsToString(pos, v, context, mode, copyToStore);
if (maybeString)
return std::move(*maybeString);
auto i = v.attrs()->get(ctx.symbols.sym_outPath);
if (!i) {
ctx.errors.make<TypeError>(
"cannot coerce %1% to a string: %2%",
showType(v),
ValuePrinter(*this, v, errorPrintOptions)
)
.withTrace(pos, errorCtx)
.debugThrow();
}
return coerceToString(
pos, i->value, context, errorCtx, mode, copyToStore, canonicalizePath
);
}
if (v.type() == nExternal) {
try {
return v.external()->coerceToString(*this, pos, context, mode, copyToStore);
} catch (Error & e) {
e.addTrace(nullptr, errorCtx);
throw;
}
}
/* Raito: Any addition to this mode is subject to extra scrutiny
* until we have better formatting tools. */
if (mode >= StringCoercionMode::Interpolation) {
if (v.type() == nInt) {
return std::to_string(v.integer().value);
}
}
if (mode >= StringCoercionMode::ToString) {
/* Note that `false' is represented as an empty string for
shell scripting convenience, just like `null'. */
if (v.type() == nBool && v.boolean()) {
return "1";
}
if (v.type() == nBool && !v.boolean()) {
return "";
}
if (v.type() == nFloat) return std::to_string(v.fpoint());
if (v.type() == nNull) return "";
if (v.isList()) {
std::string result;
for (auto [n, v2] : enumerate(v.listItems())) {
try {
result += *coerceToString(
pos,
v2,
context,
"while evaluating one element of the list",
mode,
copyToStore,
canonicalizePath
);
} catch (Error & e) {
e.addTrace(ctx.positions[pos], errorCtx);
throw;
}
if (n < v.listSize() - 1
/* !!! not quite correct */
&& (!v2.isList() || v2.listSize() != 0))
{
result += " ";
}
}
return result;
}
}
ctx.errors.make<TypeError>("cannot coerce %1% to a string: %2%",
showType(v),
ValuePrinter(*this, v, errorPrintOptions)
)
.withTrace(pos, errorCtx)
.debugThrow();
}
kj::Promise<Result<EvalPaths::PathResult<StorePath, EvalError>>>
EvalPaths::copyPathToStore(NixStringContext & context, const SourcePath & path, RepairFlag repair)
try {
if (nix::isDerivation(path.canonical().abs()))
co_return errors.make<EvalError>("file names are not allowed to end in '%1%'", drvExtension);
auto i = srcToStore.find(path);
auto dstPath = i != srcToStore.end()
? i->second
: ({
auto dstPath = TRY_AWAIT(fetchToStoreRecursive(
*store,
*prepareDump(checkSourcePath(path).canonical().abs()),
path.baseName(),
repair
));
allowPath(dstPath);
srcToStore.insert_or_assign(path, dstPath);
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
std::move(dstPath);
});
context.insert(NixStringContextElem::Opaque {
.path = dstPath
});
co_return dstPath;
} catch (...) {
co_return result::current_exception();
}
SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)
{
auto path = coerceToString(pos, v, context, errorCtx, StringCoercionMode::Strict, false, true).toOwned();
if (path == "" || path[0] != '/')
ctx.errors.make<EvalError>("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow();
return CanonPath(path);
}
StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)
{
auto path = coerceToString(pos, v, context, errorCtx, StringCoercionMode::Strict, false, true).toOwned();
if (auto storePath = ctx.store->maybeParseStorePath(path))
return *storePath;
ctx.errors.make<EvalError>("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow();
}
std::pair<SingleDerivedPath, std::string_view> EvalState::coerceToSingleDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx)
{
NixStringContext context;
auto s = forceString(v, context, pos, errorCtx);
auto csize = context.size();
if (csize != 1)
ctx.errors.make<EvalError>(
"string '%s' has %d entries in its context. It should only have exactly one entry",
s, csize)
.withTrace(pos, errorCtx).debugThrow();
auto derivedPath = std::visit(overloaded {
[&](NixStringContextElem::Opaque && o) -> SingleDerivedPath {
return std::move(o);
},
[&](NixStringContextElem::DrvDeep &&) -> SingleDerivedPath {
ctx.errors.make<EvalError>(
"string '%s' has a context which refers to a complete source and binary closure. This is not supported at this time",
s).withTrace(pos, errorCtx).debugThrow(always_progresses);
},
[&](NixStringContextElem::Built && b) -> SingleDerivedPath {
return std::move(b);
},
}, ((NixStringContextElem &&) *context.begin()).raw);
return {
std::move(derivedPath),
std::move(s),
};
}
SingleDerivedPath EvalState::coerceToSingleDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx)
{
auto [derivedPath, s_] = coerceToSingleDerivedPathUnchecked(pos, v, errorCtx);
auto s = s_;
auto sExpected = mkSingleDerivedPathStringRaw(derivedPath);
if (s != sExpected) {
/* `std::visit` is used here just to provide a more precise
error message. */
std::visit(overloaded {
[&](const SingleDerivedPath::Opaque & o) {
ctx.errors.make<EvalError>(
"path string '%s' has context with the different path '%s'",
s, sExpected)
.withTrace(pos, errorCtx).debugThrow(always_progresses);
},
[&](const SingleDerivedPath::Built & b) {
ctx.errors.make<EvalError>(
"string '%s' has context with the output '%s' from derivation '%s', but the string is not the right placeholder for this derivation output. It should be '%s'",
s, b.output, b.drvPath.to_string(*ctx.store), sExpected)
.withTrace(pos, errorCtx).debugThrow(always_progresses);
}
}, derivedPath.raw());
}
return derivedPath;
}
bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx)
{
forceValue(v1, pos);
forceValue(v2, pos);
// Special case type-compatibility between float and int
if (v1.type() == nInt && v2.type() == nFloat) {
return v1.integer().value == v2.fpoint();
}
if (v1.type() == nFloat && v2.type() == nInt) {
return v1.fpoint() == v2.integer().value;
}
// All other types are not compatible with each other.
if (v1.type() != v2.type()) return false;
/* !!! Hack to support some old broken code that relies on pointer
equality tests between sets. (Specifically, builderDefs calls
uniqList on a list of sets.) Will remove this eventually. */
auto pointerEq = [&] { return v1.pointerEqProxy() == v2.pointerEqProxy(); };
switch (v1.type()) {
case nInt:
return v1.integer() == v2.integer();
case nBool:
return v1.boolean() == v2.boolean();
case nString:
return v1.str() == v2.str();
case nPath:
return v1.string().content->str() == v2.string().content->str();
case nNull:
return true;
case nList:
if (pointerEq()) return true;
if (v1.listSize() != v2.listSize()) return false;
for (size_t n = 0; n < v1.listSize(); ++n) {
if (!eqValues(v1.listElems()[n], v2.listElems()[n], pos, errorCtx)) {
return false;
}
}
return true;
case nAttrs: {
if (pointerEq()) return true;
/* If both sets denote a derivation (type = "derivation"),
then compare their outPaths. */
if (isDerivation(v1) && isDerivation(v2)) {
auto i = v1.attrs()->get(ctx.symbols.sym_outPath);
auto j = v2.attrs()->get(ctx.symbols.sym_outPath);
if (i && j) {
return eqValues(i->value, j->value, pos, errorCtx);
}
}
if (v1.attrs()->size() != v2.attrs()->size()) return false;
/* Otherwise, compare the attributes one by one. */
Bindings::iterator i, j;
for (i = v1.attrs()->begin(), j = v2.attrs()->begin(); i != v1.attrs()->end(); ++i, ++j)
{
if (i->name != j->name || !eqValues(i->value, j->value, pos, errorCtx)) {
return false;
}
}
return true;
}
/* Functions are incomparable, except for identity (see note above about this nonsense). */
case nFunction:
return pointerEq();
case nExternal:
if (pointerEq()) return true;
return *v1.external() == *v2.external();
case nFloat:
return v1.fpoint() == v2.fpoint();
case nThunk: // Must not be left by forceValue
default:
ctx.errors.make<EvalError>("cannot compare %1% with %2%", showType(v1), showType(v2)).withTrace(pos, errorCtx).debugThrow();
}
}
bool Evaluator::fullGC() {
#if HAVE_BOEHMGC
GC_gcollect();
// Check that it ran. We might replace this with a version that uses more
// of the boehm API to get this reliably, at a maintenance cost.
// We use a 1K margin because technically this has a race condtion, but we
// probably won't encounter it in practice, because the CLI isn't concurrent
// like that.
return GC_get_bytes_since_gc() < 1024;
#else
return false;
#endif
}
void Evaluator::maybePrintStats()
{
bool showStats = getEnv("NIX_SHOW_STATS").value_or("0") != "0";
if (showStats) {
// Make the final heap size more deterministic.
#if HAVE_BOEHMGC
if (!fullGC()) {
printTaggedWarning("failed to perform a full GC before reporting stats");
}
#endif
printStatistics();
}
}
void Evaluator::printStatistics()
{
struct rusage buf;
getrusage(RUSAGE_SELF, &buf);
float cpuTime = buf.ru_utime.tv_sec + ((float) buf.ru_utime.tv_usec / 1000000);
auto mem = this->mem.getStats();
uint64_t bEnvs = mem.nrEnvs * sizeof(Env) + mem.nrValuesInEnvs * sizeof(Value *);
uint64_t bLists = mem.nrListElems * sizeof(Value *);
uint64_t bAttrsets = mem.nrAttrsets * sizeof(Bindings) + mem.nrAttrsInAttrsets * sizeof(Attr);
#if HAVE_BOEHMGC
GC_word heapSize, totalBytes;
GC_get_heap_usage_safe(&heapSize, 0, 0, 0, &totalBytes);
#endif
auto outPath = getEnv("NIX_SHOW_STATS_PATH").value_or("-");
std::fstream fs;
if (outPath != "-")
fs.open(outPath, std::fstream::out);
JSON topObj = JSON::object();
topObj["cpuTime"] = cpuTime;
topObj["envs"] = {
{"number", mem.nrEnvs},
{"elements", mem.nrValuesInEnvs},
{"bytes", bEnvs},
};
topObj["list"] = {
{"elements", mem.nrListElems},
{"bytes", bLists},
{"concats", stats.nrListConcats},
};
// reported for compatibility, even though we no longer allocate these on the heap
topObj["values"] = {
{"number", 0},
{"bytes", 0},
};
topObj["symbols"] = {
{"number", symbols.size()},
{"bytes", symbols.totalSize()},
};
topObj["sets"] = {
{"number", mem.nrAttrsets},
{"bytes", bAttrsets},
{"elements", mem.nrAttrsInAttrsets},
};
topObj["sizes"] = {
{"Env", sizeof(Env)},
{"Value", sizeof(Value)},
{"Bindings", sizeof(Bindings)},
{"Attr", sizeof(Attr)},
};
topObj["nrOpUpdates"] = stats.nrOpUpdates;
topObj["nrOpUpdateValuesCopied"] = stats.nrOpUpdateValuesCopied;
topObj["nrThunks"] = stats.nrThunks;
topObj["nrAvoided"] = stats.nrAvoided;
topObj["nrLookups"] = stats.nrLookups;
topObj["nrPrimOpCalls"] = stats.nrPrimOpCalls;
topObj["nrFunctionCalls"] = stats.nrFunctionCalls;
#if HAVE_BOEHMGC
topObj["gc"] = {
{"heapSize", heapSize},
{"totalBytes", totalBytes},
};
#endif
if (stats.countCalls) {
topObj["primops"] = stats.primOpCalls;
{
auto& list = topObj["functions"];
list = JSON::array();
for (auto & [fun, count] : stats.functionCalls) {
JSON obj = JSON::object();
if (fun->name)
obj["name"] = (std::string_view) symbols[fun->name];
else
obj["name"] = nullptr;
if (auto pos = positions[fun->pos]) {
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
obj["file"] = path->to_string();
obj["line"] = pos.line;
obj["column"] = pos.column;
}
obj["count"] = count;
list.push_back(obj);
}
}
{
auto list = topObj["attributes"];
list = JSON::array();
for (auto & i : stats.attrSelects) {
JSON obj = JSON::object();
if (auto pos = positions[i.first]) {
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
obj["file"] = path->to_string();
obj["line"] = pos.line;
obj["column"] = pos.column;
}
obj["count"] = i.second;
list.push_back(obj);
}
}
}
if (getEnv("NIX_SHOW_SYMBOLS").value_or("0") != "0") {
// XXX: overrides earlier assignment
topObj["symbols"] = JSON::array();
auto &list = topObj["symbols"];
symbols.dump([&](std::string_view s) { list.emplace_back(s); });
}
if (outPath == "-") {
std::cerr << topObj.dump(2) << std::endl;
} else {
fs << topObj.dump(2) << std::endl;
}
}
CheckedSourcePath EvalPaths::resolveExprPath(SourcePath path_)
{
auto path = checkSourcePath(path_);
unsigned int followCount = 0, maxFollow = 1024;
/* If `path' is a symlink, follow it. This is so that relative
path references work. */
while (true) {
// Basic cycle/depth limit to avoid infinite loops.
if (++followCount >= maxFollow)
throw Error("too many symbolic links encountered while traversing the path '%s'", path);
if (path.lstat().type != InputAccessor::tSymlink) break;
path = checkSourcePath(
CanonPath(path.readLink(), path.canonical().parent().value_or(CanonPath::root))
);
}
/* If `path' refers to a directory, append `/default.nix'. */
if (path.lstat().type == InputAccessor::tDirectory)
return checkSourcePath(path + "default.nix");
return path;
}
Expr & Evaluator::parseExprFromFile(const CheckedSourcePath & path)
{
return parseExprFromFile(path, builtins.staticEnv);
}
Expr & Evaluator::parseExprFromFile(const CheckedSourcePath & path, std::shared_ptr<StaticEnv> & staticEnv)
{
auto buffer = path.readFile();
return *parse(buffer.data(), buffer.size(), Pos::Origin(path), path.parent(), staticEnv);
}
Expr & Evaluator::parseExprFromString(
std::string s_,
const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv,
const FeatureSettings & featureSettings
)
{
auto s = make_ref<std::string>(std::move(s_));
return *parse(s->data(), s->size(), Pos::String{.source = s}, basePath, staticEnv, featureSettings);
}
Expr & Evaluator::parseExprFromString(
std::string s,
const SourcePath & basePath,
const FeatureSettings & featureSettings
)
{
return parseExprFromString(std::move(s), basePath, builtins.staticEnv, featureSettings);
}
std::variant<std::unique_ptr<Expr>, ExprReplBindings>
Evaluator::parseReplInput(
std::string s_,
const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv,
const FeatureSettings & featureSettings
)
{
auto s = make_ref<std::string>(std::move(s_));
return parse_repl(s->data(), s->size(), Pos::String{.source = s}, basePath, staticEnv, featureSettings);
}
Expr & Evaluator::parseStdin()
{
//Activity act(*logger, lvlTalkative, "parsing standard input");
auto s = make_ref<std::string>(drainFD(0));
return *parse(
s->data(), s->size(), Pos::Stdin{.source = s}, CanonPath::fromCwd(), builtins.staticEnv
);
}
std::optional<DebugTrace const *> Evaluator::nextDebugTrace() const
{
if (!debug) {
return std::nullopt;
}
return debug->traces().next();
}
kj::Promise<Result<EvalPaths::PathResult<SourcePath, ThrownError>>>
EvalPaths::findFile(const std::string_view path)
{
return findFile(searchPath_, path);
}
kj::Promise<Result<EvalPaths::PathResult<SourcePath, ThrownError>>>
EvalPaths::findFile(const SearchPath & searchPath, const std::string_view path, const PosIdx pos)
try {
for (auto & i : searchPath.elements) {
auto suffixOpt = i.prefix.suffixIfPotentialMatch(path);
if (!suffixOpt) continue;
auto suffix = *suffixOpt;
auto rOpt = TRY_AWAIT(resolveSearchPathPath(i.path));
if (!rOpt) continue;
auto r = *rOpt;
Path res = suffix == "" ? r : concatStrings(r, "/", suffix);
if (pathExists(res)) co_return SourcePath(CanonPath(canonPath(res)));
}
if (path.starts_with("nix/"))
co_return SourcePath(CanonPath(concatStrings(corepkgsPrefix, path.substr(4))));
co_return errors.make<ThrownError>(
evalSettings.pureEval
? "cannot look up '<%s>' in pure evaluation mode (use '--impure' to override)"
: "file '%s' was not found in the Nix search path (add it using $NIX_PATH or -I)",
path
).atPos(pos);
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<std::optional<std::string>>>
EvalPaths::resolveSearchPathPath(const SearchPath::Path & value0)
try {
auto & value = value0.s;
auto i = searchPathResolved.find(value);
if (i != searchPathResolved.end()) co_return i->second;
std::optional<std::string> res;
if (EvalSettings::isPseudoUrl(value)) {
try {
auto storePath = TRY_AWAIT(fetchers::downloadTarball(
store, EvalSettings::resolvePseudoUrl(value), "source", false)).tree.storePath;
res = { store->toRealPath(storePath) };
} catch (FileTransferError & e) {
e.addTrace(nullptr, "while downloading %s to satisfy NIX_PATH lookup, ignoring search path entry", value);
logWarning(e.info());
res = std::nullopt;
}
}
else if (value.starts_with("flake:")) {
experimentalFeatureSettings.require(Xp::Flakes);
auto flakeRef = parseFlakeRef(value.substr(6), {}, true, false);
debug("fetching flake search path element '%s''", value);
auto storePath =
TRY_AWAIT(TRY_AWAIT(flakeRef.resolve(store)).fetchTree(store)).first.storePath;
res = {store->toRealPath(storePath)};
}
else {
auto path = absPath(value);
if (pathExists(path))
res = { path };
else {
logWarning({
.msg = HintFmt("Nix search path entry '%1%' does not exist, ignoring", value)
});
res = std::nullopt;
}
}
if (res)
debug("resolved search path element '%s' to '%s'", value, *res);
else
debug("failed to resolve search path element '%s'", value);
searchPathResolved[value] = res;
co_return res;
} catch (...) {
co_return result::current_exception();
}
std::string ExternalValueBase::coerceToString(EvalState & state, const PosIdx & pos, NixStringContext & context, StringCoercionMode mode, bool copyToStore) const
{
state.ctx.errors.make<TypeError>(
"cannot coerce %1% to a string: %2%", showType(), *this
).atPos(pos).debugThrow();
}
bool ExternalValueBase::operator==(const ExternalValueBase & b) const
{
return false;
}
std::ostream & operator << (std::ostream & str, const ExternalValueBase & v) {
return v.print(str);
}
}