#include "lix/libexpr/eval.hh"
#include "lix/libexpr/eval-settings.hh"
#include "lix/libstore/path.hh"
#include "lix/libutil/archive.hh"
#include "lix/libutil/ansicolor.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/english.hh"
#include "lix/libutil/fmt.hh"
#include "lix/libexpr/primops.hh"
#include "lix/libexpr/print-options.hh"
#include "lix/libmain/shared.hh"
#include "lix/libutil/suggestions.hh"
#include "lix/libutil/types.hh"
#include "lix/libstore/store-api.hh"
#include "lix/libstore/derivations.hh"
#include "lix/libexpr/gc-alloc.hh"
#include "lix/libstore/filetransfer.hh"
#include "lix/libexpr/function-trace.hh"
#include "lix/libstore/profiles.hh"
#include "lix/libexpr/print.hh"
#include "lix/libexpr/gc-small-vector.hh"
#include "lix/libfetchers/fetch-to-store.hh"
#include "lix/libexpr/flake/flakeref.hh"
#include "lix/libutil/exit.hh"
#include "lix/libutil/json.hh"
#include "symbol-table.hh"
#include "value.hh"

#include <algorithm>
#include <iostream>
#include <ostream>
#include <sstream>
#include <cstring>
#include <optional>
#include <string>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <fstream>
#include <functional>
#include <ranges>

#include <sys/resource.h>
#include <boost/container/small_vector.hpp>

// Ignore all internal signals boehm uses for parallel marking
// FIXME: Find out how to do this with LLDB for macOS!
#ifndef __APPLE__
[[gnu::section(".debug_gdb_scripts"), gnu::used, gnu::aligned(1)]]
// TODO: We should use `SECTION_SCRIPT_ID_PYTHON_TEXT` from
// `<gdb/section-scripts.h>` instead of hardcoding 4.
// But why isn't this header exported by GDB?
static const char printer_script[] =
    "\4"
    R"(lix-ignore-boehm-signals
import gdb
gdb.execute("handle SIGPWR SIGXCPU ignore")
)";
#endif

#if HAVE_BOEHMGC

#define GC_INCLUDE_NEW

#include <gc/gc.h>
#include <gc/gc_cpp.h>

#endif

