about summary refs log tree commit diff
path: root/third_party/nix/src/libexpr/eval.cc
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/nix/src/libexpr/eval.cc')
-rw-r--r--third_party/nix/src/libexpr/eval.cc1878
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 0000000000..682ea64832
--- /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