diff options
Diffstat (limited to 'third_party/nix/src/libexpr/eval.cc')
-rw-r--r-- | third_party/nix/src/libexpr/eval.cc | 1878 |
1 files changed, 1878 insertions, 0 deletions
diff --git a/third_party/nix/src/libexpr/eval.cc b/third_party/nix/src/libexpr/eval.cc new file mode 100644 index 000000000000..682ea6483213 --- /dev/null +++ b/third_party/nix/src/libexpr/eval.cc @@ -0,0 +1,1878 @@ +#include "libexpr/eval.hh" + +#include <algorithm> +#include <chrono> +#include <cstdint> +#include <cstring> +#include <fstream> +#include <iostream> +#include <memory> +#include <new> +#include <optional> +#include <variant> + +#include <absl/base/call_once.h> +#include <absl/container/flat_hash_set.h> +#include <absl/strings/match.h> +#include <glog/logging.h> +#include <sys/resource.h> +#include <sys/time.h> +#include <unistd.h> + +#include "libexpr/eval-inline.hh" +#include "libexpr/function-trace.hh" +#include "libexpr/value.hh" +#include "libstore/derivations.hh" +#include "libstore/download.hh" +#include "libstore/globals.hh" +#include "libstore/store-api.hh" +#include "libutil/hash.hh" +#include "libutil/json.hh" +#include "libutil/util.hh" +#include "libutil/visitor.hh" + +namespace nix { +namespace { + +void ConfigureGc() { /* This function intentionally left blank. */ +} + +} // namespace + +namespace expr { + +absl::once_flag gc_flag; + +void InitGC() { absl::call_once(gc_flag, &ConfigureGc); } + +} // namespace expr + +static char* dupString(const char* s) { + char* t; + t = strdup(s); + if (t == nullptr) { + throw std::bad_alloc(); + } + return t; +} + +std::shared_ptr<Value*> allocRootValue(Value* v) { + return std::make_shared<Value*>(v); +} + +static void printValue(std::ostream& str, std::set<const Value*>& active, + const Value& v) { + checkInterrupt(); + + if (active.find(&v) != active.end()) { + str << "<CYCLE>"; + return; + } + active.insert(&v); + + switch (v.type) { + case tInt: + str << v.integer; + break; + case tBool: + str << (v.boolean ? "true" : "false"); + break; + case tString: + str << "\""; + for (const char* i = v.string.s; *i != 0; i++) { + if (*i == '\"' || *i == '\\') { + str << "\\" << *i; + } else if (*i == '\n') { + str << "\\n"; + } else if (*i == '\r') { + str << "\\r"; + } else if (*i == '\t') { + str << "\\t"; + } else { + str << *i; + } + } + str << "\""; + break; + case tPath: + str << v.path; // !!! escaping? + break; + case tNull: + str << "null"; + break; + case tAttrs: { + str << "{ "; + for (const auto& [key, value] : *v.attrs) { + str << key << " = "; + printValue(str, active, *value.value); + str << "; "; + } + str << "}"; + break; + } + case tList: + str << "[ "; + for (unsigned int n = 0; n < v.listSize(); ++n) { + printValue(str, active, *(*v.list)[n]); + str << " "; + } + str << "]"; + break; + case tThunk: + case tApp: + str << "<CODE>"; + break; + case tLambda: + str << "<LAMBDA>"; + break; + case tPrimOp: + str << "<PRIMOP>"; + break; + case tPrimOpApp: + str << "<PRIMOP-APP>"; + break; + case tFloat: + str << v.fpoint; + break; + default: + throw Error( + absl::StrCat("invalid value of type ", static_cast<int>(v.type))); + } + + active.erase(&v); +} + +std::ostream& operator<<(std::ostream& str, const Value& v) { + std::set<const Value*> active; + printValue(str, active, v); + return str; +} + +const Value* getPrimOp(const Value& v) { + const Value* primOp = &v; + while (primOp->type == tPrimOpApp) { + primOp = primOp->primOpApp.left; + } + assert(primOp->type == tPrimOp); + return primOp; +} + +std::string showType(const Value& v) { + switch (v.type) { + case tInt: + return "an integer"; + case tBool: + return "a boolean"; + case tString: + return v.string.context != nullptr ? "a string with context" : "a string"; + case tPath: + return "a path"; + case tNull: + return "null"; + case tAttrs: + return "a set"; + case tList: + return "a list"; + case tThunk: + return "a thunk"; + case tApp: + return "a function application"; + case tLambda: + return "a function"; + case tBlackhole: + return "a black hole"; + case tPrimOp: + return fmt("the built-in function '%s'", std::string(v.primOp->name)); + case tPrimOpApp: + return fmt("the partially applied built-in function '%s'", + std::string(getPrimOp(v)->primOp->name)); + case _reserved1: + LOG(FATAL) << "attempted to show the type string of the deprecated " + "tExternal value"; + break; + case tFloat: + return "a float"; + } + LOG(FATAL) + << "attempted to determine the type string of an unknown type number (" + << static_cast<int>(v.type) << ")"; + abort(); +} + +static Symbol getName(const AttrName& name, EvalState& state, Env& env) { + return std::visit( + util::overloaded{[&](const Symbol& name) -> Symbol { return name; }, + [&](Expr* expr) -> Symbol { + Value nameValue; + expr->eval(state, env, nameValue); + state.forceStringNoCtx(nameValue); + return state.symbols.Create(nameValue.string.s); + }}, + name); +} + +/* Very hacky way to parse $NIX_PATH, which is colon-separated, but + can contain URLs (e.g. "nixpkgs=https://bla...:foo=https://"). */ +static Strings parseNixPath(const std::string& s) { + Strings res; + + auto p = s.begin(); + + while (p != s.end()) { + auto start = p; + auto start2 = p; + + while (p != s.end() && *p != ':') { + if (*p == '=') { + start2 = p + 1; + } + ++p; + } + + if (p == s.end()) { + if (p != start) { + res.push_back(std::string(start, p)); + } + break; + } + + if (*p == ':') { + if (isUri(std::string(start2, s.end()))) { + ++p; + while (p != s.end() && *p != ':') { + ++p; + } + } + res.push_back(std::string(start, p)); + if (p == s.end()) { + break; + } + } + + ++p; + } + + return res; +} + +EvalState::EvalState(const Strings& _searchPath, const ref<Store>& store) + : sWith(symbols.Create("<with>")), + sOutPath(symbols.Create("outPath")), + sDrvPath(symbols.Create("drvPath")), + sType(symbols.Create("type")), + sMeta(symbols.Create("meta")), + sName(symbols.Create("name")), + sValue(symbols.Create("value")), + sSystem(symbols.Create("system")), + sOutputs(symbols.Create("outputs")), + sOutputName(symbols.Create("outputName")), + sIgnoreNulls(symbols.Create("__ignoreNulls")), + sFile(symbols.Create("file")), + sLine(symbols.Create("line")), + sColumn(symbols.Create("column")), + sFunctor(symbols.Create("__functor")), + sToString(symbols.Create("__toString")), + sRight(symbols.Create("right")), + sWrong(symbols.Create("wrong")), + sStructuredAttrs(symbols.Create("__structuredAttrs")), + sBuilder(symbols.Create("builder")), + sArgs(symbols.Create("args")), + sOutputHash(symbols.Create("outputHash")), + sOutputHashAlgo(symbols.Create("outputHashAlgo")), + sOutputHashMode(symbols.Create("outputHashMode")), + sDerivationNix(std::nullopt), + repair(NoRepair), + store(store), + baseEnv(allocEnv(128)), + staticBaseEnv(false, nullptr) { + expr::InitGC(); + + countCalls = getEnv("NIX_COUNT_CALLS").value_or("0") != "0"; + + /* Initialise the Nix expression search path. */ + if (!evalSettings.pureEval) { + Strings paths = parseNixPath(getEnv("NIX_PATH").value_or("")); + for (auto& i : _searchPath) { + addToSearchPath(i); + } + for (auto& i : paths) { + addToSearchPath(i); + } + } + addToSearchPath("nix=" + + canonPath(settings.nixDataDir + "/nix/corepkgs", true)); + + if (evalSettings.restrictEval || evalSettings.pureEval) { + allowedPaths = PathSet(); + + for (auto& i : searchPath) { + auto r = resolveSearchPathElem(i); + if (!r.first) { + continue; + } + + auto path = r.second; + + if (store->isInStore(r.second)) { + PathSet closure; + store->computeFSClosure(store->toStorePath(r.second), closure); + for (auto& path : closure) { + allowedPaths->insert(path); + } + } else { + allowedPaths->insert(r.second); + } + } + } + + createBaseEnv(); +} + +EvalState::~EvalState() = default; + +Path EvalState::checkSourcePath(const Path& path_) { + TraceFileAccess(path_); + if (!allowedPaths) { + return path_; + } + + auto i = resolvedPaths.find(path_); + if (i != resolvedPaths.end()) { + return i->second; + } + + bool found = false; + + /* 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. + */ + Path abspath = canonPath(path_); + + for (auto& i : *allowedPaths) { + if (isDirOrInDir(abspath, i)) { + found = true; + break; + } + } + + if (!found) { + throw RestrictedPathError( + "access to path '%1%' is forbidden in restricted mode", abspath); + } + + /* Resolve symlinks. */ + DLOG(INFO) << "checking access to '" << abspath << "'"; + Path path = canonPath(abspath, true); + + for (auto& i : *allowedPaths) { + if (isDirOrInDir(path, i)) { + resolvedPaths[path_] = path; + return path; + } + } + + throw RestrictedPathError( + "access to path '%1%' is forbidden in restricted mode", path); +} + +void EvalState::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.empty() && + absl::StartsWith(uri, prefix) && + (prefix[prefix.size() - 1] == '/' || uri[prefix.size()] == '/'))) { + return; + } + } + + /* If the URI is a path, then check it against allowedPaths as + well. */ + if (absl::StartsWith(uri, "/")) { + checkSourcePath(uri); + return; + } + + if (absl::StartsWith(uri, "file://")) { + checkSourcePath(std::string(uri, 7)); + return; + } + + throw RestrictedPathError( + "access to URI '%s' is forbidden in restricted mode", uri); +} + +Path EvalState::toRealPath(const Path& path, const PathSet& context) { + // FIXME: check whether 'path' is in 'context'. + return !context.empty() && store->isInStore(path) ? store->toRealPath(path) + : path; +}; + +Value* EvalState::addConstant(const std::string& name, Value& v) { + Value* v2 = allocValue(); + *v2 = v; + staticBaseEnv.vars[symbols.Create(name)] = baseEnvDispl; + baseEnv.values[baseEnvDispl++] = v2; + std::string name2 = + std::string(name, 0, 2) == "__" ? std::string(name, 2) : name; + baseEnv.values[0]->attrs->push_back(Attr(symbols.Create(name2), v2)); + return v2; +} + +Value* EvalState::addPrimOp(const std::string& name, size_t arity, + PrimOpFun primOp) { + if (arity == 0) { + Value v; + primOp(*this, noPos, nullptr, v); + return addConstant(name, v); + } + std::string name2 = + std::string(name, 0, 2) == "__" ? std::string(name, 2) : name; + Symbol sym = symbols.Create(name2); + Value* v = allocValue(); + v->type = tPrimOp; + v->primOp = std::make_shared<PrimOp>(primOp, arity, sym); + staticBaseEnv.vars[symbols.Create(name)] = baseEnvDispl; + baseEnv.values[baseEnvDispl++] = v; + baseEnv.values[0]->attrs->push_back(Attr(sym, v)); + return v; +} + +Value& EvalState::getBuiltin(const std::string& name) { + return *baseEnv.values[0]->attrs->find(symbols.Create(name))->second.value; +} + +/* Every "format" object (even temporary) takes up a few hundred bytes + of stack space, which is a real killer in the recursive + evaluator. So here are some helper functions for throwing + exceptions. */ + +LocalNoInlineNoReturn(void throwEvalError(const char* s, + const std::string& s2)) { + throw EvalError(format(s) % s2); +} + +LocalNoInlineNoReturn(void throwEvalError(const char* s, const std::string& s2, + const Pos& pos)) { + throw EvalError(format(s) % s2 % pos); +} + +LocalNoInlineNoReturn(void throwEvalError(const char* s, const std::string& s2, + const std::string& s3)) { + throw EvalError(format(s) % s2 % s3); +} + +LocalNoInlineNoReturn(void throwEvalError(const char* s, const std::string& s2, + const std::string& s3, + const Pos& pos)) { + throw EvalError(format(s) % s2 % s3 % pos); +} + +LocalNoInlineNoReturn(void throwEvalError(const char* s, const Symbol& sym, + const Pos& p1, const Pos& p2)) { + throw EvalError(format(s) % sym % p1 % p2); +} + +LocalNoInlineNoReturn(void throwTypeError(const char* s, const Pos& pos)) { + throw TypeError(format(s) % pos); +} + +LocalNoInlineNoReturn(void throwTypeError(const char* s, + const std::string& s1)) { + throw TypeError(format(s) % s1); +} + +LocalNoInlineNoReturn(void throwTypeError(const char* s, const ExprLambda& fun, + const Symbol& s2, const Pos& pos)) { + throw TypeError(format(s) % fun.showNamePos() % s2 % pos); +} + +LocalNoInlineNoReturn(void throwAssertionError(const char* s, + const std::string& s1, + const Pos& pos)) { + throw AssertionError(format(s) % s1 % pos); +} + +LocalNoInlineNoReturn(void throwUndefinedVarError(const char* s, + const std::string& s1, + const Pos& pos)) { + throw UndefinedVarError(format(s) % s1 % pos); +} + +LocalNoInline(void addErrorPrefix(Error& e, const char* s, + const std::string& s2)) { + e.addPrefix(format(s) % s2); +} + +LocalNoInline(void addErrorPrefix(Error& e, const char* s, + const ExprLambda& fun, const Pos& pos)) { + e.addPrefix(format(s) % fun.showNamePos() % pos); +} + +LocalNoInline(void addErrorPrefix(Error& e, const char* s, + const std::string& s2, const Pos& pos)) { + e.addPrefix(format(s) % s2 % pos); +} + +void mkString(Value& v, const char* s) { mkStringNoCopy(v, dupString(s)); } + +Value& mkString(Value& v, const std::string& s, const PathSet& context) { + mkString(v, s.c_str()); + if (!context.empty()) { + size_t n = 0; + v.string.context = static_cast<const char**>( + allocBytes((context.size() + 1) * sizeof(char*))); + for (auto& i : context) { + v.string.context[n++] = dupString(i.c_str()); + } + v.string.context[n] = nullptr; + } + return v; +} + +void mkPath(Value& v, const char* s) { mkPathNoCopy(v, dupString(s)); } + +inline Value* EvalState::lookupVar(Env* env, const ExprVar& var, bool noEval) { + for (size_t l = var.level; l != 0u; --l, env = env->up) { + ; + } + + if (!var.fromWith) { + return env->values[var.displ]; + } + + while (true) { + if (env->type == Env::HasWithExpr) { + if (noEval) { + return nullptr; + } + if (!env->withAttrsExpr) { + CHECK(false) << "HasWithExpr evaluated twice"; + } + Value* v = allocValue(); + evalAttrs(*env->up, env->withAttrsExpr, *v); + env->values[0] = v; + env->withAttrsExpr = nullptr; + env->type = Env::HasWithAttrs; + } + Bindings::iterator j = env->values[0]->attrs->find(var.name); + if (j != env->values[0]->attrs->end()) { + if (countCalls && (j->second.pos != nullptr)) { + attrSelects[*j->second.pos]++; + } + return j->second.value; + } + if (env->prevWith == 0u) { + throwUndefinedVarError("undefined variable '%1%' at %2%", var.name, + var.pos); + } + for (size_t l = env->prevWith; l != 0u; --l, env = env->up) { + } + } +} + +Value* EvalState::allocValue() { + nrValues++; + return new Value; +} + +Env& EvalState::allocEnv(size_t size) { + if (size > std::numeric_limits<decltype(Env::size)>::max()) { + throw Error("environment size %d is too big", size); + } + + nrEnvs++; + nrValuesInEnvs += size; + Env* env = new Env(size); + env->type = Env::Plain; + + return *env; +} + +void EvalState::mkList(Value& v, std::shared_ptr<NixList> list) { + nrListElems += list->size(); + clearValue(v); + v.type = tList; + v.list = list; +} + +void EvalState::mkList(Value& v, size_t size) { + EvalState::mkList(v, std::make_shared<NixList>(size)); +} + +unsigned long nrThunks = 0; + +static inline void mkThunk(Value& v, Env& env, Expr* expr) { + v.type = tThunk; + v.thunk.env = &env; + v.thunk.expr = expr; + nrThunks++; +} + +void EvalState::mkThunk_(Value& v, Expr* expr) { mkThunk(v, baseEnv, expr); } + +void EvalState::mkPos(Value& v, Pos* pos) { + if ((pos != nullptr) && pos->file.has_value() && pos->file.value().set()) { + mkAttrs(v, 3); + mkString(*allocAttr(v, sFile), pos->file.value()); + mkInt(*allocAttr(v, sLine), pos->line); + mkInt(*allocAttr(v, sColumn), pos->column); + } else { + mkNull(v); + } +} + +/* Create a thunk for the delayed computation of the given expression + in the given environment. But if the expression is a variable, + then look it up right away. This significantly reduces the number + of thunks allocated. */ +Value* Expr::maybeThunk(EvalState& state, Env& env) { + Value* v = state.allocValue(); + mkThunk(*v, env, this); + return v; +} + +unsigned long nrAvoided = 0; + +Value* ExprVar::maybeThunk(EvalState& state, Env& env) { + Value* v = state.lookupVar(&env, *this, true); + /* The value might not be initialised in the environment yet. + In that case, ignore it. */ + if (v != nullptr) { + nrAvoided++; + return v; + } + return Expr::maybeThunk(state, env); +} + +Value* ExprString::maybeThunk(EvalState& state, Env& env) { + nrAvoided++; + return &v; +} + +Value* ExprInt::maybeThunk(EvalState& state, Env& env) { + nrAvoided++; + return &v; +} + +Value* ExprFloat::maybeThunk(EvalState& state, Env& env) { + nrAvoided++; + return &v; +} + +Value* ExprPath::maybeThunk(EvalState& state, Env& env) { + nrAvoided++; + return &v; +} + +void EvalState::evalFile(const Path& path_, Value& v) { + auto path = checkSourcePath(path_); + + FileEvalCache::iterator i; + if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) { + v = i->second; + return; + } + + Path path2 = resolveExprPath(path); + if ((i = fileEvalCache.find(path2)) != fileEvalCache.end()) { + v = i->second; + return; + } + + VLOG(2) << "evaluating file '" << path2 << "'"; + Expr* e = nullptr; + + auto j = fileParseCache.find(path2); + if (j != fileParseCache.end()) { + e = j->second; + } + + if (e == nullptr) { + e = parseExprFromFile(checkSourcePath(path2)); + } + + fileParseCache[path2] = e; + + try { + eval(e, v); + } catch (Error& e) { + addErrorPrefix(e, "while evaluating the file '%1%':\n", path2); + throw; + } + + fileEvalCache[path2] = v; + if (path != path2) { + fileEvalCache[path] = v; + } +} + +void EvalState::resetFileCache() { + fileEvalCache.clear(); + fileParseCache.clear(); +} + +void EvalState::eval(Expr* e, Value& v) { e->eval(*this, baseEnv, v); } + +inline bool EvalState::evalBool(Env& env, Expr* e) { + Value v; + e->eval(*this, env, v); + if (v.type != tBool) { + throwTypeError("value is %1% while a Boolean was expected", v); + } + return v.boolean; +} + +inline bool EvalState::evalBool(Env& env, Expr* e, const Pos& pos) { + Value v; + e->eval(*this, env, v); + if (v.type != tBool) { + throwTypeError("value is %1% while a Boolean was expected, at %2%", v, pos); + } + return v.boolean; +} + +inline void EvalState::evalAttrs(Env& env, Expr* e, Value& v) { + e->eval(*this, env, v); + if (v.type != tAttrs) { + throwTypeError("value is %1% while a set was expected", v); + } +} + +void Expr::eval(EvalState& state, Env& env, Value& v) { abort(); } + +void ExprInt::eval(EvalState& state, Env& env, Value& v) { v = this->v; } + +void ExprFloat::eval(EvalState& state, Env& env, Value& v) { v = this->v; } + +void ExprString::eval(EvalState& state, Env& env, Value& v) { v = this->v; } + +void ExprPath::eval(EvalState& state, Env& env, Value& v) { v = this->v; } + +void ExprAttrs::eval(EvalState& state, Env& env, Value& value) { + state.mkAttrs(value, attrs.size() + dynamicAttrs.size()); + Env* dynamicEnv = &env; + + if (recursive) { + /* Create a new environment that contains the attributes in + this `rec'. */ + Env& env2(state.allocEnv(attrs.size())); + env2.up = &env; + dynamicEnv = &env2; + + /* The recursive attributes are evaluated in the new + environment, while the inherited attributes are evaluated + in the original environment. */ + size_t displ = 0; + for (auto& attr : attrs) { + Value* vAttr; + vAttr = + attr.second.e->maybeThunk(state, attr.second.inherited ? env : env2); + env2.values[displ++] = vAttr; + value.attrs->push_back(Attr(attr.first, vAttr, &attr.second.pos)); + } + } else { + // TODO(tazjin): insert range + for (auto& i : attrs) { + value.attrs->push_back( + Attr(i.first, i.second.e->maybeThunk(state, env), &i.second.pos)); + } + } + + /* Dynamic attrs apply *after* rec. */ + for (auto& i : dynamicAttrs) { + Value nameVal; + i.nameExpr->eval(state, *dynamicEnv, nameVal); + state.forceValue(nameVal, i.pos); + if (nameVal.type == tNull) { + continue; + } + state.forceStringNoCtx(nameVal); + Symbol nameSym = state.symbols.Create(nameVal.string.s); + Bindings::iterator j = value.attrs->find(nameSym); + if (j != value.attrs->end()) { + throwEvalError("dynamic attribute '%1%' at %2% already defined at %3%", + nameSym, i.pos, *j->second.pos); + } + + value.attrs->push_back( + Attr(nameSym, i.valueExpr->maybeThunk(state, *dynamicEnv), &i.pos)); + } +} + +void ExprLet::eval(EvalState& state, Env& env, Value& v) { + /* Create a new environment that contains the attributes in this + `let'. */ + Env& env2(state.allocEnv(attrs->attrs.size())); + env2.up = &env; + + /* The recursive attributes are evaluated in the new environment, + while the inherited attributes are evaluated in the original + environment. */ + size_t displ = 0; + for (auto& i : attrs->attrs) { + env2.values[displ++] = + i.second.e->maybeThunk(state, i.second.inherited ? env : env2); + } + + body->eval(state, env2, v); +} + +void ExprList::eval(EvalState& state, Env& env, Value& v) { + state.mkList(v, elems.size()); + for (size_t n = 0; n < elems.size(); ++n) { + (*v.list)[n] = elems[n]->maybeThunk(state, env); + } +} + +void ExprVar::eval(EvalState& state, Env& env, Value& v) { + Value* v2 = state.lookupVar(&env, *this, false); + state.forceValue(*v2, pos); + v = *v2; +} + +static 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; + } + out << getName(i, state, env); + } + return out.str(); +} + +uint64_t nrLookups = 0; + +void ExprSelect::eval(EvalState& state, Env& env, Value& v) { + Value vTmp; + Pos* pos2 = nullptr; + Value* vAttrs = &vTmp; + + e->eval(state, env, vTmp); + + try { + for (auto& i : attrPath) { + nrLookups++; + Bindings::iterator j; + Symbol name = getName(i, state, env); + if (def != nullptr) { + state.forceValue(*vAttrs, pos); + if (vAttrs->type != tAttrs || + (j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) { + def->eval(state, env, v); + return; + } + } else { + state.forceAttrs(*vAttrs, pos); + if ((j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) { + throwEvalError("attribute '%1%' missing, at %2%", name, pos); + } + } + vAttrs = j->second.value; + pos2 = j->second.pos; + if (state.countCalls && (pos2 != nullptr)) { + state.attrSelects[*pos2]++; + } + } + + state.forceValue(*vAttrs, (pos2 != nullptr ? *pos2 : this->pos)); + + } catch (Error& e) { + // This code relies on 'sDerivationNix' being correcty mutated at + // some prior point (it would previously otherwise have been a + // nullptr). + // + // We haven't seen this fail, so for now the contained value is + // just accessed at the risk of potentially crashing. + if ((pos2 != nullptr) && pos2->file != state.sDerivationNix.value()) { + addErrorPrefix(e, "while evaluating the attribute '%1%' at %2%:\n", + showAttrPath(state, env, attrPath), *pos2); + } + throw; + } + + v = *vAttrs; +} + +void ExprOpHasAttr::eval(EvalState& state, Env& env, Value& v) { + Value vTmp; + Value* vAttrs = &vTmp; + + e->eval(state, env, vTmp); + + for (auto& i : attrPath) { + state.forceValue(*vAttrs); + Bindings::iterator j; + Symbol name = getName(i, state, env); + if (vAttrs->type != tAttrs || + (j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) { + mkBool(v, false); + return; + } + vAttrs = j->second.value; + } + + mkBool(v, true); +} + +void ExprLambda::eval(EvalState& state, Env& env, Value& v) { + v.type = tLambda; + v.lambda.env = &env; + v.lambda.fun = this; +} + +void ExprApp::eval(EvalState& state, Env& env, Value& v) { + /* FIXME: vFun prevents GCC from doing tail call optimisation. */ + Value vFun; + e1->eval(state, env, vFun); + state.callFunction(vFun, *(e2->maybeThunk(state, env)), v, pos); +} + +void EvalState::callPrimOp(Value& fun, Value& arg, Value& v, const Pos& pos) { + /* Figure out the number of arguments still needed. */ + size_t argsDone = 0; + Value* primOp = &fun; + while (primOp->type == tPrimOpApp) { + argsDone++; + primOp = primOp->primOpApp.left; + } + assert(primOp->type == tPrimOp); + auto arity = primOp->primOp->arity; + auto argsLeft = arity - argsDone; + + if (argsLeft == 1) { + /* We have all the arguments, so call the primop. */ + + /* Put all the arguments in an array. */ + Value* vArgs[arity]; + auto n = arity - 1; + vArgs[n--] = &arg; + for (Value* arg = &fun; arg->type == tPrimOpApp; + arg = arg->primOpApp.left) { + vArgs[n--] = arg->primOpApp.right; + } + + /* And call the primop. */ + nrPrimOpCalls++; + if (countCalls) { + primOpCalls[primOp->primOp->name]++; + } + primOp->primOp->fun(*this, pos, vArgs, v); + } else { + Value* fun2 = allocValue(); + *fun2 = fun; + v.type = tPrimOpApp; + v.primOpApp.left = fun2; + v.primOpApp.right = &arg; + } +} + +void EvalState::callFunction(Value& fun, Value& arg, Value& v, const Pos& pos) { + auto trace = evalSettings.traceFunctionCalls + ? std::make_unique<FunctionCallTrace>(pos) + : nullptr; + + forceValue(fun, pos); + + if (fun.type == tPrimOp || fun.type == tPrimOpApp) { + callPrimOp(fun, arg, v, pos); + return; + } + + // If the value to be called is an attribute set, check whether it + // contains an appropriate function in the '__functor' element and + // use that. + if (fun.type == tAttrs) { + auto found = fun.attrs->find(sFunctor); + if (found != fun.attrs->end()) { + // fun 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 + auto& fun2 = *allocValue(); + fun2 = fun; + /* !!! Should we use the attr pos here? */ + Value v2; + // functors are called with the element itself as the first + // parameter, which is partially applied here + callFunction(*found->second.value, fun2, v2, pos); + return callFunction(v2, arg, v, pos); + } + } + + if (fun.type != tLambda) { + throwTypeError( + "attempt to call something which is not a function but %1%, at %2%", + fun, pos); + } + + ExprLambda& lambda(*fun.lambda.fun); + + auto size = (lambda.arg.empty() ? 0 : 1) + + (lambda.matchAttrs ? lambda.formals->formals.size() : 0); + Env& env2(allocEnv(size)); + env2.up = fun.lambda.env; + + size_t displ = 0; + + if (!lambda.matchAttrs) { + env2.values[displ++] = &arg; + + } else { + forceAttrs(arg, pos); + + if (!lambda.arg.empty()) { + 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. */ + size_t attrsUsed = 0; + for (auto& i : lambda.formals->formals) { + Bindings::iterator j = arg.attrs->find(i.name); + if (j == arg.attrs->end()) { + if (i.def == nullptr) { + throwTypeError("%1% called without required argument '%2%', at %3%", + lambda, i.name, pos); + } + env2.values[displ++] = i.def->maybeThunk(*this, env2); + } else { + attrsUsed++; + env2.values[displ++] = j->second.value; + } + } + + /* Check that each actual argument is listed as a formal + argument (unless the attribute match specifies a `...'). */ + if (!lambda.formals->ellipsis && attrsUsed != arg.attrs->size()) { + /* Nope, so show the first unexpected argument to the + user. */ + for (auto& i : *arg.attrs) { + if (lambda.formals->argNames.find(i.second.name) == + lambda.formals->argNames.end()) { + throwTypeError("%1% called with unexpected argument '%2%', at %3%", + lambda, i.second.name, pos); + } + } + abort(); // shouldn't happen + } + } + + nrFunctionCalls++; + if (countCalls) { + incrFunctionCall(&lambda); + } + + /* Evaluate the body. This is conditional on showTrace, because + catching exceptions makes this function not tail-recursive. */ + if (settings.showTrace) { + try { + lambda.body->eval(*this, env2, v); + } catch (Error& e) { + addErrorPrefix(e, "while evaluating %1%, called from %2%:\n", lambda, + pos); + throw; + } + } else { + fun.lambda.fun->body->eval(*this, env2, v); + } +} + +// Lifted out of callFunction() because it creates a temporary that +// prevents tail-call optimisation. +void EvalState::incrFunctionCall(ExprLambda* fun) { functionCalls[fun]++; } + +void EvalState::autoCallFunction(Bindings* args, Value& fun, Value& res) { + forceValue(fun); + + if (fun.type == tAttrs) { + auto found = fun.attrs->find(sFunctor); + if (found != fun.attrs->end()) { + Value* v = allocValue(); + callFunction(*found->second.value, fun, *v, noPos); + forceValue(*v); + return autoCallFunction(args, *v, res); + } + } + + if (fun.type != tLambda || !fun.lambda.fun->matchAttrs) { + res = fun; + return; + } + + Value* actualArgs = allocValue(); + mkAttrs(*actualArgs, fun.lambda.fun->formals->formals.size()); + + if (fun.lambda.fun->formals->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) { + actualArgs->attrs->push_back(v); + } + } else { + // Otherwise, only pass the arguments that the function accepts + for (auto& i : fun.lambda.fun->formals->formals) { + Bindings::iterator j = args->find(i.name); + if (j != args->end()) { + actualArgs->attrs->push_back(j->second); + } else if (i.def == nullptr) { + throwTypeError( + "cannot auto-call a function that has an argument without a " + "default " + "value ('%1%')", + i.name); + } + } + } + + callFunction(fun, *actualArgs, res, noPos); +} + +void ExprWith::eval(EvalState& state, Env& env, Value& v) { + Env& env2(state.allocEnv(1)); + env2.up = &env; + env2.prevWith = prevWith; + env2.type = Env::HasWithExpr; + /* placeholder for result of attrs */ + env2.values[0] = nullptr; + env2.withAttrsExpr = this->attrs; + + body->eval(state, env2, v); +} + +void ExprIf::eval(EvalState& state, Env& env, Value& v) { + (state.evalBool(env, cond) ? then : else_)->eval(state, env, v); +} + +void ExprAssert::eval(EvalState& state, Env& env, Value& v) { + if (!state.evalBool(env, cond, pos)) { + std::ostringstream out; + cond->show(out); + throwAssertionError("assertion %1% failed at %2%", out.str(), pos); + } + body->eval(state, env, v); +} + +void ExprOpNot::eval(EvalState& state, Env& env, Value& v) { + mkBool(v, !state.evalBool(env, e)); +} + +void ExprOpEq::eval(EvalState& state, Env& env, Value& v) { + Value v1; + e1->eval(state, env, v1); + Value v2; + e2->eval(state, env, v2); + mkBool(v, state.eqValues(v1, v2)); +} + +void ExprOpNEq::eval(EvalState& state, Env& env, Value& v) { + Value v1; + e1->eval(state, env, v1); + Value v2; + e2->eval(state, env, v2); + mkBool(v, !state.eqValues(v1, v2)); +} + +void ExprOpAnd::eval(EvalState& state, Env& env, Value& v) { + mkBool(v, state.evalBool(env, e1, pos) && state.evalBool(env, e2, pos)); +} + +void ExprOpOr::eval(EvalState& state, Env& env, Value& v) { + mkBool(v, state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos)); +} + +void ExprOpImpl::eval(EvalState& state, Env& env, Value& v) { + mkBool(v, !state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos)); +} + +void ExprOpUpdate::eval(EvalState& state, Env& env, Value& dest) { + Value v1; + Value v2; + state.evalAttrs(env, e1, v1); + state.evalAttrs(env, e2, v2); + + state.nrOpUpdates++; + + clearValue(dest); + dest.type = tAttrs; + dest.attrs = Bindings::Merge(*v1.attrs, *v2.attrs); +} + +void ExprOpConcatLists::eval(EvalState& state, Env& env, Value& v) { + Value v1; + e1->eval(state, env, v1); + Value v2; + e2->eval(state, env, v2); + state.concatLists(v, {&v1, &v2}, pos); +} + +void EvalState::concatLists(Value& v, const NixList& lists, const Pos& pos) { + nrListConcats++; + + auto outlist = std::make_shared<NixList>(); + + for (Value* list : lists) { + forceList(*list, pos); + outlist->insert(outlist->end(), list->list->begin(), list->list->end()); + } + + mkList(v, outlist); +} + +void ExprConcatStrings::eval(EvalState& state, Env& env, Value& v) { + PathSet context; + std::ostringstream s; + NixInt n = 0; + NixFloat nf = 0; + + bool first = !forceString; + ValueType firstType = tString; + + for (auto& i : *es) { + Value vTmp; + i->eval(state, env, vTmp); + + /* If the first element is a path, then the result will also + be a path, we don't copy anything (yet - that's done later, + since paths are copied when they are used in a derivation), + and none of the strings are allowed to have contexts. */ + if (first) { + firstType = vTmp.type; + first = false; + } + + if (firstType == tInt) { + if (vTmp.type == tInt) { + n += vTmp.integer; + } else if (vTmp.type == tFloat) { + // Upgrade the type from int to float; + firstType = tFloat; + nf = n; + nf += vTmp.fpoint; + } else { + throwEvalError("cannot add %1% to an integer, at %2%", showType(vTmp), + pos); + } + } else if (firstType == tFloat) { + if (vTmp.type == tInt) { + nf += vTmp.integer; + } else if (vTmp.type == tFloat) { + nf += vTmp.fpoint; + } else { + throwEvalError("cannot add %1% to a float, at %2%", showType(vTmp), + pos); + } + } else { + s << state.coerceToString(pos, vTmp, context, false, + firstType == tString); + } + } + + if (firstType == tInt) { + mkInt(v, n); + } else if (firstType == tFloat) { + mkFloat(v, nf); + } else if (firstType == tPath) { + if (!context.empty()) { + throwEvalError( + "a string that refers to a store path cannot be appended to a path, " + "at %1%", + pos); + } + auto path = canonPath(s.str()); + mkPath(v, path.c_str()); + } else { + mkString(v, s.str(), context); + } +} + +void ExprPos::eval(EvalState& state, Env& env, Value& v) { + state.mkPos(v, &pos); +} + +template <typename T> +using traceable_flat_hash_set = absl::flat_hash_set<T>; + +void EvalState::forceValueDeep(Value& v) { + traceable_flat_hash_set<const Value*> seen; + + std::function<void(Value & v)> recurse; + + recurse = [&](Value& v) { + if (seen.find(&v) != seen.end()) { + return; + } + seen.insert(&v); + + forceValue(v); + + if (v.type == tAttrs) { + for (auto& i : *v.attrs) { + try { + recurse(*i.second.value); + } catch (Error& e) { + addErrorPrefix(e, "while evaluating the attribute '%1%' at %2%:\n", + i.second.name, *i.second.pos); + throw; + } + } + } else if (v.isList()) { + for (size_t n = 0; n < v.listSize(); ++n) { + recurse(*(*v.list)[n]); + } + } + }; + + recurse(v); +} + +NixInt EvalState::forceInt(Value& v, const Pos& pos) { + forceValue(v, pos); + if (v.type != tInt) { + throwTypeError("value is %1% while an integer was expected, at %2%", v, + pos); + } + return v.integer; +} + +NixFloat EvalState::forceFloat(Value& v, const Pos& pos) { + forceValue(v, pos); + if (v.type == tInt) { + return static_cast<NixFloat>(v.integer); + } + if (v.type != tFloat) { + throwTypeError("value is %1% while a float was expected, at %2%", v, pos); + } + return v.fpoint; +} + +bool EvalState::forceBool(Value& v, const Pos& pos) { + forceValue(v); + if (v.type != tBool) { + throwTypeError("value is %1% while a Boolean was expected, at %2%", v, pos); + } + return v.boolean; +} + +bool EvalState::isFunctor(Value& fun) { + return fun.type == tAttrs && fun.attrs->find(sFunctor) != fun.attrs->end(); +} + +void EvalState::forceFunction(Value& v, const Pos& pos) { + forceValue(v); + if (v.type != tLambda && v.type != tPrimOp && v.type != tPrimOpApp && + !isFunctor(v)) { + throwTypeError("value is %1% while a function was expected, at %2%", v, + pos); + } +} + +std::string EvalState::forceString(Value& v, const Pos& pos) { + forceValue(v, pos); + if (v.type != tString) { + if (pos) { + throwTypeError("value is %1% while a string was expected, at %2%", v, + pos); + } else { + throwTypeError("value is %1% while a string was expected", v); + } + } + return std::string(v.string.s); +} + +void copyContext(const Value& v, PathSet& context) { + if (v.string.context != nullptr) { + for (const char** p = v.string.context; *p != nullptr; ++p) { + context.insert(*p); + } + } +} + +std::string EvalState::forceString(Value& v, PathSet& context, const Pos& pos) { + std::string s = forceString(v, pos); + copyContext(v, context); + return s; +} + +std::string EvalState::forceStringNoCtx(Value& v, const Pos& pos) { + std::string s = forceString(v, pos); + if (v.string.context != nullptr) { + if (pos) { + throwEvalError( + "the string '%1%' is not allowed to refer to a store path (such as " + "'%2%'), at %3%", + v.string.s, v.string.context[0], pos); + } else { + throwEvalError( + "the string '%1%' is not allowed to refer to a store path (such as " + "'%2%')", + v.string.s, v.string.context[0]); + } + } + return s; +} + +bool EvalState::isDerivation(Value& v) { + if (v.type != tAttrs) { + return false; + } + Bindings::iterator i = v.attrs->find(sType); + if (i == v.attrs->end()) { + return false; + } + forceValue(*i->second.value); + if (i->second.value->type != tString) { + return false; + } + return strcmp(i->second.value->string.s, "derivation") == 0; +} + +std::optional<std::string> EvalState::tryAttrsToString(const Pos& pos, Value& v, + PathSet& context, + bool coerceMore, + bool copyToStore) { + auto i = v.attrs->find(sToString); + if (i != v.attrs->end()) { + Value v1; + callFunction(*i->second.value, v, v1, pos); + return coerceToString(pos, v1, context, coerceMore, copyToStore); + } + + return {}; +} + +std::string EvalState::coerceToString(const Pos& pos, Value& v, + PathSet& context, bool coerceMore, + bool copyToStore) { + forceValue(v); + + std::string s; + + if (v.type == tString) { + copyContext(v, context); + return v.string.s; + } + + if (v.type == tPath) { + Path path(canonPath(v.path)); + return copyToStore ? copyPathToStore(context, path) : path; + } + + if (v.type == tAttrs) { + auto maybeString = + tryAttrsToString(pos, v, context, coerceMore, copyToStore); + if (maybeString) { + return *maybeString; + } + auto i = v.attrs->find(sOutPath); + if (i == v.attrs->end()) { + throwTypeError("cannot coerce a set to a string, at %1%", pos); + } + return coerceToString(pos, *i->second.value, context, coerceMore, + copyToStore); + } + + if (coerceMore) { + /* Note that `false' is represented as an empty string for + shell scripting convenience, just like `null'. */ + if (v.type == tBool && v.boolean) { + return "1"; + } + if (v.type == tBool && !v.boolean) { + return ""; + } + if (v.type == tInt) { + return std::to_string(v.integer); + } + if (v.type == tFloat) { + return std::to_string(v.fpoint); + } + if (v.type == tNull) { + return ""; + } + + if (v.isList()) { + std::string result; + for (size_t n = 0; n < v.listSize(); ++n) { + result += coerceToString(pos, *(*v.list)[n], context, coerceMore, + copyToStore); + if (n < v.listSize() - 1 + /* !!! not quite correct */ + && (!(*v.list)[n]->isList() || (*v.list)[n]->listSize() != 0)) { + result += " "; + } + } + return result; + } + } + + throwTypeError("cannot coerce %1% to a string, at %2%", v, pos); +} + +std::string EvalState::copyPathToStore(PathSet& context, const Path& path) { + if (nix::isDerivation(path)) { + throwEvalError("file names are not allowed to end in '%1%'", drvExtension); + } + + Path dstPath; + if (!srcToStore[path].empty()) { + dstPath = srcToStore[path]; + } else { + dstPath = + settings.readOnlyMode + ? store + ->computeStorePathForPath(baseNameOf(path), + checkSourcePath(path)) + .first + : store->addToStore(baseNameOf(path), checkSourcePath(path), true, + htSHA256, defaultPathFilter, repair); + srcToStore[path] = dstPath; + VLOG(2) << "copied source '" << path << "' -> '" << dstPath << "'"; + } + + context.insert(dstPath); + return dstPath; +} + +Path EvalState::coerceToPath(const Pos& pos, Value& v, PathSet& context) { + std::string path = coerceToString(pos, v, context, false, false); + if (path.empty() || path[0] != '/') { + throwEvalError("string '%1%' doesn't represent an absolute path, at %2%", + path, pos); + } + return path; +} + +bool EvalState::eqValues(Value& v1, Value& v2) { + forceValue(v1); + forceValue(v2); + + /* !!! 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. */ + if (&v1 == &v2) { + return true; + } + + // Special case type-compatibility between float and int + if (v1.type == tInt && v2.type == tFloat) { + return v1.integer == v2.fpoint; + } + if (v1.type == tFloat && v2.type == tInt) { + return v1.fpoint == v2.integer; + } + + // All other types are not compatible with each other. + if (v1.type != v2.type) { + return false; + } + + switch (v1.type) { + case tInt: + return v1.integer == v2.integer; + + case tBool: + return v1.boolean == v2.boolean; + + case tString: + return strcmp(v1.string.s, v2.string.s) == 0; + + case tPath: + return strcmp(v1.path, v2.path) == 0; + + case tNull: + return true; + + case tList: + if (v1.listSize() != v2.listSize()) { + return false; + } + for (size_t n = 0; n < v1.listSize(); ++n) { + if (!eqValues(*(*v1.list)[n], *(*v2.list)[n])) { + return false; + } + } + return true; + + case tAttrs: { + // As an optimisation if both values are pointing towards the + // same attribute set, we can skip all this extra work. + if (v1.attrs == v2.attrs) { + return true; + } + + /* If both sets denote a derivation (type = "derivation"), + then compare their outPaths. */ + if (isDerivation(v1) && isDerivation(v2)) { + Bindings::iterator i = v1.attrs->find(sOutPath); + Bindings::iterator j = v2.attrs->find(sOutPath); + if (i != v1.attrs->end() && j != v2.attrs->end()) { + return eqValues(*i->second.value, *j->second.value); + } + } + + return v1.attrs->Equal(v2.attrs.get(), *this); + } + + /* Functions are incomparable. */ + case tLambda: + case tPrimOp: + case tPrimOpApp: + return false; + + case tFloat: + return v1.fpoint == v2.fpoint; + + default: + throwEvalError("cannot compare %1% with %2%", showType(v1), showType(v2)); + } +} + +void EvalState::printStats() { + bool showStats = getEnv("NIX_SHOW_STATS").value_or("0") != "0"; + + struct rusage buf; + getrusage(RUSAGE_SELF, &buf); + float cpuTime = buf.ru_utime.tv_sec + + (static_cast<float>(buf.ru_utime.tv_usec) / 1000000); + + uint64_t bEnvs = nrEnvs * sizeof(Env) + nrValuesInEnvs * sizeof(Value*); + uint64_t bLists = nrListElems * sizeof(Value*); + uint64_t bValues = nrValues * sizeof(Value); + uint64_t bAttrsets = + nrAttrsets * sizeof(Bindings) + nrAttrsInAttrsets * sizeof(Attr); + + if (showStats) { + auto outPath = getEnv("NIX_SHOW_STATS_PATH").value_or("-"); + std::fstream fs; + if (outPath != "-") { + fs.open(outPath, std::fstream::out); + } + JSONObject topObj(outPath == "-" ? std::cerr : fs, true); + topObj.attr("cpuTime", cpuTime); + { + auto envs = topObj.object("envs"); + envs.attr("number", nrEnvs); + envs.attr("elements", nrValuesInEnvs); + envs.attr("bytes", bEnvs); + } + { + auto lists = topObj.object("list"); + lists.attr("elements", nrListElems); + lists.attr("bytes", bLists); + lists.attr("concats", nrListConcats); + } + { + auto values = topObj.object("values"); + values.attr("number", nrValues); + values.attr("bytes", bValues); + } + { + auto syms = topObj.object("symbols"); + syms.attr("number", symbols.Size()); + syms.attr("bytes", symbols.TotalSize()); + } + { + auto sets = topObj.object("sets"); + sets.attr("number", nrAttrsets); + sets.attr("bytes", bAttrsets); + sets.attr("elements", nrAttrsInAttrsets); + } + { + auto sizes = topObj.object("sizes"); + sizes.attr("Env", sizeof(Env)); + sizes.attr("Value", sizeof(Value)); + sizes.attr("Bindings", sizeof(Bindings)); + sizes.attr("Attr", sizeof(Attr)); + } + topObj.attr("nrOpUpdates", nrOpUpdates); + topObj.attr("nrOpUpdateValuesCopied", nrOpUpdateValuesCopied); + topObj.attr("nrThunks", nrThunks); + topObj.attr("nrAvoided", nrAvoided); + topObj.attr("nrLookups", nrLookups); + topObj.attr("nrPrimOpCalls", nrPrimOpCalls); + topObj.attr("nrFunctionCalls", nrFunctionCalls); + + if (countCalls) { + { + auto obj = topObj.object("primops"); + for (auto& i : primOpCalls) { + obj.attr(i.first, i.second); + } + } + { + auto list = topObj.list("functions"); + for (auto& i : functionCalls) { + auto obj = list.object(); + if (i.first->name.has_value()) { + obj.attr("name", (const std::string&)i.first->name.value()); + } else { + obj.attr("name", nullptr); + } + if (i.first->pos) { + obj.attr("file", (const std::string&)i.first->pos.file); + obj.attr("line", i.first->pos.line); + obj.attr("column", i.first->pos.column); + } + obj.attr("count", i.second); + } + } + { + auto list = topObj.list("attributes"); + for (auto& i : attrSelects) { + auto obj = list.object(); + if (i.first) { + obj.attr("file", (const std::string&)i.first.file); + obj.attr("line", i.first.line); + obj.attr("column", i.first.column); + } + obj.attr("count", i.second); + } + } + } + + // TODO(tazjin): what is this? commented out because .dump() is gone. + // if (getEnv("NIX_SHOW_SYMBOLS", "0") != "0") { + // auto list = topObj.list("symbols"); + // symbols.dump([&](const std::string& s) { list.elem(s); }); + // } + } +} + +void EvalState::TraceFileAccess(const Path& realPath) { + if (file_access_trace_fn) { + if (last_traced_file != realPath) { + file_access_trace_fn(realPath); + // Basic deduplication. + last_traced_file = std::string(realPath); + } + } +} + +void EvalState::EnableFileAccessTracing(std::function<void(const Path&)> fn) { + file_access_trace_fn = fn; +} + +size_t valueSize(const Value& v) { + traceable_flat_hash_set<const Bindings*> seenBindings; + traceable_flat_hash_set<const Env*> seenEnvs; + traceable_flat_hash_set<const NixList*> seenLists; + traceable_flat_hash_set<const char*> seenStrings; + traceable_flat_hash_set<const Value*> seenValues; + + auto doString = [&](const char* s) -> size_t { + if (seenStrings.find(s) != seenStrings.end()) { + return 0; + } + seenStrings.insert(s); + return strlen(s) + 1; + }; + + std::function<size_t(const Value& v)> doValue; + std::function<size_t(const Env& v)> doEnv; + + doValue = [&](const Value& v) -> size_t { + if (seenValues.find(&v) != seenValues.end()) { + return 0; + } + seenValues.insert(&v); + + size_t sz = sizeof(Value); + + switch (v.type) { + case tString: + sz += doString(v.string.s); + if (v.string.context != nullptr) { + for (const char** p = v.string.context; *p != nullptr; ++p) { + sz += doString(*p); + } + } + break; + case tPath: + sz += doString(v.path); + break; + case tAttrs: + if (seenBindings.find(v.attrs.get()) == seenBindings.end()) { + seenBindings.insert(v.attrs.get()); + sz += sizeof(Bindings); + for (const auto& i : *v.attrs) { + sz += doValue(*i.second.value); + } + } + break; + case tList: + if (seenLists.find(v.list.get()) == seenLists.end()) { + seenLists.insert(v.list.get()); + sz += v.listSize() * sizeof(Value*); + for (const Value* v : *v.list) { + sz += doValue(*v); + } + } + break; + case tThunk: + sz += doEnv(*v.thunk.env); + break; + case tApp: + sz += doValue(*v.app.left); + sz += doValue(*v.app.right); + break; + case tLambda: + sz += doEnv(*v.lambda.env); + break; + case tPrimOpApp: + sz += doValue(*v.primOpApp.left); + sz += doValue(*v.primOpApp.right); + break; + default:; + } + + return sz; + }; + + doEnv = [&](const Env& env) -> size_t { + if (seenEnvs.find(&env) != seenEnvs.end()) { + return 0; + } + seenEnvs.insert(&env); + + size_t sz = sizeof(Env) + sizeof(Value*) * env.size; + + if (env.type != Env::HasWithExpr) { + for (const Value* v : env.values) { + if (v != nullptr) { + sz += doValue(*v); + } + } + } else { + // TODO(kanepyork): trace ExprWith? how important is this accounting? + } + + if (env.up != nullptr) { + sz += doEnv(*env.up); + } + + return sz; + }; + + return doValue(v); +} + +EvalSettings evalSettings; + +static GlobalConfig::Register r1(&evalSettings); + +} // namespace nix |