namespace nix {

RootValue allocRootValue(Value v)
{
    return std::allocate_shared<Value>(TraceableAllocator<Value>(), v);
}

// Pretty print types for assertion errors
std::ostream & operator << (std::ostream & os, const ValueType t) {
    os << showType(t);
    return os;
}

std::string printValue(EvalState & state, Value & v)
{
    std::ostringstream out;
    v.print(state, out);
    return out.str();
}

std::string_view showType(ValueType type, bool withArticle)
{
    #define WA(a, w) withArticle ? a " " w : w
    switch (type) {
        case nInt: return WA("an", "integer");
        case nBool: return WA("a", "Boolean");
        case nString: return WA("a", "string");
        case nPath: return WA("a", "path");
        case nNull: return "null";
        case nAttrs: return WA("a", "set");
        case nList: return WA("a", "list");
        case nFunction: return WA("a", "function");
        case nExternal: return WA("an", "external value");
        case nFloat: return WA("a", "float");
        case nThunk: return WA("a", "thunk");
    }
    abort();
}


std::string showType(const Value & v)
{
    // Allow selecting a subset of enum values
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
    switch (v.internalType()) {
    case tString:
        return v.string().context ? "a string with context" : "a string";
    case tAuxiliary:
#pragma GCC diagnostic push
#pragma GCC diagnostic error "-Wswitch-enum"
        switch (v.auxiliary()->type()) {
        case Value::Acb::tExternal:
            return v.external()->showType();
        case Value::Acb::tFloat:
        case Value::Acb::tNull:
        case Value::Acb::tLambda:
        case Value::Acb::tInt:
            return std::string(showType(v.type()));
        case Value::Acb::tPrimOp:
            return fmt("the built-in function '%s'", v.primOp()->name);
        }
#pragma GCC diagnostic pop
    case tThunk:
        return v.isBlackhole() ? "a black hole" : "a thunk";
    case tApp:
        if (v.isPrimOpApp()) {
            return fmt(
                "the partially applied built-in function '%s'", v.app().target().primOp()->name
            );
        } else {
            return "a function application";
        }
    default:
        return std::string(showType(v.type()));
    }
#pragma GCC diagnostic pop
}


#if HAVE_BOEHMGC
/* Called when the Boehm GC runs out of memory. */
static void * oomHandler(size_t requested)
{
    /* Convert this to a proper C++ exception. */
    throw std::bad_alloc();
}

#endif


static Symbol getName(const AttrName & name, EvalState & state, Env & env)
{
    if (name.symbol) {
        return name.symbol;
    } else {
        Value nameValue;
        name.expr->eval(state, env, nameValue);
        state.forceStringNoCtx(nameValue, name.expr->getPos(), "while evaluating an attribute name");
        return state.ctx.symbols.create(nameValue.str());
    }
}

static bool libexprInitialised = false;

void initLibExpr()
{
    if (libexprInitialised) return;

#if HAVE_BOEHMGC
    /* Initialise the Boehm garbage collector. */

    /* Don't look for interior pointers. This reduces the odds of
       misdetection a bit. */
    GC_set_all_interior_pointers(0);
    for (int i = 1; i < 8; i++) {
        GC_REGISTER_DISPLACEMENT(i);
    }

    /* We don't have any roots in data segments, so don't scan from
       there. */
    GC_set_no_dls(1);

    GC_INIT();

    // Enable parallel marking
    GC_start_mark_threads();

    GC_set_oom_fn(oomHandler);

    /* Set the initial heap size to something fairly big (25% of
       physical RAM, up to a maximum of 384 MiB) so that in most cases
       we don't need to garbage collect at all.  (Collection has a
       fairly significant overhead.)  The heap size can be overridden
       through libgc's GC_INITIAL_HEAP_SIZE environment variable.  We
       should probably also provide a nix.conf setting for this.  Note
       that GC_expand_hp() causes a lot of virtual, but not physical
       (resident) memory to be allocated.  This might be a problem on
       systems that don't overcommit. */
    if (!getEnv("GC_INITIAL_HEAP_SIZE")) {
        int64_t size = 32 * 1024 * 1024;
#if HAVE_SYSCONF && defined(_SC_PAGESIZE) && defined(_SC_PHYS_PAGES)
        int64_t maxSize = 384 * 1024 * 1024;
        int64_t pageSize = sysconf(_SC_PAGESIZE);
        int64_t pages = sysconf(_SC_PHYS_PAGES);
        if (pageSize != -1) {
            size = (pageSize * pages) / 4; // 25% of RAM
        }
        if (size > maxSize) size = maxSize;
#endif
        debug("setting initial heap size to %1% bytes", size);
        GC_expand_hp(size);
    }

#endif

    fetchers::initLibFetchers();

    libexprInitialised = true;
}

StaticSymbols::StaticSymbols(SymbolTable & symbols)
    : outPath(symbols.create("outPath"))
    , drvPath(symbols.create("drvPath"))
    , type(symbols.create("type"))
    , meta(symbols.create("meta"))
    , name(symbols.create("name"))
    , value(symbols.create("value"))
    , system(symbols.create("system"))
    , overrides(symbols.create("__overrides"))
    , outputs(symbols.create("outputs"))
    , outputName(symbols.create("outputName"))
    , ignoreNulls(symbols.create("__ignoreNulls"))
    , file(symbols.create("file"))
    , line(symbols.create("line"))
    , column(symbols.create("column"))
    , functor(symbols.create("__functor"))
    , toString(symbols.create("__toString"))
    , right(symbols.create("right"))
    , wrong(symbols.create("wrong"))
    , structuredAttrs(symbols.create("__structuredAttrs"))
    , allowedReferences(symbols.create("allowedReferences"))
    , allowedRequisites(symbols.create("allowedRequisites"))
    , disallowedReferences(symbols.create("disallowedReferences"))
    , disallowedRequisites(symbols.create("disallowedRequisites"))
    , maxSize(symbols.create("maxSize"))
    , maxClosureSize(symbols.create("maxClosureSize"))
    , builder(symbols.create("builder"))
    , args(symbols.create("args"))
    , contentAddressed(symbols.create("__contentAddressed"))
    , impure(symbols.create("__impure"))
    , outputHash(symbols.create("outputHash"))
    , outputHashAlgo(symbols.create("outputHashAlgo"))
    , outputHashMode(symbols.create("outputHashMode"))
    , recurseForDerivations(symbols.create("recurseForDerivations"))
    , description(symbols.create("description"))
    , self(symbols.create("self"))
    , startSet(symbols.create("startSet"))
    , operator_(symbols.create("operator"))
    , key(symbols.create("key"))
    , path(symbols.create("path"))
    , prefix(symbols.create("prefix"))
    , outputSpecified(symbols.create("outputSpecified"))
    , exprSymbols{
        .sub = symbols.create("__sub"),
        .lessThan = symbols.create("__lessThan"),
        .mul = symbols.create("__mul"),
        .div = symbols.create("__div"),
        .or_ = symbols.create("or"),
        .findFile = symbols.create("__findFile"),
        .nixPath = symbols.create("__nixPath"),
        .body = symbols.create("body"),
        .overrides = symbols.create("__overrides"),
    }
{
}

EvalMemory::EvalMemory()
{
    assert(libexprInitialised);
#if HAVE_BOEHMGC
    GC_add_roots(gcCache, gcCache + CACHES);
#endif
}

EvalMemory::~EvalMemory()
{
#if HAVE_BOEHMGC
    GC_remove_roots(gcCache, gcCache + CACHES);
#endif
}

EvalBuiltins::EvalBuiltins(
    EvalMemory & mem,
    SymbolTable & symbols,
    const SearchPath & searchPath,
    const Path & storeDir,
    size_t size
)
    : mem(mem)
    , symbols(symbols)
    , env(mem.allocEnv(size))
    , staticEnv{std::make_shared<StaticEnv>(nullptr, nullptr)}
{
    createBaseEnv(searchPath, storeDir);
}

EvalPaths::EvalPaths(
    AsyncIoRoot & aio,
    const ref<Store> & store,
    SearchPath searchPath,
    EvalErrorContext & errors
)
    : store(store)
    , searchPath_(std::move(searchPath))
    , errors(errors)
{
    if (evalSettings.restrictEval || evalSettings.pureEval) {
        allowedPaths = AllowedPath{.allowAllChildren = false};

        for (auto & i : searchPath_.elements) {
            auto r = aio.blockOn(resolveSearchPathPath(i.path));
            if (!r) continue;

            auto path = std::move(*r);

            if (store->isInStore(path)) {
                try {
                    StorePathSet closure;
                    aio.blockOn(store->computeFSClosure(store->toStorePath(path).first, closure));
                    for (auto & path : closure)
                        allowPath(path);
                } catch (InvalidPath &) {
                    allowPath(path);
                }
            } else
                allowPath(path);
        }
    }
}

Evaluator::Evaluator(
    AsyncIoRoot & aio,
    const SearchPath & _searchPath,
    ref<Store> store,
    std::shared_ptr<Store> buildStore,
    std::function<ReplExitStatus(EvalState & es, ValMap const & extraEnv)> debugRepl
)
    : s(symbols)
    , paths(aio, store, [&] {
        SearchPath searchPath;
        if (!evalSettings.pureEval) {
            for (auto & i : _searchPath.elements)
                searchPath.elements.emplace_back(SearchPath::Elem {i});
            for (auto & i : evalSettings.nixPath.get())
                searchPath.elements.emplace_back(SearchPath::Elem::parse(i));
        }
        return searchPath;
    }(), errors)
    , builtins(mem, symbols, paths.searchPath(), store->config().storeDir)
    , repair(NoRepair)
    , store(store)
    , buildStore(buildStore ? ref<Store>::unsafeFromPtr(buildStore) : store)
    , debug{
          debugRepl ? std::make_unique<DebugState>(
              positions,
              symbols,
              [this, debugRepl](const ValMap & extraEnv, NeverAsync) {
                  return activeEval
                      ? debugRepl(*activeEval, extraEnv)
                      : ReplExitStatus::Continue;
              })
                    : nullptr
      }
    , errors{positions, debug.get()}
{
    stats.countCalls = getEnv("NIX_COUNT_CALLS").value_or("0") != "0";

    static_assert(sizeof(Env) <= 16, "environment must be <= 16 bytes");
}

box_ptr<EvalState> Evaluator::begin(AsyncIoRoot & aio)
{
    assert(!activeEval);
    return box_ptr<EvalState>::unsafeFromNonnull(
        std::unique_ptr<EvalState>(new EvalState(aio, *this))
    );
}

EvalState::EvalState(AsyncIoRoot & aio, Evaluator & ctx) : ctx(ctx), aio(aio)
{
    ctx.activeEval = this;
}

EvalState::~EvalState()
{
    ctx.activeEval = nullptr;
}


void EvalPaths::allowPath(const Path & path)
{
    if (!allowedPaths) {
        return;
    }

    CanonPath p(path);
    auto * level = &*allowedPaths;
    for (const auto & entry : p) {
        level = &level->children.emplace(std::piecewise_construct, std::tuple(entry), std::tuple())
                     .first->second;
    }
    level->allowAllChildren = true;
}

void EvalPaths::allowPath(const StorePath & storePath)
{
    if (allowedPaths)
        allowPath(store->toRealPath(storePath));
}

void EvalPaths::allowAndSetStorePathString(const StorePath & storePath, Value & v)
{
    allowPath(storePath);

    mkStorePathString(storePath, v);
}

CheckedSourcePath EvalPaths::checkSourcePath(const SourcePath & path_)
{
    if (!allowedPaths) return auto(path_).unsafeIntoChecked();

    auto i = resolvedPaths.find(path_.canonical().abs());
    if (i != resolvedPaths.end())
        return i->second;

    /* First canonicalize the path without symlinks, so we make sure an
     * attacker can't append ../../... to a path that would be in allowedPaths
     * and thus leak symlink targets.
     */
    const CanonPath abspath{path_.canonical().abs()};

    if (abspath.abs().starts_with(corepkgsPrefix)) {
        return SourcePath(std::move(abspath)).unsafeIntoChecked();
    }

    /* Resolve symlinks. This is mostly restricted copy of canonPath with
       resolveSymlinks=true, because we need access to intermediat paths. */
    debug("checking access to '%s'", abspath);

    /* Count the number of times we follow a symlink and stop at some
       arbitrary (but high) limit to prevent infinite loops. */
    unsigned int followCount = 0, maxFollow = 1024;

    std::optional<CanonPath> componentsBacking;
    std::vector<std::string_view> components(abspath.begin(), abspath.end());

retry:

    if (++followCount >= maxFollow) {
        throw Error("infinite symlink recursion in path '%1%'", path_);
    }

    // TODO: tests for this stuff
    const auto * level = &*allowedPaths;
    CheckedSourcePath current = SourcePath(CanonPath::root).unsafeIntoChecked();
    for (auto ct = components.begin(); ct != components.end(); ct++) {
        auto & p = *ct;
        // an empty level means all subpaths are allowed, propagate this forwards
        // by setting level=nullptr for the subsequent checks. a symlink will set
        // level to the "VFS" root and restart the check with a the resolved path
        if (level) {
            if (level->allowAllChildren) {
                level = nullptr;
            } else if (auto it = level->children.find(p); it != level->children.end()) {
                level = &it->second;
            } else {
                goto failed;
            }
        }
        auto next = (current + p).unsafeIntoChecked();
        auto st = next.maybeLstat();
        // resolve symlinks, treating nonexistant components like regular directories.
        // this mirrors canonPath behavior and is necessary for `builtins.pathExists`.
        if (st && st->type == InputAccessor::tSymlink) {
            auto target = next.readLink();
            auto levelResolved = target.starts_with("/")
                ? CanonPath(target)
                : CanonPath(current.canonical().abs() + "/" + target);
            for (ct++; ct != components.end(); ct++) {
                levelResolved.push(*ct);
            }
            components = {levelResolved.begin(), levelResolved.end()};
            componentsBacking = std::move(levelResolved);
            followCount += 1;
            goto retry;
        }
        current = std::move(next);
    }
    // Downstream users (e.g. `builtins.readDir` or `builtins.path`) will want to descend.
    if (level && !level->allowAllChildren) {
        goto failed;
    }

    resolvedPaths.insert_or_assign(path_.canonical().abs(), current);
    return current;

failed:
    auto modeInformation = evalSettings.pureEval
        ? "in pure eval mode (use '--impure' to override)"
        : "in restricted mode";
    throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", abspath, modeInformation);
}


void EvalPaths::checkURI(const std::string & uri)
{
    if (!evalSettings.restrictEval) return;

    /* 'uri' should be equal to a prefix, or in a subdirectory of a
       prefix. Thus, the prefix https://github.co does not permit
       access to https://github.com. Note: this allows 'http://' and
       'https://' as prefixes for any http/https URI. */
    for (auto & prefix : evalSettings.allowedUris.get())
        if (uri == prefix ||
            (uri.size() > prefix.size()
            && prefix.size() > 0
            && uri.starts_with(prefix)
            && (prefix[prefix.size() - 1] == '/' || uri[prefix.size()] == '/')))
            return;

    /* If the URI is a path, then check it against allowedPaths as
       well. */
    if (uri.starts_with("/")) {
        checkSourcePath(CanonPath(uri));
        return;
    }

    if (uri.starts_with("file://")) {
        checkSourcePath(CanonPath(std::string(uri, 7)));
        return;
    }

    throw RestrictedPathError("access to URI '%s' is forbidden in restricted mode", uri);
}


Path EvalPaths::toRealPath(const Path & path, const NixStringContext & context)
{
    // FIXME: check whether 'path' is in 'context'.
    return
        !context.empty() && store->isInStore(path)
        ? store->toRealPath(path)
        : path;
}

void EvalBuiltins::addConstant(const std::string & name, const Value & v, Constant info)
{
    auto name2 = name.substr(0, 2) == "__" ? name.substr(2) : name;

    constantInfos.push_back({name2, info});

    if (!(evalSettings.pureEval && info.impureOnly)) {
        /* Check the type, if possible.

           We might know the type of a thunk in advance, so be allowed
           to just write it down in that case. */
        if (auto gotType = v.type(true); gotType != nThunk) {
            assert(info.type == gotType);
        }

        /* Install value the base environment. */
        staticEnv->vars.insert_or_assign(symbols.create(name), baseEnvDispl);
        env.values[baseEnvDispl++] = v;
        env.values[0].attrs()->push_back(Attr(symbols.create(name2), v));
    }
}

std::ostream & operator<<(std::ostream & output, const PrimOp & primOp)
{
    output << "primop " << primOp.name;
    return output;
}

void EvalBuiltins::addPrimOp(PrimOpDetails primOp)
{
    /* Hack to make constants lazy: turn them into a application of
       the primop to a dummy value. */
    if (primOp.arity == 0) {
        primOp.arity = 1;
        Value vPrimOp{NewValueAs::primop, *new PrimOp(primOp)};
        Value v{NewValueAs::app, mem, vPrimOp, vPrimOp};
        addConstant(
            vPrimOp.primOp()->name,
            v,
            {
                .type = nFunction,
                .doc = vPrimOp.primOp()->doc,
            }
        );
    }

    auto envName = symbols.create(primOp.name);
    if (primOp.name.starts_with("__"))
        primOp.name = primOp.name.substr(2);

    Value v{NewValueAs::primop, *new PrimOp(std::move(primOp))};
    staticEnv->vars.insert_or_assign(auto(envName), baseEnvDispl);
    env.values[baseEnvDispl++] = v;
    env.values[0].attrs()->push_back(Attr(symbols.create(v.primOp()->name), v));
}


Value & EvalBuiltins::get(const std::string & name)
{
    return env.values[0].attrs()->get(symbols.create(name))->value;
}


std::optional<EvalBuiltins::Doc> EvalBuiltins::getDoc(Value & v)
{
    if (v.isPrimOp()) {
        auto v2 = &v;
        if (auto * doc = v2->primOp()->doc)
            return Doc {
                .pos = {},
                .name = v2->primOp()->name,
                .arity = v2->primOp()->arity,
                .args = v2->primOp()->args,
                .doc = doc,
            };
    }
    return {};
}


static std::set<std::string_view> sortedBindingNames(const SymbolTable & st, const StaticEnv & se)
{
    std::set<std::string_view> bindings;
    for (auto [symbol, displ] : se.vars)
        bindings.emplace(st[symbol]);
    return bindings;
}


// just for the current level of StaticEnv, not the whole chain.
void printStaticEnvBindings(const SymbolTable & st, const StaticEnv & se)
{
    std::cout << ANSI_MAGENTA;
    for (auto & i : sortedBindingNames(st, se))
        std::cout << i << " ";
    std::cout << ANSI_NORMAL;
    std::cout << std::endl;
}

// just for the current level of Env, not the whole chain.
void printWithBindings(const SymbolTable & st, const Env & env)
{
    if (env.values[0].type() == nAttrs) {
        std::set<std::string_view> bindings;
        for (const auto & attr : *env.values[0].attrs()) {
            bindings.emplace(st[attr.name]);
        }

        std::cout << "with: ";
        std::cout << ANSI_MAGENTA;
        for (auto & i : bindings)
            std::cout << i << " ";
        std::cout << ANSI_NORMAL;
        std::cout << std::endl;
    }
}

void printEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & env, int lvl)
{
    std::cout << "Env level " << lvl << std::endl;

    if (se.up && env.up) {
        std::cout << "static: ";
        printStaticEnvBindings(st, se);
        if (se.isWith)
            printWithBindings(st, env);
        std::cout << std::endl;
        printEnvBindings(st, *se.up, *env.up, ++lvl);
    } else {
        std::cout << ANSI_MAGENTA;
        // for the top level, don't print the double underscore ones;
        // they are in builtins.
        for (auto & i : sortedBindingNames(st, se))
            if (!i.starts_with("__"))
                std::cout << i << " ";
        std::cout << ANSI_NORMAL;
        std::cout << std::endl;
        if (se.isWith)
            printWithBindings(st, env);  // probably nothing there for the top level.
        std::cout << std::endl;

    }
}

void printEnvBindings(const EvalState &es, const Expr & expr, const Env & env)
{
    // just print the names for now
    auto se = es.ctx.debug->staticEnvFor(expr);
    if (se)
        printEnvBindings(es.ctx.symbols, *se, env, 0);
}

void mapStaticEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & env, ValMap & vm)
{
    // add bindings for the next level up first, so that the bindings for this level
    // override the higher levels.
    // The top level bindings (builtins) are skipped since they are added for us by initEnv()
    if (env.up && se.up) {
        mapStaticEnvBindings(st, *se.up, *env.up, vm);

        if (se.isWith && env.values[0].type() == nAttrs) {
            // add 'with' bindings.
            Bindings::iterator j = env.values[0].attrs()->begin();
            while (j != env.values[0].attrs()->end()) {
                vm[std::string(st[j->name])] = j->value;
                ++j;
            }
        } else {
            // iterate through staticenv bindings and add them.
            for (auto & i : se.vars)
                vm[std::string(st[i.first])] = env.values[i.second];
        }
    }
}

