Files
lix/lix/libexpr/eval.hh
T
Raito Bezarius de0447c2c6 -------------- ongoing crimes
Change-Id: Iedd51025b3a5a25b6570983726f8a237c53f5192
2025-11-21 17:40:21 +01:00

902 lines
27 KiB
C++

#pragma once
///@file
#include "lix/libexpr/attr-set.hh"
#include "lix/libexpr/eval-error.hh"
#include "lix/libexpr/gc-alloc.hh"
#include "lix/libutil/box_ptr.hh"
#include "lix/libutil/generator.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/source-path.hh"
#include "lix/libutil/types.hh"
#include "lix/libexpr/value.hh"
#include "lix/libexpr/nixexpr.hh"
#include "lix/libexpr/symbol-table.hh"
#include "lix/libutil/config.hh"
#include "lix/libutil/experimental-features.hh"
#include "lix/libexpr/search-path.hh"
#include "lix/libexpr/repl-exit-status.hh"
#include "lix/libutil/backed-string-view.hh"
#include <concepts>
#include <map>
#include <optional>
#include <unordered_map>
#include <functional>
namespace nix {
class Store;
class EvalState;
class StorePath;
struct SingleDerivedPath;
enum RepairFlag : bool;
struct MemoryInputAccessor;
namespace eval_cache {
class EvalCache;
}
std::ostream & operator<<(std::ostream & output, const PrimOp & primOp);
/**
* Info about a constant
*/
struct Constant
{
/**
* Optional type of the constant (known since it is a fixed value).
*
* @todo we should use an enum for this.
*/
ValueType type = nThunk;
/**
* Optional free-form documentation about the constant.
*/
const char * doc = nullptr;
/**
* Whether the constant is impure, and not available in pure mode.
*/
bool impureOnly = false;
};
using ValMap = GcMap<std::string, Value>;
struct alignas(Value::Acb::TAG_ALIGN) Env
{
Env * up;
Value values[0];
};
void printEnvBindings(const EvalState &es, const Expr & expr, const Env & env);
void printEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & env, int lvl = 0);
std::unique_ptr<ValMap> mapStaticEnvBindings(const SymbolTable & st, const StaticEnv & se, const Env & env);
void copyContext(const Value & v, NixStringContext & context);
std::string printValue(EvalState & state, Value & v);
std::ostream & operator << (std::ostream & os, const ValueType t);
/**
* Initialise the evaluator (including Boehm GC, if applicable).
*/
void initLibExpr();
struct RegexCache;
struct DebugTrace {
std::shared_ptr<Pos> pos;
const Expr & expr;
const Env & env;
HintFmt hint;
bool isError;
std::shared_ptr<const DebugTrace> parent;
};
struct DebugState
{
private:
std::weak_ptr<const DebugTrace> latestTrace;
const PosTable & positions;
const SymbolTable & symbols;
public:
std::function<ReplExitStatus(ValMap const & extraEnv, NeverAsync)> errorCallback;
bool stop = false;
bool inDebugger = false;
std::map<const Expr *, const std::shared_ptr<const StaticEnv>> exprEnvs;
int trylevel = 0;
explicit DebugState(
const PosTable & positions,
const SymbolTable & symbols,
std::function<ReplExitStatus(ValMap const & extraEnv, NeverAsync)> errorCallback
)
: positions(positions)
, symbols(symbols)
, errorCallback(errorCallback)
{
assert(errorCallback);
}
void onEvalError(const EvalError * error, const Env & env, const Expr & expr, NeverAsync = {});
const std::shared_ptr<const StaticEnv> staticEnvFor(const Expr & expr) const
{
if (auto i = exprEnvs.find(&expr); i != exprEnvs.end()) {
return i->second;
}
return nullptr;
}
class TraceFrame
{
friend struct DebugState;
template<std::derived_from<EvalError> T>
friend class EvalErrorBuilder;
// holds both the data for this frame *and* a deleter that pulls this frame
// off the trace stack. EvalErrorBuilder uses this for withFrame fake trace
// frames, and to avoid needing to see this class definition in its header.
const std::shared_ptr<const DebugTrace> entry = nullptr;
explicit TraceFrame(std::shared_ptr<const DebugTrace> entry): entry(std::move(entry)) {}
public:
TraceFrame(std::nullptr_t) {}
};
TraceFrame addTrace(DebugTrace t);
/// Enumerates the debug frame stack, from the current frame to the root frame.
/// All values are guaranteed to not be null, but must be pointers because C++.
Generator<const DebugTrace *> traces()
{
for (auto current = latestTrace.lock(); current; current = current->parent) {
co_yield current.get();
}
}
};
struct StaticSymbols
{
const Symbol outPath, drvPath, type, meta, name, value, system, overrides, outputs, outputName,
ignoreNulls, file, line, column, functor, toString, right, wrong, structuredAttrs,
allowedReferences, allowedRequisites, disallowedReferences, disallowedRequisites, maxSize,
maxClosureSize, builder, args, contentAddressed, impure, outputHash, outputHashAlgo,
outputHashMode, recurseForDerivations, description, self, startSet, operator_, key,
path, prefix, outputSpecified;
const Expr::AstSymbols exprSymbols;
explicit StaticSymbols(SymbolTable & symbols);
};
class EvalMemory
{
static constexpr size_t CACHES = 8;
static constexpr size_t CACHE_INCREMENT = sizeof(void *);
/**
* Allocation caches for small values.
*/
void * gcCache[CACHES] = {};
public:
struct Statistics
{
unsigned long nrEnvs = 0;
unsigned long nrValuesInEnvs = 0;
unsigned long nrAttrsets = 0;
unsigned long nrAttrsInAttrsets = 0;
unsigned long nrListElems = 0;
};
EvalMemory();
~EvalMemory();
EvalMemory(const EvalMemory &) = delete;
EvalMemory(EvalMemory &&) = delete;
EvalMemory & operator=(const EvalMemory &) = delete;
EvalMemory & operator=(EvalMemory &&) = delete;
inline void * allocBytes(size_t size);
template<typename T>
inline T * allocType(size_t n = 1);
inline Env & allocEnv(size_t size);
Bindings * allocBindings(size_t capacity);
Value::List * newList(size_t length);
BindingsBuilder buildBindings(SymbolTable & symbols, size_t capacity)
{
return BindingsBuilder(*this, symbols, allocBindings(capacity), capacity);
}
const Statistics getStats() const { return stats; }
private:
Statistics stats;
};
class EvalBuiltins
{
EvalMemory & mem;
SymbolTable & symbols;
public:
explicit EvalBuiltins(
EvalMemory & mem,
SymbolTable & symbols,
const SearchPath & searchPath,
const Path & storeDir,
size_t size = 128
);
/**
* The base environment, containing the builtin functions and
* values.
*/
Env & env;
/**
* The same, but used during parsing to resolve variables.
*/
std::shared_ptr<StaticEnv> staticEnv; // !!! should be private
/**
* Name and documentation about every constant.
*
* Constants from primops are hard to crawl, and their docs will go
* here too.
*/
std::vector<std::pair<std::string, Constant>> constantInfos;
private:
unsigned int baseEnvDispl = 0;
void createBaseEnv(const SearchPath & searchPath, const Path & storeDir);
void addConstant(const std::string & name, const Value & v, Constant info);
void addPrimOp(PrimOpDetails primOp);
Value prepareNixPath(const SearchPath & searchPath);
public:
Value & get(const std::string & name);
struct Doc
{
Pos pos;
std::optional<std::string> name;
size_t arity;
std::vector<std::string> args;
/**
* Unlike the other `doc` fields in this file, this one should never be
* `null`.
*/
const char * doc;
};
std::optional<Doc> getDoc(Value & v);
};
struct CachedEvalFile;
struct EvalRuntimeCaches
{
RootValue vCallFlake;
RootValue vImportedDrvToDerivation;
/**
* Cache used by prim_match() and other regex functions.
*/
std::shared_ptr<RegexCache> regexes;
/**
* A cache from path names to values for evalFile().
*/
std::map<SourcePath, std::shared_ptr<CachedEvalFile>> fileEval;
};
struct EvalErrorContext
{
const PosTable & positions;
DebugState * debug;
template<std::derived_from<EvalError> T, typename... Args>
[[gnu::noinline]]
EvalErrorBuilder<T> make(const Args & ... args) {
return EvalErrorBuilder<T>(positions, debug, args...);
}
};
class EvalPaths
{
struct Responder {
std::function<bool (std::string const &)> canResolve;
std::function<kj::Promise<Result<std::optional<std::string>>> (std::string const &, ref<Store>)> resolve;
};
ref<Store> store;
std::set<std::string> allowedNixPathPrefixes;
std::vector<Responder> searchPathResponders;
SearchPath searchPath_;
bool pureEval;
EvalErrorContext & errors;
public:
EvalPaths(
AsyncIoRoot & aio,
const ref<Store> & store,
SearchPath initialSearchPath,
bool pureEval,
EvalErrorContext & errors
);
void parseAndConfigureEnvironmentNixPath(std::string const & value);
void configureSettingNixPath(std::list<std::string> const & value);
const SearchPath & searchPath() const { return searchPath_; }
std::string resolvePseudoUrl(std::string_view url) {
if (url.starts_with("channel:"))
return "https://channels.nixos.org/" + std::string(url.substr(8)) + "/nixexprs.tar.xz";
else
return std::string(url);
}
bool registerAllowedNixPathPrefix(const std::string & allowedPrefix) {
auto [_, added] = allowedNixPathPrefixes.insert(allowedPrefix);
return added;
}
void registerSearchPathResolver(std::function<bool (std::string const &)> eligible, std::function<kj::Promise<Result<std::optional<std::string>>> (std::string const & value, ref<Store> store)> resolver) {
searchPathResponders.push_back({.canResolve=eligible,.resolve=resolver});
}
private:
struct AllowedPath
{
struct ComponentLess : std::less<>
{
// we'll only use this for string-likes, it's fine. trust me sis.
using is_transparent = void;
};
std::map<std::string, AllowedPath, ComponentLess> children;
bool allowAllChildren = false;
};
/**
* The allowed filesystem paths in restricted or pure evaluation
* mode.
*/
std::optional<AllowedPath> allowedPaths;
/* Cache for calls to addToStore(); maps source paths to the store
paths. */
std::map<SourcePath, StorePath> srcToStore;
std::map<std::string, std::optional<std::string>> searchPathResolved;
/**
* Cache used by checkSourcePath().
*/
std::unordered_map<Path, CheckedSourcePath> resolvedPaths;
public:
/**
* Allow access to a path.
*/
void allowPath(const Path & path);
/**
* Allow access to a store path. Note that this gets remapped to
* the real store path if `store` is a chroot store.
*/
void allowPath(const StorePath & storePath);
/**
* Allow access to a store path and return it as a string.
*/
void allowAndSetStorePathString(const StorePath & storePath, Value & v);
/**
* Check whether access to a path is allowed and throw an error if
* not. Otherwise return the canonicalised path.
*/
CheckedSourcePath checkSourcePath(const SourcePath & path);
/**
* If `path` refers to a directory, then append "/default.nix".
*/
CheckedSourcePath resolveExprPath(SourcePath path);
void checkURI(const std::string & uri);
/**
* When using a diverted store and 'path' is in the Nix store, map
* 'path' to the diverted location (e.g. /nix/store/foo is mapped
* to /home/alice/my-nix/nix/store/foo). However, this is only
* done if the context is not empty, since otherwise we're
* probably trying to read from the actual /nix/store. This is
* intended to distinguish between import-from-derivation and
* sources stored in the actual /nix/store.
*/
Path toRealPath(const Path & path, const NixStringContext & context);
/**
* findFile wants to throw a debuggable error when the requested file
* is not found, but it can't invoke the debugger itself because it's
* async code. This wraps the result-or-error to allow it regardless.
* This happens for copyPathToStore as well, with another error type.
*/
template<typename T, typename E>
struct PathResult : private std::variant<T, EvalErrorBuilder<E>>
{
PathResult(T p) : std::variant<T, EvalErrorBuilder<E>>(std::move(p)) {}
PathResult(EvalErrorBuilder<E> e) : std::variant<T, EvalErrorBuilder<E>>(std::move(e)) {}
T unwrap(NeverAsync = {}) &&
{
return std::visit(
overloaded{
[](T & p) -> T { return std::move(p); },
[](EvalErrorBuilder<E> & e) -> T {
std::move(e).debugThrow(always_progresses);
}
},
static_cast<std::variant<T, EvalErrorBuilder<E>> &>(*this)
);
}
};
/**
* Look up a file in the search path.
*/
kj::Promise<Result<PathResult<SourcePath, ThrownError>>> findFile(const std::string_view path);
kj::Promise<Result<PathResult<SourcePath, ThrownError>>>
findFile(const SearchPath & searchPath, const std::string_view path, const PosIdx pos = noPos);
/**
* Try to resolve a search path value (not the optinal key part)
*
* If the specified search path element is a URI, download it.
*
* If it is not found, return `std::nullopt`
*/
kj::Promise<Result<std::optional<std::string>>>
resolveSearchPathPath(const SearchPath::Path & path);
kj::Promise<Result<PathResult<StorePath, EvalError>>> copyPathToStore(
NixStringContext & context, const SourcePath & path, RepairFlag repair = NoRepair
);
/**
* Create a string representing a store path.
*
* The string is the printed store path with a context containing a
* single `NixStringContextElem::Opaque` element of that store path.
*/
void mkStorePathString(const StorePath & storePath, Value & v);
};
struct EvalStatistics
{
unsigned long nrLookups = 0;
unsigned long nrAvoided = 0;
unsigned long nrOpUpdates = 0;
unsigned long nrOpUpdateValuesCopied = 0;
unsigned long nrListConcats = 0;
unsigned long nrPrimOpCalls = 0;
unsigned long nrFunctionCalls = 0;
unsigned long nrThunks = 0;
bool countCalls = false;
std::map<std::string, size_t> primOpCalls;
std::map<ExprLambda *, size_t> functionCalls;
std::map<PosIdx, size_t> attrSelects;
void addCall(ExprLambda & fun);
};
class Evaluator
{
friend class EvalBuiltins;
friend class EvalState;
EvalState * activeEval = nullptr;
public:
SymbolTable symbols;
PosTable positions;
const StaticSymbols s;
EvalMemory mem;
EvalRuntimeCaches caches;
EvalPaths paths;
EvalBuiltins builtins;
EvalStatistics stats;
/**
* If set, force copying files to the Nix store even if they
* already exist there.
*/
RepairFlag repair;
/**
* Store used to materialise .drv files.
*/
const ref<Store> store;
/**
* Store used to build stuff.
*/
ref<Store> buildStore;
std::unique_ptr<DebugState> debug;
EvalErrorContext errors;
Evaluator(
AsyncIoRoot & aio,
const SearchPath & _searchPath,
ref<Store> store,
std::shared_ptr<Store> buildStore = nullptr,
std::function<ReplExitStatus(EvalState & es, ValMap const & extraEnv)> debugRepl = nullptr
);
Evaluator(const Evaluator &) = delete;
Evaluator(Evaluator &&) = delete;
Evaluator & operator=(const Evaluator &) = delete;
Evaluator & operator=(Evaluator &&) = delete;
/**
* Parse a Nix expression from the specified file.
*/
Expr & parseExprFromFile(const CheckedSourcePath & path);
Expr & parseExprFromFile(const CheckedSourcePath & path, std::shared_ptr<StaticEnv> & staticEnv);
/**
* Parse a Nix expression from the specified string.
*/
Expr & parseExprFromString(
std::string s,
const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv,
const FeatureSettings & xpSettings = featureSettings
);
Expr & parseExprFromString(
std::string s,
const SourcePath & basePath,
const FeatureSettings & xpSettings = featureSettings
);
std::variant<std::unique_ptr<Expr>, ExprReplBindings>
parseReplInput(
std::string s,
const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv,
const FeatureSettings & xpSettings = featureSettings
);
Expr & parseStdin();
/**
* Creates a thunk that will evaluate the given expression when forced.
*/
void evalLazily(Expr & e, Value & v);
private:
Expr * parse(
char * text,
size_t length,
Pos::Origin origin,
const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv,
const FeatureSettings & xpSettings = featureSettings);
std::variant<std::unique_ptr<Expr>, ExprReplBindings>
parse_repl(
char * text,
size_t length,
Pos::Origin origin,
const SourcePath & basePath,
std::shared_ptr<StaticEnv> & staticEnv,
const FeatureSettings & xpSettings = featureSettings
);
public:
BindingsBuilder buildBindings(size_t capacity)
{
return mem.buildBindings(symbols, capacity);
}
/**
* Print statistics, if enabled.
*
* Performs a full memory GC before printing the statistics, so that the
* GC statistics are more accurate.
*/
void maybePrintStats();
/**
* Print statistics, unconditionally, cheaply, without performing a GC first.
*/
void printStatistics();
/**
* Perform a full memory garbage collection - not incremental.
*
* @return true if Nix was built with GC and a GC was performed, false if not.
* The return value is currently not thread safe - just the return value.
*/
bool fullGC();
/**
* Create an `EvalState` in prepation to evaluate some amount of Nix code.
*
* While preparation of evaluation can be done with Evaluator itself only,
* actually evaluating things requires an EvalState. This function creates
* an EvalState and returns it. At most one EvalState per Evaluator may be
* live at any given point, and references to this EvalState must not live
* anywhere except in the returned box, local variables, or arguments. Any
* reference held in an object type is illegal, be it in a lambda capture,
* a pointer member of an object, a hidden member such as arguments passed
* to functions by `std::thread` or `std::async`—all references held where
* they could be copied are moved from are disallowed. EvalState is thus a
* witness type that a given thread may evaluate nix code and must *never*
* be run inside `kj::Promise` context. This is due to a kj limitation, in
* which it is not possible to block on a promise while already running in
* a promise without doing this blocking on a different event loop/thread.
*/
box_ptr<EvalState> begin(AsyncIoRoot & aio);
};
class EvalState
{
friend class Evaluator;
explicit EvalState(AsyncIoRoot & aio, Evaluator & ctx);
public:
Evaluator & ctx;
AsyncIoRoot & aio;
EvalState(const EvalState &) = delete;
EvalState(EvalState &&) = delete;
EvalState & operator=(const EvalState &) = delete;
EvalState & operator=(EvalState &&) = delete;
~EvalState();
/**
* Evaluate an expression read from the given file to normal form.
*/
void evalFile(const SourcePath & path, Value & v);
void resetFileCache();
/**
* Evaluate an expression to normal form
*
* @param [out] v The resulting is stored here.
*/
void eval(Expr & e, Value & v);
/**
* Evaluation the expression, then verify that it has the expected
* type.
*/
inline bool evalBool(Env & env, Expr & e);
inline void evalAttrs(Env & env, Expr & e, Value & v);
inline void evalList(Env & env, Expr & e, Value & v);
/**
* If `v` is a thunk, enter it and overwrite `v` with the result
* of the evaluation of the thunk. If `v` is a delayed function
* application, call the function and overwrite `v` with the
* result. Otherwise, this is a no-op.
*/
inline void forceValue(Value & v, const PosIdx pos);
void tryFixupBlackHolePos(Value & v, PosIdx pos);
/**
* Force a value, then recursively force list elements and
* attributes.
*/
void forceValueDeep(Value & v);
/**
* Force `v`, and then verify that it has the expected type.
*/
NixInt forceInt(Value & v, const PosIdx pos, std::string_view errorCtx);
NixFloat forceFloat(Value & v, const PosIdx pos, std::string_view errorCtx);
bool forceBool(Value & v, const PosIdx pos, std::string_view errorCtx);
void forceAttrs(Value & v, const PosIdx pos, std::string_view errorCtx);
inline void forceList(Value & v, const PosIdx pos, std::string_view errorCtx);
/**
* @param v either lambda or primop
*/
void forceFunction(Value & v, const PosIdx pos, std::string_view errorCtx);
std::string_view forceString(Value & v, const PosIdx pos, std::string_view errorCtx);
std::string_view forceString(Value & v, NixStringContext & context, const PosIdx pos, std::string_view errorCtx);
std::string_view forceStringNoCtx(Value & v, const PosIdx pos, std::string_view errorCtx);
/**
* Realise the given context, and return a mapping from the placeholders
* used to construct the associated value to their final store path
*/
[[nodiscard]] StringMap realiseContext(const NixStringContext & context);
public:
/**
* @return true iff the value `v` denotes a derivation (i.e. a
* set with attribute `type = "derivation"`).
*/
bool isDerivation(Value & v);
std::optional<std::string> tryAttrsToString(const PosIdx pos, Value & v,
NixStringContext & context, StringCoercionMode mode = StringCoercionMode::Strict, bool copyToStore = true);
/**
* String coercion.
*
* Converts strings, paths and derivations to a
* string. If `copyToStore` is set,
* referenced paths are copied to the Nix store as a side effect.
*/
BackedStringView coerceToString(const PosIdx pos, Value & v, NixStringContext & context,
std::string_view errorCtx,
StringCoercionMode mode = StringCoercionMode::Strict, bool copyToStore = true,
bool canonicalizePath = true);
/**
* Path coercion.
*
* Converts strings, paths and derivations to a
* path. The result is guaranteed to be a canonicalised, absolute
* path. Nothing is copied to the store.
*/
SourcePath coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx);
/**
* Like coerceToPath, but the result must be a store path.
*/
StorePath coerceToStorePath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx);
/**
* Part of `coerceToSingleDerivedPath()` without any store IO which is exposed for unit testing only.
*/
std::pair<SingleDerivedPath, std::string_view> coerceToSingleDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx);
/**
* Coerce to `SingleDerivedPath`.
*
* Must be a string which is either a literal store path.
*
* Even more importantly, the string context must be exactly one
* element, which is either a `NixStringContextElem::Opaque` or
* `NixStringContextElem::Built`. (`NixStringContextEleme::DrvDeep`
* is not permitted).
*
* The string is parsed based on the context --- the context is the
* source of truth, and ultimately tells us what we want, and then
* we ensure the string corresponds to it.
*/
SingleDerivedPath coerceToSingleDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx);
private:
inline Value * lookupVar(Env * env, const ExprVar & var, bool noEval);
friend struct ExprVar;
friend struct ExprSet;
friend struct ExprLet;
/**
* Current Nix call stack depth, used with `max-call-depth` setting to throw stack overflow hopefully before we run out of system stack.
*/
size_t callDepth = 0;
public:
/**
* Do a deep equality test between two values. That is, list
* elements and attributes are compared recursively.
*/
bool eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx);
bool isFunctor(Value & fun);
void callFunction(Value & fun, std::span<Value> args, Value & vRes, const PosIdx pos);
void callFunction(Value & fun, Value & arg, Value & vRes, const PosIdx pos)
{
callFunction(fun, {&arg, 1}, vRes, pos);
}
/**
* Automatically call a function for which each argument has a
* default value or has a binding in the `args` map.
*/
void autoCallFunction(Bindings & args, Value & fun, Value & res, PosIdx pos);
void mkPos(Value & v, PosIdx pos);
/**
* Create a string representing a `SingleDerivedPath::Built`.
*
* The string is the printed store path with a context containing a
* single `NixStringContextElem::Built` element of the drv path and
* output name.
*
* @param value Value we are settings
*
* @param b the drv whose output we are making a string for, and the
* output
*
* @param staticOutputPath Output path for that string.
* Will be printed to form string.
*/
void mkOutputString(
Value & value,
const SingleDerivedPath::Built & b,
const StorePath & staticOutputPath);
/**
* Create a string representing a `SingleDerivedPath`.
*
* A combination of `mkStorePathString` and `mkOutputString`.
*/
void mkSingleDerivedPathString(
const SingleDerivedPath & p,
Value & v);
void
concatLists(Value & v, std::span<Value> lists, const PosIdx pos, std::string_view errorCtx);
private:
/**
* Like `mkOutputString` but just creates a raw string, not an
* string Value, which would also have a string context.
*/
std::string mkOutputStringRaw(
const StorePath & staticOutputPath);
/**
* Like `mkSingleDerivedPathStringRaw` but just creates a raw string
* Value, which would also have a string context.
*/
std::string mkSingleDerivedPathStringRaw(
const SingleDerivedPath & p);
};
/**
* @return A string representing the type of the value `v`.
*
* @param withArticle Whether to begin with an english article, e.g. "an
* integer" vs "integer".
*/
std::string_view showType(ValueType type, bool withArticle = true);
std::string showType(const Value & v);
static constexpr std::string_view corepkgsPrefix{"/__corepkgs__/"};
}
#include "lix/libexpr/eval-inline.hh" // IWYU pragma: keep