std::unique_ptr<ValMap> mapStaticEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & env)
{
    auto vm = std::make_unique<ValMap>();
    mapStaticEnvBindings(st, se, env, *vm);
    return vm;
}

/**
 * Sets `inDebugger` to true on construction and false on destruction.
 */
class DebuggerGuard {
    bool & inDebugger;
public:
    DebuggerGuard(bool & inDebugger) : inDebugger(inDebugger) {
        inDebugger = true;
    }
    ~DebuggerGuard() {
        inDebugger = false;
    }
};

void DebugState::onEvalError(
    const EvalError * error, const Env & env, const Expr & expr, NeverAsync
)
{
    // Make sure we have a debugger to run and we're not already in a debugger.
    if (inDebugger)
        return;

    auto dts =
        error && expr.getPos()
        ? addTrace(DebugTrace {
            .pos = error->info().pos ? error->info().pos : positions[expr.getPos()],
            .expr = expr,
            .env = env,
            .hint = error->info().msg,
            .isError = true
        })
        : nullptr;

    if (error)
    {
        printError("%s\n", Uncolored(error->what()));

        if (trylevel > 0 && error->info().level != lvlInfo) {
            if (evalSettings.ignoreExceptionsDuringTry) {
                printError("This exception occurred in a 'tryEval' call, " ANSI_RED "despite the use of " ANSI_GREEN "--ignore-try" ANSI_RED " to attempt to skip these" ANSI_NORMAL ". This is probably a bug. We would appreciate if you report it along with what caused it at https://git.lix.systems/lix-project/lix/issues.\n");
            } else {
                printError("This exception occurred in a 'tryEval' call. Use " ANSI_GREEN "--ignore-try" ANSI_NORMAL " to skip these.\n");
            }
        }
    }

    auto se = staticEnvFor(expr);
    if (se) {
        auto vm = mapStaticEnvBindings(symbols, *se.get(), env);
        DebuggerGuard _guard(inDebugger);
        auto exitStatus = errorCallback(*vm, {});
        switch (exitStatus) {
            case ReplExitStatus::QuitAll:
                if (error)
                    throw *error;
                throw Exit(0);
            case ReplExitStatus::Continue:
                break;
            default:
                abort();
        }
    }
}

DebugState::TraceFrame DebugState::addTrace(DebugTrace t)
{
    struct UnlinkDebugTrace
    {
        DebugState * state;
        void operator()(DebugTrace * trace)
        {
            state->latestTrace = trace->parent;
            delete trace;
        }
    };

    t.parent = latestTrace.lock();
    std::unique_ptr<DebugTrace, UnlinkDebugTrace> trace(
        new auto(std::move(t)), UnlinkDebugTrace{.state = this}
    );
    std::shared_ptr<DebugTrace> entry(std::move(trace));
    latestTrace = entry;
    return TraceFrame{entry};
}

template<typename... Args>
static DebugState::TraceFrame makeDebugTraceStacker(
    EvalState & state,
    Expr & expr,
    Env & env,
    std::shared_ptr<Pos> && pos,
    const Args & ... formatArgs)
{
    auto trace = state.ctx.debug->addTrace(DebugTrace{
        .pos = std::move(pos),
        .expr = expr,
        .env = env,
        .hint = HintFmt(formatArgs...),
        .isError = false,
    });
    if (state.ctx.debug->stop && state.ctx.debug->errorCallback)
        state.ctx.debug->onEvalError(nullptr, env, expr);
    return trace;
}

inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval)
{
    for (auto l = var.level; l; --l, env = env->up) ;

    if (!var.fromWith) {
        return &env->values[var.displ];
    }

    // This early exit defeats the `maybeThunk` optimization for variables from `with`,
    // The added complexity of handling this appears to be similarly in cost, or
    // the cases where applicable were insignificant in the first place.
    if (noEval) return nullptr;

    auto * fromWith = var.fromWith;
    while (1) {
        forceAttrs(
            env->values[0],
            fromWith->pos,
            "while evaluating the first subexpression of a with expression"
        );
        auto j = env->values[0].attrs()->get(var.name);
        if (j) {
            if (ctx.stats.countCalls) ctx.stats.attrSelects[j->pos]++;
            return &j->value;
        }
        if (!fromWith->parentWith)
            ctx.errors.make<UndefinedVarError>("undefined variable '%1%'", ctx.symbols[var.name]).atPos(var.pos).withFrame(*env, var).debugThrow();
        for (size_t l = fromWith->prevWith; l; --l, env = env->up) ;
        fromWith = fromWith->parentWith;
    }
}

Value::List * EvalMemory::newList(size_t size)
{
    auto list =
        reinterpret_cast<Value::List *>(allocBytes(sizeof(Value::List) + size * sizeof(Value *)));
    list->size = size;
    stats.nrListElems += size;
    return list;
}


void Evaluator::evalLazily(Expr & e, Value & v)
{
    v = {NewValueAs::thunk, mem, builtins.env, e};
    stats.nrThunks++;
}


void EvalState::mkPos(Value & v, PosIdx p)
{
    auto origin = ctx.positions.originOf(p);
    if (auto path = std::get_if<CheckedSourcePath>(&origin)) {
        auto attrs = ctx.buildBindings(3);
        attrs.alloc(ctx.s.file).mkString(path->to_string());
        makePositionThunks(*this, p, attrs.alloc(ctx.s.line), attrs.alloc(ctx.s.column));
        v.mkAttrs(attrs);
    } else
        v.mkNull();
}


void EvalPaths::mkStorePathString(const StorePath & p, Value & v)
{
    v.mkString(
        store->printStorePath(p),
        NixStringContext {
            NixStringContextElem::Opaque { .path = p },
        });
}


std::string EvalState::mkOutputStringRaw(
    const StorePath & staticOutputPath)
{
    return ctx.store->printStorePath(staticOutputPath);
}


void EvalState::mkOutputString(
    Value & value,
    const SingleDerivedPath::Built & b,
    const StorePath & staticOutputPath)
{
    value.mkString(mkOutputStringRaw(staticOutputPath), NixStringContext { b });
}


std::string EvalState::mkSingleDerivedPathStringRaw(
    const SingleDerivedPath & p)
{
    return std::visit(overloaded {
        [&](const SingleDerivedPath::Opaque & o) {
            return ctx.store->printStorePath(o.path);
        },
        [&](const SingleDerivedPath::Built & b) {
            auto drv = aio.blockOn(ctx.store->readDerivation(b.drvPath.path));
            auto i = drv.outputs.find(b.output);
            if (i == drv.outputs.end())
                throw Error("derivation '%s' does not have output '%s'", b.drvPath.to_string(*ctx.store), b.output);
            auto staticOutputPath = i->second.path(*ctx.store, drv.name, b.output);
            return mkOutputStringRaw(staticOutputPath);
        }
    }, p.raw());
}


void EvalState::mkSingleDerivedPathString(
    const SingleDerivedPath & p,
    Value & v)
{
    v.mkString(
        mkSingleDerivedPathStringRaw(p),
        NixStringContext {
            std::visit([](auto && v) -> NixStringContextElem { return v; }, p),
        });
}


/* 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)
{
    state.ctx.stats.nrThunks++;
    return {NewValueAs::thunk, state.ctx.mem, env, *this};
}

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 && !v->isInvalid()) {
        state.ctx.stats.nrAvoided++;
        return *v;
    }
    return Expr::maybeThunk(state, env);
}

Value ExprLiteral::maybeThunk(EvalState & state, Env & env)
{
    state.ctx.stats.nrAvoided++;
    return v;
}


struct CachedEvalFile
{
    Value result;
    explicit CachedEvalFile(Value result): result(result) {}
};

void EvalState::evalFile(const SourcePath & path_, Value & v)
{
    auto path = ctx.paths.checkSourcePath(path_);

    if (auto i = ctx.caches.fileEval.find(path); i != ctx.caches.fileEval.end()) {
        v = i->second->result;
        return;
    }

    auto resolvedPath = ctx.paths.resolveExprPath(path);
    if (auto i = ctx.caches.fileEval.find(resolvedPath); i != ctx.caches.fileEval.end()) {
        v = i->second->result;
        return;
    }

    debug("evaluating file '%1%'", resolvedPath);
    Expr & e = ctx.parseExprFromFile(ctx.paths.checkSourcePath(resolvedPath));

    try {
        auto dts = ctx.debug
            ? makeDebugTraceStacker(
                *this,
                e,
                ctx.builtins.env,
                e.getPos() ? std::make_shared<Pos>(ctx.positions[e.getPos()]) : nullptr,
                "while evaluating the file '%1%':", resolvedPath.to_string())
            : nullptr;

        eval(e, v);
    } catch (Error & e) {
        e.addTrace(nullptr, "while evaluating the file '%1%':", resolvedPath.to_string());
        throw;
    }

    auto cache = std::allocate_shared<CachedEvalFile>(TraceableAllocator<CachedEvalFile>(), v);
    ctx.caches.fileEval[resolvedPath] = cache;
    if (path != resolvedPath) ctx.caches.fileEval[path] = cache;
}


void EvalState::resetFileCache()
{
    ctx.caches.fileEval.clear();
}


void EvalState::eval(Expr & e, Value & v)
{
    e.eval(*this, ctx.builtins.env, v);
}

#define checkType(typeName, stringName) \
    if (v.type() != (typeName)) \
        ctx.errors.make<TypeError>( \
                "expected a %1% but found %2%: %3%", \
                Uncolored(stringName), \
                showType(v), \
                ValuePrinter(*this, v, errorPrintOptions) \
            ).atPos(e.getPos()).withFrame(env, e).debugThrow();

inline bool EvalState::evalBool(Env & env, Expr & e)
{
    Value v;
    e.eval(*this, env, v);
    checkType(nBool, "Boolean");
    return v.boolean();
}


inline void EvalState::evalAttrs(Env & env, Expr & e, Value & v)
{
    e.eval(*this, env, v);
    checkType(nAttrs, "set");
}

inline void EvalState::evalList(Env & env, Expr & e, Value & v)
{
    e.eval(*this, env, v);
    checkType(nList, "list");
}


void Expr::eval(EvalState & state, Env & env, Value & v)
{
    abort();
}


void ExprLiteral::eval(EvalState & state, Env & env, Value & v)
{
    v = this->v;
}

Env * ExprAttrs::buildInheritFromEnv(EvalState & state, Env & up)
{
    Env & inheritEnv = state.ctx.mem.allocEnv(inheritFromExprs->size());
    inheritEnv.up = &up;

    Displacement displ = 0;
    for (auto & from : *inheritFromExprs)
        inheritEnv.values[displ++] = from->maybeThunk(state, up);

    return &inheritEnv;
}

void ExprSet::eval(EvalState & state, Env & env, Value & v)
{
    Bindings::Size capacity = attrs.size() + dynamicAttrs.size();
    v.mkAttrs(state.ctx.buildBindings(capacity).finish());
    auto dynamicEnv = &env;

    if (recursive) {

        /* Create a new environment that contains the attributes in
           this `rec'. */
        Env & env2(state.ctx.mem.allocEnv(attrs.size()));
        env2.up = &env;
        dynamicEnv = &env2;
        Env * inheritEnv = inheritFromExprs ? buildInheritFromEnv(state, env2) : nullptr;

        ExprAttrs::AttrDefs::iterator overrides = attrs.find(state.ctx.s.overrides);
        bool hasOverrides = overrides != attrs.end();

        /* The recursive attributes are evaluated in the new
           environment, while the inherited attributes are evaluated
           in the original environment. */
        Displacement displ = 0;
        for (auto & i : attrs) {
            Value vAttr;
            if (hasOverrides && i.second.kind != ExprAttrs::AttrDef::Kind::Inherited) {
                vAttr = {
                    NewValueAs::thunk,
                    state.ctx.mem,
                    *i.second.chooseByKind(&env2, &env, inheritEnv),
                    *i.second.e
                };
                state.ctx.stats.nrThunks++;
            } else {
                vAttr =
                    i.second.e->maybeThunk(state, *i.second.chooseByKind(&env2, &env, inheritEnv));
            }
            env2.values[displ++] = vAttr;
            v.attrs()->push_back(Attr(i.first, vAttr, i.second.pos));
        }

        /* If the rec contains an attribute called `__overrides', then
           evaluate it, and add the attributes in that set to the rec.
           This allows overriding of recursive attributes, which is
           otherwise not possible.  (You can use the // operator to
           replace an attribute, but other attributes in the rec will
           still reference the original value, because that value has
           been substituted into the bodies of the other attributes.
           Hence we need __overrides.) */
        if (hasOverrides) {
            Value & vOverrides = (*v.attrs())[overrides->second.displ].value;
            state.forceAttrs(vOverrides, noPos, "while evaluating the `__overrides` attribute");
            Bindings * newBnds = state.ctx.mem.allocBindings(capacity + vOverrides.attrs()->size());
            for (auto & i : *v.attrs())
                newBnds->push_back(i);
            for (auto & i : *vOverrides.attrs()) {
                ExprAttrs::AttrDefs::iterator j = attrs.find(i.name);
                if (j != attrs.end()) {
                    (*newBnds)[j->second.displ] = i;
                    env2.values[j->second.displ] = i.value;
                } else
                    newBnds->push_back(i);
            }
            newBnds->sort();
            v.mkAttrs(newBnds);
        }
    }

    else {
        Env * inheritEnv = inheritFromExprs ? buildInheritFromEnv(state, env) : nullptr;
        for (auto & i : attrs) {
            v.attrs()->push_back(Attr(
                    i.first,
                    i.second.e->maybeThunk(state, *i.second.chooseByKind(&env, &env, inheritEnv)),
                    i.second.pos));
        }
    }

    /* Dynamic attrs apply *after* rec and __overrides. */
    for (auto & i : dynamicAttrs) {
        Value nameVal;
        i.nameExpr->eval(state, *dynamicEnv, nameVal);
        state.forceValue(nameVal, i.pos);
        if (nameVal.type() == nNull)
            continue;
        state.forceStringNoCtx(nameVal, i.pos, "while evaluating the name of a dynamic attribute");
        auto nameSym = state.ctx.symbols.create(nameVal.str());
        auto j = v.attrs()->get(nameSym);
        if (j) {
            state.ctx.errors
                .make<EvalError>(
                    "dynamic attribute '%1%' already defined at %2%",
                    state.ctx.symbols[nameSym],
                    state.ctx.positions[j->pos]
                )
                .atPos(i.pos)
                .withFrame(env, *this)
                .debugThrow();
        }

        i.valueExpr->setName(nameSym);
        /* Keep sorted order so find can catch duplicates */
        v.attrs()->push_back(Attr(nameSym, i.valueExpr->maybeThunk(state, *dynamicEnv), i.pos));
        v.attrs()->sort(); // FIXME: inefficient
    }

    v.attrs()->pos = pos;
}


void ExprLet::eval(EvalState & state, Env & env, Value & v)
{
    /* Create a new environment that contains the attributes in this
       `let'. */
    Env & env2(state.ctx.mem.allocEnv(attrs.size()));
    env2.up = &env;

    Env * inheritEnv = inheritFromExprs ? buildInheritFromEnv(state, env2) : nullptr;

    /* The recursive attributes are evaluated in the new environment,
       while the inherited attributes are evaluated in the original
       environment. */
    Displacement displ = 0;
    for (auto & i : attrs) {
        env2.values[displ++] = i.second.e->maybeThunk(
            state,
            *i.second.chooseByKind(&env2, &env, inheritEnv));
    }

    body->eval(state, env2, v);
}


void ExprList::eval(EvalState & state, Env & env, Value & v)
{
    auto result = state.ctx.mem.newList(elems.size());
    v = {NewValueAs::list, result};
    for (auto && [n, v2] : enumerate(result->span())) {
        v2 = elems[n]->maybeThunk(state, env);
    }
}

Value ExprList::maybeThunk(EvalState & state, Env & env)
{
    if (elems.empty()) {
        return Value::EMPTY_LIST;
    }
    return Expr::maybeThunk(state, env);
}


void ExprVar::eval(EvalState & state, Env & env, Value & v)
{
    Value * v2 = state.lookupVar(&env, *this, false);
    try {
        state.forceValue(*v2, pos);
    } catch (Error & e) {
        /* `name` can be invalid if we are an ExprInheritFrom */
        if (name) {
            e.addTrace(
                state.ctx.positions[getPos()],
                "while evaluating %s",
                state.ctx.symbols[name]
            );
        }
        throw;
    }
    v = *v2;
}


void ExprInheritFrom::eval(EvalState & state, Env & env, Value & v)
{
    Value & v2 = env.values[displ];
    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;
        try {
            out << state.ctx.symbols[getName(i, state, env)];
        } catch (Error & e) {
            assert(!i.symbol);
            out << "\"${...}\"";
        }
    }
    return out.str();
}

/** Returns `nullptr` if we should be using a default instead. */
Attr const * ExprSelect::selectSingleAttr(
    EvalState & state, Env & env, AttrName const & attrName, Value & vCurrent
)
{
    Symbol const attrSym = getName(attrName, state, env);

    try {
        state.forceValue(vCurrent, pos);
    } catch (Error & e) {
        // clang-format off
        e.addTrace(state.ctx.positions[attrName.pos], HintFmt(
            "while evaluating an expression to select '%s' on it", state.ctx.symbols[attrSym]
        ));
        // clang-format on
        throw;
    }

    if (vCurrent.type() != nAttrs) {
        // If we have an `or` provided default, then it doesn't have to be an attrset.
        // Let the caller know there's no attr value here.
        if (def != nullptr) {
            return nullptr;
        }

        // Otherwise, we must type error.
        // clang-format off
        state.ctx.errors.make<TypeError>(
            "expected a set but found %s: %s",
            showType(vCurrent),
            ValuePrinter(state, vCurrent, errorPrintOptions)
        ).addTrace(
            attrName.pos,
            HintFmt("while selecting '%s'", state.ctx.symbols[attrSym])
        ).debugThrow();
        // clang-format on
    }

    // Now that we know it's an attrset, we can actually look for the name.

    auto const attrIt = vCurrent.attrs()->get(attrSym);
    if (!attrIt) {

        // Again if we have an `or` provided default, then missing attr is not an error.
        if (def != nullptr) {
            return nullptr;
        }

        // Otherwise, we collect all attr names and throw an attr missing error.

        std::set<std::string> const allAttrNames = *vCurrent.attrs()
            | std::views::transform([&state](auto const & attr) {
                  return std::string{state.ctx.symbols[attr.name]};
              })
            | std::ranges::to<std::set>();

        auto suggestions = Suggestions::bestMatches(allAttrNames, state.ctx.symbols[attrSym]);
        state.ctx.errors.make<EvalError>("attribute '%s' missing", state.ctx.symbols[attrSym])
            .atPos(attrName.pos)
            .withSuggestions(suggestions)
            .withFrame(env, *this)
            .debugThrow();
    }

    // If we made it here, then we successfully found the attribute.
    // Return it to our caller!

    return attrIt;
}

void ExprSelect::eval(EvalState & state, Env & env, Value & v)
{
    // Position for the current attrset Value in this select chain.
    PosIdx posCurrent;
    // Position for the current selector in this select chain.
    PosIdx posCurrentSyntax;

    Value baseSelectee;
    try {
        // Evaluate the original thing we're selecting on.
        e->eval(state, env, baseSelectee);
    } catch (Error & e) {
        // clang-format off
        e.addTrace(state.ctx.positions[getPos()], HintFmt(
            "while evaluating an expression to select '%s' on it",
            showAttrPath(state.ctx.symbols, attrPath)
        ));
        // clang-format on
        throw;
    }

    try {
        // With the original selectee evaluated, we'll walk the selection path starting
        // with the evaluated original selectee.
        std::reference_wrapper<Value> curSelectee = std::ref(baseSelectee);
        for (AttrName const & attrName : attrPath) {
            state.ctx.stats.nrLookups++;

            // Select `attrName` on `curSelectee`.
            auto const attr = selectSingleAttr(state, env, attrName, curSelectee.get());
            if (!attr) {
                // Use default.
                try {
                    this->def->eval(state, env, v);
                } catch (Error & err) {
                    err.addTrace(
                        state.ctx.positions[this->def->pos],
                        "while evaluating fallback for missing attribute '%s'",
                        state.ctx.symbols[getName(attrName, state, env)]
                    );
                    throw;
                }
                return;
            }

            // The selection worked. If we have another iteration, then we use `attr->value`
            // as the thing to select on. If this is the last iteration, then `attr->value`
            // is the final value this ExprSelect evaluated to.
            curSelectee = std::ref(attr->value);

            posCurrent = attr->pos;
            posCurrentSyntax = attrName.pos;
            if (state.ctx.stats.countCalls) {
                state.ctx.stats.attrSelects[posCurrent]++;
            }
        }

        state.forceValue(curSelectee.get(), posCurrent ? posCurrent : posCurrentSyntax);

        v = curSelectee.get();

    } catch (Error & err) {
        auto const & lastPos = state.ctx.positions[posCurrent];
        if (lastPos && !std::get_if<Pos::Hidden>(&lastPos.origin)) {
            err.addTrace(
                lastPos, "while evaluating the attribute '%s'", showAttrPath(state, env, attrPath)
            );
        }

        throw;
    }
}

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, getPos());
        const Attr * j;
        auto name = getName(i, state, env);
        if (vAttrs->type() != nAttrs || (j = vAttrs->attrs()->get(name)) == nullptr) {
            v.mkBool(false);
            return;
        } else {
            vAttrs = &j->value;
        }
    }

    v.mkBool(true);
}


void ExprLambda::eval(EvalState & state, Env & env, Value & v)
{
    v = {NewValueAs::lambda, state.ctx.mem, env, *this};
}

namespace {
/** Increments a count on construction and decrements on destruction.
 */
class CallDepth {
  size_t & count;
public:
  CallDepth(size_t & count) : count(count) {
    ++count;
  }
  ~CallDepth() {
    --count;
  }
};
};

struct FormalsMatch
{
    std::vector<SymbolStr> missing;
    std::vector<SymbolStr> unexpected;
    std::set<std::string> unused;
};

/** Matchup an attribute argument set to a lambda's formal arguments,
 * or return what arguments were required but not given, or given but not allowed.
 */
FormalsMatch matchupLambdaAttrs(EvalState & state, Env & env, Displacement & displ, AttrsPattern const & pattern, Bindings & attrs, SymbolTable & symbols)
{
    size_t attrsUsed = 0;

    FormalsMatch result;

    for (auto const & formal : pattern.formals) {

        // The attribute whose name matches the name of the formal we're matching up, if it exists.
        Attr const * matchingArg = attrs.get(formal.name);
        if (matchingArg) {
            attrsUsed += 1;
            env.values[displ] = matchingArg->value;
            displ += 1;

            // We're done here. Move on to the next formal.
            continue;
        }

        // The argument for this formal wasn't given.
        result.unused.emplace(symbols[formal.name]);
        // If the formal has a default, use it.
        if (formal.def) {
            env.values[displ] = formal.def->maybeThunk(state, env);
            displ += 1;
        } else {
            // Otherwise, let our caller know what was missing.
            result.missing.push_back(symbols[formal.name]);
        }
    }

    // Check for unexpected extra arguments.
    if (!pattern.ellipsis && attrsUsed != attrs.size()) {
        // Return the first unexpected argument.
        for (Attr const & attr : attrs) {
            if (!pattern.has(attr.name)) {
                result.unexpected.push_back(symbols[attr.name]);
            }
        }
    }

    return result;
}

Env & SimplePattern::match(
    ExprLambda & lambda, EvalState & state, Env & up, Value & arg, const PosIdx pos
)
{
    Env & env2(state.ctx.mem.allocEnv(1));
    env2.up = &up;
    env2.values[0] = arg;
    return env2;
}

Env & AttrsPattern::match(
    ExprLambda & lambda, EvalState & state, Env & up, Value & arg, const PosIdx pos
)
{
    auto & ctx = state.ctx;

    Env & env2(ctx.mem.allocEnv(formals.size() + (name ? 1 : 0)));
    env2.up = &up;
    Displacement displ = 0;

    try {
        state.forceAttrs(
            arg, lambda.pos, "while evaluating the value passed for the lambda argument"
        );
    } catch (Error & e) {
        if (pos) e.addTrace(ctx.positions[pos], "from call site");
        throw;
    }

    if (name) {
        env2.values[displ++] = arg;
    }

    ///* For each formal argument, get the actual argument.  If
    //   there is no matching actual argument but the formal
    //   argument has a default, use the default. */
    auto const formalsMatch =
        matchupLambdaAttrs(state, env2, displ, *this, *arg.attrs(), ctx.symbols);

    if (!formalsMatch.unexpected.empty() || !formalsMatch.missing.empty()) {
        Suggestions sug; // empty suggestions -> no suggestions
        if (!formalsMatch.unexpected.empty()) {
            // suggestions only for the first unexpected argument
            // TODO: suggestions for all unexpected arguments
            sug = Suggestions::bestMatches(formalsMatch.unused, formalsMatch.unexpected.front());
        }

        auto argFmt = [](const SymbolStr & argument) { return HintFmt("'%s'", argument); };

        [&](){
            if (formalsMatch.unexpected.empty() && !formalsMatch.missing.empty()) {
                return ctx.errors.make<TypeError>(
                    "function '%s' called without required argument%s %s", lambda.getName(ctx.symbols),
                    Uncolored((formalsMatch.missing.size() == 1) ? "" : "s"),
                    Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.missing))
                );
            } else if (!formalsMatch.unexpected.empty() && formalsMatch.missing.empty()) {
                return ctx.errors.make<TypeError>(
                    "function '%s' called with unexpected argument%s %s", lambda.getName(ctx.symbols),
                    Uncolored((formalsMatch.unexpected.size() == 1) ? "" : "s"),
                    Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.unexpected))
                );
            } else {
                return ctx.errors.make<TypeError>(
                    "function '%s' called without required argument%s %s and with unexpected argument%s %s", lambda.getName(ctx.symbols),
                    Uncolored((formalsMatch.missing.size() == 1) ? "" : "s"),
                    Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.missing)),
                    Uncolored((formalsMatch.unexpected.size() == 1) ? "" : "s"),
                    Uncolored(concatStringsCommaAnd(argFmt, formalsMatch.unexpected))
                );
            }
        }().atPos(lambda.pos)
            .withTrace(pos, "from call site")
            .withSuggestions(sug)
            .withFrame(up, lambda)
            .debugThrow();
    }

    return env2;
}

void EvalState::callFunction(Value & fun, std::span<Value> args, Value & vRes, const PosIdx pos)
{
    if (callDepth > evalSettings.maxCallDepth)
        ctx.errors.make<EvalError>("stack overflow; max-call-depth exceeded").atPos(pos).debugThrow();
    CallDepth _level(callDepth);

    auto trace = evalSettings.traceFunctionCalls
        ? std::make_unique<FunctionCallTrace>(ctx.positions[pos])
        : nullptr;

    forceValue(fun, pos);

    Value vCur(fun);

    auto makeAppChain = [&]() {
        if (vCur.isApp()) {
            auto & app = vCur.app();
            vRes = {NewValueAs::app, ctx.mem, app.left(), app.args(), args};
        } else {
            vRes = {NewValueAs::app, ctx.mem, vCur, args};
        }
    };

    const Attr * functor;

    while (args.size() > 0) {

        if (vCur.isLambda()) {

            ExprLambda & lambda(*vCur.lambda().fun);

            Env & env2 = lambda.pattern->match(lambda, *this, *vCur.lambda().env(), args[0], pos);

            ctx.stats.nrFunctionCalls++;
            if (ctx.stats.countCalls) ctx.stats.addCall(lambda);

            /* Evaluate the body. */
            try {
                lambda.body->eval(*this, env2, vCur);
            } catch (Error & e) {
                if (loggerSettings.showTrace.get()) {
                    e.addTrace(
                        ctx.positions[lambda.pos],
                        "while calling %s",
                        lambda.getQuotedName(ctx.symbols));
                    if (pos) e.addTrace(ctx.positions[pos], "from call site");
                }
                throw;
            }

            args = args.subspan(1);
        }

        else if (vCur.isPrimOp()) {

            size_t argsLeft = vCur.primOp()->arity;

            if (args.size() < argsLeft) {
                /* We don't have enough arguments, so create a tPrimOpApp chain. */
                makeAppChain();
                return;
            } else {
                /* We have all the arguments, so call the primop. */
                auto * fn = vCur.primOp();

                ctx.stats.nrPrimOpCalls++;
                if (ctx.stats.countCalls) ctx.stats.primOpCalls[fn->name]++;

                try {
                    SmallVector<Value *, 4> pargs(argsLeft);
                    for (unsigned i = 0; i < argsLeft; i++) {
                        pargs[i] = &args[i];
                    }
                    fn->fun(*this, pargs.data(), vCur);
                } catch (ThrownError & e) {
                    // Distinguish between an error that simply happened while "throw"
                    // was being evaluated and an explicit thrown error.
                    if (fn->name == "throw") {
                        e.addTrace(ctx.positions[pos], "caused by explicit %s", "throw");
                    } else {
                        e.addTrace(ctx.positions[pos], "while calling the '%s' builtin", fn->name);
                    }
                    throw;
                } catch (Error & e) {
                    e.addTrace(ctx.positions[pos], "while calling the '%1%' builtin", fn->name);
                    throw;
                }

                args = args.subspan(argsLeft);
            }
        }

        else if (vCur.isPrimOpApp()) {
            auto & app = vCur.app();
            auto prevArgs = app.args();

            assert(!vCur.app().left().isApp());

            /* Figure out the number of arguments still needed. */
            Value primOp = app.target();
            auto arity = primOp.primOp()->arity;

            if (args.size() < arity - prevArgs.size()) {
                /* We still don't have enough arguments, so extend the tPrimOpApp chain. */
                makeAppChain();
                return;
            } else {
                /* We have all the arguments, so call the primop with
                   the previous and new arguments. */

                // max arity as of writing is 3. even 4 seems excessive though.
                SmallVector<Value *, 4> vArgs;
                for (auto & arg : prevArgs) {
                    vArgs.push_back(&arg);
                }
                while (vArgs.size() < arity) {
                    vArgs.push_back(&args[0]);
                    args = args.subspan(1);
                }

                auto fn = primOp.primOp();
                ctx.stats.nrPrimOpCalls++;
                if (ctx.stats.countCalls) ctx.stats.primOpCalls[fn->name]++;

                try {
                    // TODO:
                    // 1. Unify this and above code. Heavily redundant.
                    // 2. Create a fake env (arg1, arg2, etc.) and a fake expr (arg1: arg2: etc: builtins.name arg1 arg2 etc)
                    //    so the debugger allows to inspect the wrong parameters passed to the builtin.
                    fn->fun(*this, vArgs.data(), vCur);
                } catch (Error & e) {
                    e.addTrace(ctx.positions[pos], "while calling the '%1%' builtin", fn->name);
                    throw;
                }
            }
        }

        else if (vCur.type() == nAttrs && (functor = vCur.attrs()->get(ctx.s.functor))) {
            /* 'vCur' may be allocated on the stack of the calling
               function, but for functors we may keep a reference, so
               heap-allocate a copy and use that instead. */
            Value args2[] = {vCur, args[0]};
            try {
                callFunction(functor->value, args2, vCur, functor->pos);
            } catch (Error & e) {
                e.addTrace(ctx.positions[pos], "while calling a functor (an attribute set with a '__functor' attribute)");
                throw;
            }
            args = args.subspan(1);
        }

        else
            ctx.errors.make<TypeError>(
                    "attempt to call something which is not a function but %1%: %2%",
                    showType(vCur),
                    ValuePrinter(*this, vCur, errorPrintOptions))
                .atPos(pos)
                .debugThrow();
    }

    vRes = vCur;
}


void ExprCall::eval(EvalState & state, Env & env, Value & v)
{
    Value vFun;
    fun->eval(state, env, vFun);

    // Empirical arity of Nixpkgs lambdas by regex e.g. ([a-zA-Z]+:(\s|(/\*.*\/)|(#.*\n))*){5}
    // 2: over 4000
    // 3: about 300
    // 4: about 60
    // 5: under 10
    // This excluded attrset lambdas (`{...}:`). Contributions of mixed lambdas appears insignificant at ~150 total.
    SmallValueVector<4> vArgs(args.size());
    for (size_t i = 0; i < args.size(); ++i)
        vArgs[i] = args[i]->maybeThunk(state, env);

    state.callFunction(vFun, vArgs, v, pos);
}


// Lifted out of callFunction() because it creates a temporary that
// prevents tail-call optimisation.
void EvalStatistics::addCall(ExprLambda & fun)
{
    functionCalls[&fun]++;
}


void EvalState::autoCallFunction(Bindings & args, Value & fun, Value & res, PosIdx pos)
{
    forceValue(fun, pos);

    if (fun.type() == nAttrs) {
        auto found = fun.attrs()->get(ctx.s.functor);
        if (found) {
            Value v;
            callFunction(found->value, fun, v, pos);
            forceValue(v, pos);
            return autoCallFunction(args, v, res, pos);
        }
    }

    if (!fun.isLambda()) {
        res = fun;
        return;
    }
    auto pattern = dynamic_cast<AttrsPattern *>(fun.lambda().fun->pattern.get());
    if (!pattern) {
        res = fun;
        return;
    }

    auto attrs = ctx.buildBindings(std::max(static_cast<uint32_t>(pattern->formals.size()), args.size()));

    if (pattern->ellipsis) {
        // If the formals have an ellipsis (eg the function accepts extra args) pass
        // all available automatic arguments (which includes arguments specified on
        // the command line via --arg/--argstr)
        for (auto & v : args)
            attrs.insert(v);
    } else {
        // Otherwise, only pass the arguments that the function accepts
        for (auto & i : pattern->formals) {
            auto j = args.get(i.name);
            if (j) {
                attrs.insert(*j);
            } else if (!i.def) {
                ctx.errors
                    .make<MissingArgumentError>(
                        R"(cannot evaluate a function that has an argument without a value ('%1%')
Lix attempted to evaluate a function as a top level expression; in
this case it must have its arguments supplied either by default
values, or passed explicitly with '--arg' or '--argstr'. See
https://docs.lix.systems/manual/lix/stable/language/constructs.html#functions)",
                        ctx.symbols[i.name]
                    )
                    .atPos(i.pos)
                    .withFrame(*fun.lambda().env(), *fun.lambda().fun)
                    .debugThrow();
            }
        }
    }

    Value vAttrs{NewValueAs::attrs, attrs.finish()};
    callFunction(fun, vAttrs, res, pos);
}


void ExprWith::eval(EvalState & state, Env & env, Value & v)
{
    Env & env2(state.ctx.mem.allocEnv(1));
    env2.up = &env;
    env2.values[0] = attrs->maybeThunk(state, env);

    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)) {
        state.ctx.errors.make<AssertionError>("assertion failed")
            .atPos(pos)
            .withFrame(env, *this)
            .debugThrow();
    }
    body->eval(state, env, v);
}


void ExprOpNot::eval(EvalState & state, Env & env, Value & v)
{
    v.mkBool(!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);
    v.mkBool(state.eqValues(v1, v2, pos, "while testing two values for equality"));
}


void ExprOpNEq::eval(EvalState & state, Env & env, Value & v)
{
    Value v1; e1->eval(state, env, v1);
    Value v2; e2->eval(state, env, v2);
    v.mkBool(!state.eqValues(v1, v2, pos, "while testing two values for inequality"));
}


void ExprOpAnd::eval(EvalState & state, Env & env, Value & v)
{
    v.mkBool(state.evalBool(env, *e1) && state.evalBool(env, *e2));
}


void ExprOpOr::eval(EvalState & state, Env & env, Value & v)
{
    v.mkBool(state.evalBool(env, *e1) || state.evalBool(env, *e2));
}


void ExprOpImpl::eval(EvalState & state, Env & env, Value & v)
{
    v.mkBool(!state.evalBool(env, *e1) || state.evalBool(env, *e2));
}


void ExprOpUpdate::eval(EvalState & state, Env & env, Value & v)
{
    Value v1, v2;
    state.evalAttrs(env, *e1, v1);
    state.evalAttrs(env, *e2, v2);

    state.ctx.stats.nrOpUpdates++;

    if (v1.attrs()->size() == 0) { v = v2; return; }
    if (v2.attrs()->size() == 0) { v = v1; return; }

    auto attrs = state.ctx.buildBindings(v1.attrs()->size() + v2.attrs()->size());

    /* Merge the sets, preferring values from the second set.  Make
       sure to keep the resulting vector in sorted order. */
    Bindings::iterator i = v1.attrs()->begin();
    Bindings::iterator j = v2.attrs()->begin();

    while (i != v1.attrs()->end() && j != v2.attrs()->end()) {
        if (i->name == j->name) {
            attrs.insert(*j);
            ++i; ++j;
        }
        else if (i->name < j->name)
            attrs.insert(*i++);
        else
            attrs.insert(*j++);
    }

    while (i != v1.attrs()->end()) attrs.insert(*i++);
    while (j != v2.attrs()->end()) attrs.insert(*j++);

    v.mkAttrs(attrs.alreadySorted());

    state.ctx.stats.nrOpUpdateValuesCopied += v.attrs()->size();
}


void ExprOpConcatLists::eval(EvalState & state, Env & env, Value & v)
{
    state.ctx.stats.nrListConcats++;

    /* We don't call into `concatLists` as that loses the position information of the expressions. */

    Value v1; state.evalList(env, *e1, v1);
    Value v2; state.evalList(env, *e2, v2);

    size_t l1 = v1.listSize(), l2 = v2.listSize(), len = l1 + l2;

    if (l1 == 0)
        v = v2;
    else if (l2 == 0)
        v = v1;
    else {
        auto list = state.ctx.mem.newList(len);
        v = {NewValueAs::list, list};
        auto out = list->elems;
        std::copy(v1.listElems(), v1.listElems() + l1, out);
        std::copy(v2.listElems(), v2.listElems() + l2, out + l1);
    }
}

void EvalState::concatLists(
    Value & v, std::span<Value> lists, const PosIdx pos, std::string_view errorCtx
)
{
    ctx.stats.nrListConcats++;

    Value * nonEmpty = 0;
    size_t len = 0;
    for (size_t n = 0; n < lists.size(); ++n) {
        forceList(lists[n], pos, errorCtx);
        auto l = lists[n].listSize();
        len += l;
        if (l) {
            nonEmpty = &lists[n];
        }
    }

    if (nonEmpty && len == nonEmpty->listSize()) {
        v = *nonEmpty;
        return;
    }

    auto list = ctx.mem.newList(len);
    v = {NewValueAs::list, list};
    auto out = list->elems;
    for (size_t n = 0, pos = 0; n < lists.size(); ++n) {
        auto l = lists[n].listSize();
        if (l) {
            std::copy(lists[n].listItems().begin(), lists[n].listItems().end(), out + pos);
        }
        pos += l;
    }
}


void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
{
    NixStringContext context;
    std::vector<BackedStringView> s;
    size_t sSize = 0;
    NixInt n{0};
    NixFloat nf = 0;

    bool first = !isInterpolation;
    ValueType firstType = nString;

    const auto str = [&] {
        std::string result;
        result.reserve(sSize);
        for (const auto & part : s) result += *part;
        return result;
    };
    /* build a gc'd value string directly instead of going through str()
       and mkString to save an allocation and copy */
    const auto gcStr = [&] {
        auto result = Value::Str::gcAlloc(sSize);

        char * tmp = result->contents;
        for (const auto & part : s) {
            memcpy(tmp, part->data(), part->size());
            tmp += part->size();
        }
        return result;
    };

    // List of returned strings. References to these Values must NOT be persisted.
    SmallTemporaryValueVector<conservativeStackReservation> values(es.size());
    Value * vTmpP = values.data();

    for (auto & [i_pos, i] : es) {
        Value & vTmp = *vTmpP++;
        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();
        }

        if (firstType == nInt) {
            if (vTmp.type() == nInt) {
                auto newN = n + vTmp.integer();
                if (auto checked = newN.valueChecked(); checked.has_value()) {
                    n = NixInt(*checked);
                } else {
                    state.ctx.errors
                        .make<EvalError>("integer overflow in adding %1% + %2%", n, vTmp.integer())
                        .atPos(i_pos)
                        .debugThrow();
                }
            } else if (vTmp.type() == nFloat) {
                // Upgrade the type from int to float;
                firstType = nFloat;
                nf = n.value;
                nf += vTmp.fpoint();
            } else
                state.ctx.errors.make<EvalError>("cannot add %1% to an integer", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow();
        } else if (firstType == nFloat) {
            if (vTmp.type() == nInt) {
                nf += vTmp.integer().value;
            } else if (vTmp.type() == nFloat) {
                nf += vTmp.fpoint();
            } else
                state.ctx.errors.make<EvalError>("cannot add %1% to a float", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow();
        } else {
            if (s.empty()) s.reserve(es.size());

            /* If we are coercing inside of an interpolation, we may allow slightly more comfort by coercing things like integers. */
            auto coercionMode = isInterpolation && featureSettings.isEnabled(Xp::CoerceIntegers)
                ? StringCoercionMode::Interpolation : StringCoercionMode::Strict;

            /* skip canonization of first path, which would only be not
            canonized in the first place if it's coming from a ./${foo} type
            path */
            auto part = state.coerceToString(i_pos, vTmp, context,
                                             "while evaluating a path segment",
                                             coercionMode, firstType == nString, !first);
            sSize += part->size();
            s.emplace_back(std::move(part));
        }

        first = false;
    }

    if (firstType == nInt)
        v.mkInt(n);
    else if (firstType == nFloat)
        v.mkFloat(nf);
    else if (firstType == nPath) {
        if (!context.empty())
            state.ctx.errors.make<EvalError>("a string that refers to a store path cannot be appended to a path").atPos(pos).withFrame(env, *this).debugThrow();
        v.mkPath(CanonPath(canonPath(str())));
    } else
        v.mkStringMove(gcStr(), context);
}


void ExprPos::eval(EvalState & state, Env & env, Value & v)
{
    state.mkPos(v, pos);
}


void ExprBlackHole::eval(EvalState & state, Env & env, Value & v)
{
    state.ctx.errors.make<InfiniteRecursionError>("infinite recursion encountered")
        .debugThrow();
}

void ExprDebugFrame::eval(EvalState & state, Env & env, Value & v)
{
    auto dts =
        makeDebugTraceStacker(state, *inner, env, state.ctx.positions[pos], message);
    inner->eval(state, env, v);
}

// always force this to be separate, otherwise forceValue may inline it and take
// a massive perf hit
[[gnu::noinline]]
void EvalState::tryFixupBlackHolePos(Value & v, PosIdx pos)
{
    if (!v.isBlackhole())
        return;
    auto e = std::current_exception();
    try {
        std::rethrow_exception(e);
    } catch (InfiniteRecursionError & e) {
        e.atPos(ctx.positions[pos]);
    } catch (...) {
    }
}


void EvalState::forceValueDeep(Value & v)
{
    std::set<const Value *> seen;

    std::function<void(Value & v)> recurse;

    recurse = [&](Value & v) {
        if (!seen.insert(&v).second) return;

        forceValue(v, noPos);

        if (v.type() == nAttrs) {
            for (auto & i : *v.attrs())
                try {
                    // If the value is a thunk, we're evaling. Otherwise no trace necessary.
                    auto dts = ctx.debug && i.value.isThunk()
                        ? makeDebugTraceStacker(
                              *this,
                              *i.value.thunk().expr,
                              *i.value.thunk().env(),
                              ctx.positions[i.pos],
                              "while evaluating the attribute '%1%'",
                              ctx.symbols[i.name]
                          )
                        : nullptr;

                    recurse(i.value);
                } catch (Error & e) {
                    e.addTrace(ctx.positions[i.pos], "while evaluating the attribute '%1%'", ctx.symbols[i.name]);
                    throw;
                }
        }

        else if (v.isList()) {
            for (auto & v2 : v.listItems()) {
                recurse(v2);
            }
        }
    };

    recurse(v);
}


NixInt EvalState::forceInt(Value & v, const PosIdx pos, std::string_view errorCtx)
{
    try {
        forceValue(v, pos);
        if (v.type() != nInt)
            ctx.errors.make<TypeError>(
                "expected an integer but found %1%: %2%",
                showType(v),
                ValuePrinter(*this, v, errorPrintOptions)
            ).atPos(pos).debugThrow();
        return v.integer();
    } catch (Error & e) {
        e.addTrace(ctx.positions[pos], errorCtx);
        throw;
    }

    return v.integer();
}


NixFloat EvalState::forceFloat(Value & v, const PosIdx pos, std::string_view errorCtx)
{
    try {
        forceValue(v, pos);
        if (v.type() == nInt)
            return v.integer().value;
        else if (v.type() != nFloat)
            ctx.errors.make<TypeError>(
                "expected a float but found %1%: %2%",
                showType(v),
                ValuePrinter(*this, v, errorPrintOptions)
            ).atPos(pos).debugThrow();
        return v.fpoint();
    } catch (Error & e) {
        e.addTrace(ctx.positions[pos], errorCtx);
        throw;
    }
}


bool EvalState::forceBool(Value & v, const PosIdx pos, std::string_view errorCtx)
{
    try {
        forceValue(v, pos);
        if (v.type() != nBool)
            ctx.errors.make<TypeError>(
                "expected a Boolean but found %1%: %2%",
                showType(v),
                ValuePrinter(*this, v, errorPrintOptions)
            ).atPos(pos).debugThrow();
        return v.boolean();
    } catch (Error & e) {
        e.addTrace(ctx.positions[pos], errorCtx);
        throw;
    }

    return v.boolean();
}


bool EvalState::isFunctor(Value & fun)
{
    return fun.type() == nAttrs && fun.attrs()->get(ctx.s.functor);
}


void EvalState::forceFunction(Value & v, const PosIdx pos, std::string_view errorCtx)
{
    try {
        forceValue(v, pos);
        if (v.type() != nFunction && !isFunctor(v))
            ctx.errors.make<TypeError>(
                "expected a function but found %1%: %2%",
                showType(v),
                ValuePrinter(*this, v, errorPrintOptions)
            ).atPos(pos).debugThrow();
    } catch (Error & e) {
        e.addTrace(ctx.positions[pos], errorCtx);
        throw;
    }
}


std::string_view EvalState::forceString(Value & v, const PosIdx pos, std::string_view errorCtx)
{
    try {
        forceValue(v, pos);
        if (v.type() != nString)
            ctx.errors.make<TypeError>(
                "expected a string but found %1%: %2%",
                showType(v),
                ValuePrinter(*this, v, errorPrintOptions)
            ).atPos(pos).debugThrow();
        return v.str();
    } catch (Error & e) {
        e.addTrace(ctx.positions[pos], errorCtx);
        throw;
    }
}


void copyContext(const Value & v, NixStringContext & context)
{
    if (v.string().context)
        for (const char * * p = v.string().context; *p; ++p)
            context.insert(NixStringContextElem::parse(*p));
}


std::string_view EvalState::forceString(Value & v, NixStringContext & context, const PosIdx pos, std::string_view errorCtx)
{
    auto s = forceString(v, pos, errorCtx);
    copyContext(v, context);
    return s;
}


std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos, std::string_view errorCtx)
{
    auto s = forceString(v, pos, errorCtx);
    if (v.string().context) {
        ctx.errors
            .make<EvalError>(
                "the string '%1%' is not allowed to refer to a store path (such as '%2%')",
                v.str(),
                v.string().context[0]
            )
            .withTrace(pos, errorCtx)
            .debugThrow();
    }
    return s;
}


bool EvalState::isDerivation(Value & v)
{
    if (v.type() != nAttrs) return false;
    auto i = v.attrs()->get(ctx.s.type);
    if (!i) {
        return false;
    }
    forceValue(i->value, i->pos);
    if (i->value.type() != nString) {
        return false;
    }
    return i->value.str() == "derivation";
}


std::optional<std::string> EvalState::tryAttrsToString(const PosIdx pos, Value & v,
    NixStringContext & context, StringCoercionMode mode, bool copyToStore)
{
    auto i = v.attrs()->get(ctx.s.toString);
    if (i) {
        Value v1;
        try {
            callFunction(i->value, v, v1, i->pos);
            return coerceToString(pos, v1, context,
                    "while evaluating the result of the `__toString` attribute",
                    mode, copyToStore).toOwned();
        } catch (EvalError & e) {
            e.addTrace(ctx.positions[pos], "while converting a set to string");
            throw;
        }
    }

    return {};
}

BackedStringView EvalState::coerceToString(
    const PosIdx pos,
    Value & v,
    NixStringContext & context,
    std::string_view errorCtx,
    StringCoercionMode mode,
    bool copyToStore,
    bool canonicalizePath)
{
    forceValue(v, pos);

    if (v.type() == nString) {
        copyContext(v, context);
        return v.str();
    }

    if (v.type() == nPath) {
        return !canonicalizePath && !copyToStore
            // FIXME: hack to preserve path literals that end in a slash, as in /foo/${x}.
            ? std::string(v.string().content->str())
            : (copyToStore
                   ? ctx.store->printStorePath(
                         aio.blockOn(ctx.paths.copyPathToStore(context, v.path(), ctx.repair))
                             .unwrap()
                     )
                   : v.path().to_string());
    }

    if (v.type() == nAttrs) {
        auto maybeString = tryAttrsToString(pos, v, context, mode, copyToStore);
        if (maybeString)
            return std::move(*maybeString);
        auto i = v.attrs()->get(ctx.s.outPath);
        if (!i) {
            ctx.errors.make<TypeError>(
                "cannot coerce %1% to a string: %2%",
                showType(v),
                ValuePrinter(*this, v, errorPrintOptions)
            )
                .withTrace(pos, errorCtx)
                .debugThrow();
        }
        return coerceToString(
            pos, i->value, context, errorCtx, mode, copyToStore, canonicalizePath
        );
    }

    if (v.type() == nExternal) {
        try {
            return v.external()->coerceToString(*this, pos, context, mode, copyToStore);
        } catch (Error & e) {
            e.addTrace(nullptr, errorCtx);
            throw;
        }
    }

    /* Raito: Any addition to this mode is subject to extra scrutiny
     * until we have better formatting tools. */
    if (mode >= StringCoercionMode::Interpolation) {
        if (v.type() == nInt) {
            return std::to_string(v.integer().value);
        }
    }

    if (mode >= StringCoercionMode::ToString) {
        /* Note that `false' is represented as an empty string for
           shell scripting convenience, just like `null'. */
        if (v.type() == nBool && v.boolean()) {
            return "1";
        }
        if (v.type() == nBool && !v.boolean()) {
            return "";
        }
        if (v.type() == nFloat) return std::to_string(v.fpoint());
        if (v.type() == nNull) return "";

        if (v.isList()) {
            std::string result;
            for (auto [n, v2] : enumerate(v.listItems())) {
                try {
                    result += *coerceToString(
                        pos,
                        v2,
                        context,
                        "while evaluating one element of the list",
                        mode,
                        copyToStore,
                        canonicalizePath
                    );
                } catch (Error & e) {
                    e.addTrace(ctx.positions[pos], errorCtx);
                    throw;
                }
                if (n < v.listSize() - 1
                    /* !!! not quite correct */
                    && (!v2.isList() || v2.listSize() != 0))
                {
                    result += " ";
                }
            }
            return result;
        }
    }

    ctx.errors.make<TypeError>("cannot coerce %1% to a string: %2%",
        showType(v),
        ValuePrinter(*this, v, errorPrintOptions)
    )
        .withTrace(pos, errorCtx)
        .debugThrow();
}


kj::Promise<Result<EvalPaths::PathResult<StorePath, EvalError>>>
EvalPaths::copyPathToStore(NixStringContext & context, const SourcePath & path, RepairFlag repair)
try {
    if (nix::isDerivation(path.canonical().abs()))
        co_return errors.make<EvalError>("file names are not allowed to end in '%1%'", drvExtension);

    auto i = srcToStore.find(path);

    auto dstPath = i != srcToStore.end()
        ? i->second
        : ({
            auto dstPath = TRY_AWAIT(fetchToStoreRecursive(
                *store,
                *prepareDump(checkSourcePath(path).canonical().abs()),
                path.baseName(),
                repair
            ));
            allowPath(dstPath);
            srcToStore.insert_or_assign(path, dstPath);
            printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
            std::move(dstPath);
        });

    context.insert(NixStringContextElem::Opaque {
        .path = dstPath
    });
    co_return dstPath;
} catch (...) {
    co_return result::current_exception();
}


SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)
{
    auto path = coerceToString(pos, v, context, errorCtx, StringCoercionMode::Strict, false, true).toOwned();
    if (path == "" || path[0] != '/')
        ctx.errors.make<EvalError>("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow();
    return CanonPath(path);
}


StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)
{
    auto path = coerceToString(pos, v, context, errorCtx, StringCoercionMode::Strict, false, true).toOwned();
    if (auto storePath = ctx.store->maybeParseStorePath(path))
        return *storePath;
    ctx.errors.make<EvalError>("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow();
}


std::pair<SingleDerivedPath, std::string_view> EvalState::coerceToSingleDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx)
{
    NixStringContext context;
    auto s = forceString(v, context, pos, errorCtx);
    auto csize = context.size();
    if (csize != 1)
        ctx.errors.make<EvalError>(
            "string '%s' has %d entries in its context. It should only have exactly one entry",
            s, csize)
            .withTrace(pos, errorCtx).debugThrow();
    auto derivedPath = std::visit(overloaded {
        [&](NixStringContextElem::Opaque && o) -> SingleDerivedPath {
            return std::move(o);
        },
        [&](NixStringContextElem::DrvDeep &&) -> SingleDerivedPath {
            ctx.errors.make<EvalError>(
                "string '%s' has a context which refers to a complete source and binary closure. This is not supported at this time",
                s).withTrace(pos, errorCtx).debugThrow(always_progresses);
        },
        [&](NixStringContextElem::Built && b) -> SingleDerivedPath {
            return std::move(b);
        },
    }, ((NixStringContextElem &&) *context.begin()).raw);
    return {
        std::move(derivedPath),
        std::move(s),
    };
}


SingleDerivedPath EvalState::coerceToSingleDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx)
{
    auto [derivedPath, s_] = coerceToSingleDerivedPathUnchecked(pos, v, errorCtx);
    auto s = s_;
    auto sExpected = mkSingleDerivedPathStringRaw(derivedPath);
    if (s != sExpected) {
        /* `std::visit` is used here just to provide a more precise
           error message. */
        std::visit(overloaded {
            [&](const SingleDerivedPath::Opaque & o) {
                ctx.errors.make<EvalError>(
                    "path string '%s' has context with the different path '%s'",
                    s, sExpected)
                    .withTrace(pos, errorCtx).debugThrow(always_progresses);
            },
            [&](const SingleDerivedPath::Built & b) {
                ctx.errors.make<EvalError>(
                    "string '%s' has context with the output '%s' from derivation '%s', but the string is not the right placeholder for this derivation output. It should be '%s'",
                    s, b.output, b.drvPath.to_string(*ctx.store), sExpected)
                    .withTrace(pos, errorCtx).debugThrow(always_progresses);
            }
        }, derivedPath.raw());
    }
    return derivedPath;
}


bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx)
{
    forceValue(v1, pos);
    forceValue(v2, pos);

    // Special case type-compatibility between float and int
    if (v1.type() == nInt && v2.type() == nFloat) {
        return v1.integer().value == v2.fpoint();
    }
    if (v1.type() == nFloat && v2.type() == nInt) {
        return v1.fpoint() == v2.integer().value;
    }

    // All other types are not compatible with each other.
    if (v1.type() != v2.type()) return false;

    /* !!! Hack to support some old broken code that relies on pointer
       equality tests between sets.  (Specifically, builderDefs calls
       uniqList on a list of sets.)  Will remove this eventually. */
    auto pointerEq = [&] { return v1.pointerEqProxy() == v2.pointerEqProxy(); };

    switch (v1.type()) {
        case nInt:
            return v1.integer() == v2.integer();

        case nBool:
            return v1.boolean() == v2.boolean();

        case nString:
            return v1.str() == v2.str();

        case nPath:
            return v1.string().content->str() == v2.string().content->str();

        case nNull:
            return true;

        case nList:
            if (pointerEq()) return true;
            if (v1.listSize() != v2.listSize()) return false;
            for (size_t n = 0; n < v1.listSize(); ++n) {
                if (!eqValues(v1.listElems()[n], v2.listElems()[n], pos, errorCtx)) {
                    return false;
                }
            }
            return true;

        case nAttrs: {
            if (pointerEq()) return true;
            /* If both sets denote a derivation (type = "derivation"),
               then compare their outPaths. */
            if (isDerivation(v1) && isDerivation(v2)) {
                auto i = v1.attrs()->get(ctx.s.outPath);
                auto j = v2.attrs()->get(ctx.s.outPath);
                if (i && j) {
                    return eqValues(i->value, j->value, pos, errorCtx);
                }
            }

            if (v1.attrs()->size() != v2.attrs()->size()) return false;

            /* Otherwise, compare the attributes one by one. */
            Bindings::iterator i, j;
            for (i = v1.attrs()->begin(), j = v2.attrs()->begin(); i != v1.attrs()->end(); ++i, ++j)
            {
                if (i->name != j->name || !eqValues(i->value, j->value, pos, errorCtx)) {
                    return false;
                }
            }

            return true;
        }

        /* Functions are incomparable, except for identity (see note above about this nonsense). */
        case nFunction:
            return pointerEq();

        case nExternal:
            if (pointerEq()) return true;
            return *v1.external() == *v2.external();

        case nFloat:
            return v1.fpoint() == v2.fpoint();

        case nThunk: // Must not be left by forceValue
        default:
            ctx.errors.make<EvalError>("cannot compare %1% with %2%", showType(v1), showType(v2)).withTrace(pos, errorCtx).debugThrow();
    }
}

bool Evaluator::fullGC() {
#if HAVE_BOEHMGC
    GC_gcollect();
    // Check that it ran. We might replace this with a version that uses more
    // of the boehm API to get this reliably, at a maintenance cost.
    // We use a 1K margin because technically this has a race condtion, but we
    // probably won't encounter it in practice, because the CLI isn't concurrent
    // like that.
    return GC_get_bytes_since_gc() < 1024;
#else
    return false;
#endif
}

void Evaluator::maybePrintStats()
{
    bool showStats = getEnv("NIX_SHOW_STATS").value_or("0") != "0";

    if (showStats) {
        // Make the final heap size more deterministic.
#if HAVE_BOEHMGC
        if (!fullGC()) {
            printTaggedWarning("failed to perform a full GC before reporting stats");
        }
#endif
        printStatistics();
    }
}

void Evaluator::printStatistics()
{
    struct rusage buf;
    getrusage(RUSAGE_SELF, &buf);
    float cpuTime = buf.ru_utime.tv_sec + ((float) buf.ru_utime.tv_usec / 1000000);
    auto mem = this->mem.getStats();

    uint64_t bEnvs = mem.nrEnvs * sizeof(Env) + mem.nrValuesInEnvs * sizeof(Value *);
    uint64_t bLists = mem.nrListElems * sizeof(Value *);
    uint64_t bAttrsets = mem.nrAttrsets * sizeof(Bindings) + mem.nrAttrsInAttrsets * sizeof(Attr);

#if HAVE_BOEHMGC
    GC_word heapSize, totalBytes;
    GC_get_heap_usage_safe(&heapSize, 0, 0, 0, &totalBytes);
#endif

    auto outPath = getEnv("NIX_SHOW_STATS_PATH").value_or("-");
    std::fstream fs;
    if (outPath != "-")
        fs.open(outPath, std::fstream::out);
    JSON topObj = JSON::object();
    topObj["cpuTime"] = cpuTime;
    topObj["envs"] = {
        {"number", mem.nrEnvs},
        {"elements", mem.nrValuesInEnvs},
        {"bytes", bEnvs},
    };
    topObj["list"] = {
        {"elements", mem.nrListElems},
        {"bytes", bLists},
        {"concats", stats.nrListConcats},
    };
    // reported for compatibility, even though we no longer allocate these on the heap
    topObj["values"] = {
        {"number", 0},
        {"bytes", 0},
    };
    topObj["symbols"] = {
        {"number", symbols.size()},
        {"bytes", symbols.totalSize()},
    };
    topObj["sets"] = {
        {"number", mem.nrAttrsets},
        {"bytes", bAttrsets},
        {"elements", mem.nrAttrsInAttrsets},
    };
    topObj["sizes"] = {
        {"Env", sizeof(Env)},
        {"Value", sizeof(Value)},
        {"Bindings", sizeof(Bindings)},
        {"Attr", sizeof(Attr)},
    };
    topObj["nrOpUpdates"] = stats.nrOpUpdates;
    topObj["nrOpUpdateValuesCopied"] = stats.nrOpUpdateValuesCopied;
    topObj["nrThunks"] = stats.nrThunks;
    topObj["nrAvoided"] = stats.nrAvoided;
    topObj["nrLookups"] = stats.nrLookups;
    topObj["nrPrimOpCalls"] = stats.nrPrimOpCalls;
    topObj["nrFunctionCalls"] = stats.nrFunctionCalls;
#if HAVE_BOEHMGC
    topObj["gc"] = {
        {"heapSize", heapSize},
        {"totalBytes", totalBytes},
    };
#endif

    if (stats.countCalls) {
        topObj["primops"] = stats.primOpCalls;
        {
            auto& list = topObj["functions"];
            list = JSON::array();
            for (auto & [fun, count] : stats.functionCalls) {
                JSON obj = JSON::object();
                if (fun->name)
                    obj["name"] = (std::string_view) symbols[fun->name];
                else
                    obj["name"] = nullptr;
                if (auto pos = positions[fun->pos]) {
                    if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
                        obj["file"] = path->to_string();
                    obj["line"] = pos.line;
                    obj["column"] = pos.column;
                }
                obj["count"] = count;
                list.push_back(obj);
            }
        }
        {
            auto list = topObj["attributes"];
            list = JSON::array();
            for (auto & i : stats.attrSelects) {
                JSON obj = JSON::object();
                if (auto pos = positions[i.first]) {
                    if (auto path = std::get_if<CheckedSourcePath>(&pos.origin))
                        obj["file"] = path->to_string();
                    obj["line"] = pos.line;
                    obj["column"] = pos.column;
                }
                obj["count"] = i.second;
                list.push_back(obj);
            }
        }
    }

    if (getEnv("NIX_SHOW_SYMBOLS").value_or("0") != "0") {
        // XXX: overrides earlier assignment
        topObj["symbols"] = JSON::array();
        auto &list = topObj["symbols"];
        symbols.dump([&](std::string_view s) { list.emplace_back(s); });
    }
    if (outPath == "-") {
        std::cerr << topObj.dump(2) << std::endl;
    } else {
        fs << topObj.dump(2) << std::endl;
    }
}


CheckedSourcePath EvalPaths::resolveExprPath(SourcePath path_)
{
    auto path = checkSourcePath(path_);
    unsigned int followCount = 0, maxFollow = 1024;

    /* If `path' is a symlink, follow it.  This is so that relative
       path references work. */
    while (true) {
        // Basic cycle/depth limit to avoid infinite loops.
        if (++followCount >= maxFollow)
            throw Error("too many symbolic links encountered while traversing the path '%s'", path);
        if (path.lstat().type != InputAccessor::tSymlink) break;
        path = checkSourcePath(
            CanonPath(path.readLink(), path.canonical().parent().value_or(CanonPath::root))
        );
    }

    /* If `path' refers to a directory, append `/default.nix'. */
    if (path.lstat().type == InputAccessor::tDirectory)
        return checkSourcePath(path + "default.nix");

    return path;
}


Expr & Evaluator::parseExprFromFile(const CheckedSourcePath & path)
{
    return parseExprFromFile(path, builtins.staticEnv);
}


Expr & Evaluator::parseExprFromFile(const CheckedSourcePath & path, std::shared_ptr<StaticEnv> & staticEnv)
{
    auto buffer = path.readFile();
    return *parse(buffer.data(), buffer.size(), Pos::Origin(path), path.parent(), staticEnv);
}


Expr & Evaluator::parseExprFromString(
    std::string s_,
    const SourcePath & basePath,
    std::shared_ptr<StaticEnv> & staticEnv,
    const FeatureSettings & featureSettings
)
{
    auto s = make_ref<std::string>(std::move(s_));
    return *parse(s->data(), s->size(), Pos::String{.source = s}, basePath, staticEnv, featureSettings);
}


Expr & Evaluator::parseExprFromString(
    std::string s,
    const SourcePath & basePath,
    const FeatureSettings & featureSettings
)
{
    return parseExprFromString(std::move(s), basePath, builtins.staticEnv, featureSettings);
}

std::variant<std::unique_ptr<Expr>, ExprReplBindings>
Evaluator::parseReplInput(
    std::string s_,
    const SourcePath & basePath,
    std::shared_ptr<StaticEnv> & staticEnv,
    const FeatureSettings & featureSettings
)
{
    auto s = make_ref<std::string>(std::move(s_));
    return parse_repl(s->data(), s->size(), Pos::String{.source = s}, basePath, staticEnv, featureSettings);
}

Expr & Evaluator::parseStdin()
{
    //Activity act(*logger, lvlTalkative, "parsing standard input");
    auto s = make_ref<std::string>(drainFD(0));
    return *parse(
        s->data(), s->size(), Pos::Stdin{.source = s}, CanonPath::fromCwd(), builtins.staticEnv
    );
}


kj::Promise<Result<EvalPaths::PathResult<SourcePath, ThrownError>>>
EvalPaths::findFile(const std::string_view path)
{
    return findFile(searchPath_, path);
}


kj::Promise<Result<EvalPaths::PathResult<SourcePath, ThrownError>>>
EvalPaths::findFile(const SearchPath & searchPath, const std::string_view path, const PosIdx pos)
try {
    for (auto & i : searchPath.elements) {
        auto suffixOpt = i.prefix.suffixIfPotentialMatch(path);

        if (!suffixOpt) continue;
        auto suffix = *suffixOpt;

        auto rOpt = TRY_AWAIT(resolveSearchPathPath(i.path));
        if (!rOpt) continue;
        auto r = *rOpt;

        Path res = suffix == "" ? r : concatStrings(r, "/", suffix);
        if (pathExists(res)) co_return SourcePath(CanonPath(canonPath(res)));
    }

    if (path.starts_with("nix/"))
        co_return SourcePath(CanonPath(concatStrings(corepkgsPrefix, path.substr(4))));

    co_return errors.make<ThrownError>(
        evalSettings.pureEval
            ? "cannot look up '<%s>' in pure evaluation mode (use '--impure' to override)"
            : "file '%s' was not found in the Nix search path (add it using $NIX_PATH or -I)",
        path
    ).atPos(pos);
} catch (...) {
    co_return result::current_exception();
}


kj::Promise<Result<std::optional<std::string>>>
EvalPaths::resolveSearchPathPath(const SearchPath::Path & value0)
try {
    auto & value = value0.s;
    auto i = searchPathResolved.find(value);
    if (i != searchPathResolved.end()) co_return i->second;

    std::optional<std::string> res;

    if (EvalSettings::isPseudoUrl(value)) {
        try {
            auto storePath = TRY_AWAIT(fetchers::downloadTarball(
                store, EvalSettings::resolvePseudoUrl(value), "source", false)).tree.storePath;
            res = { store->toRealPath(storePath) };
        } catch (FileTransferError & e) {
            e.addTrace(nullptr, "while downloading %s to satisfy NIX_PATH lookup, ignoring search path entry", value);
            logWarning(e.info());
            res = std::nullopt;
        }
    }

    else if (value.starts_with("flake:")) {
        experimentalFeatureSettings.require(Xp::Flakes);
        auto flakeRef = parseFlakeRef(value.substr(6), {}, true, false);
        debug("fetching flake search path element '%s''", value);
        auto storePath =
            TRY_AWAIT(TRY_AWAIT(flakeRef.resolve(store)).fetchTree(store)).first.storePath;
        res = {store->toRealPath(storePath)};
    }

    else {
        auto path = absPath(value);
        if (pathExists(path))
            res = { path };
        else {
            logWarning({
                .msg = HintFmt("Nix search path entry '%1%' does not exist, ignoring", value)
            });
            res = std::nullopt;
        }
    }

    if (res)
        debug("resolved search path element '%s' to '%s'", value, *res);
    else
        debug("failed to resolve search path element '%s'", value);

    searchPathResolved[value] = res;
    co_return res;
} catch (...) {
    co_return result::current_exception();
}


std::string ExternalValueBase::coerceToString(EvalState & state, const PosIdx & pos, NixStringContext & context, StringCoercionMode mode, bool copyToStore) const
{
    state.ctx.errors.make<TypeError>(
        "cannot coerce %1% to a string: %2%", showType(), *this
    ).atPos(pos).debugThrow();
}


bool ExternalValueBase::operator==(const ExternalValueBase & b) const
{
    return false;
}


std::ostream & operator << (std::ostream & str, const ExternalValueBase & v) {
    return v.print(str);
}


}
