diff options
author | Vincent Ambo <tazjin@google.com> | 2020-05-17T14·52+0100 |
---|---|---|
committer | Vincent Ambo <tazjin@google.com> | 2020-05-17T14·52+0100 |
commit | 7994fd1d545cc5c876d6f21db7ddf9185d23dad6 (patch) | |
tree | 32dd695785378c5b9c8be97fc583e9dfc62cb105 /third_party/nix/src/libexpr | |
parent | cf8cd640c1adf74a3706efbcb0ea4625da106fb2 (diff) | |
parent | 90b3b31dc27f31e9b11653a636025d29ddb087a3 (diff) |
Add 'third_party/nix/' from commit 'be66c7a6b24e3c3c6157fd37b86c7203d14acf10' r/724
git-subtree-dir: third_party/nix git-subtree-mainline: cf8cd640c1adf74a3706efbcb0ea4625da106fb2 git-subtree-split: be66c7a6b24e3c3c6157fd37b86c7203d14acf10
Diffstat (limited to 'third_party/nix/src/libexpr')
35 files changed, 9111 insertions, 0 deletions
diff --git a/third_party/nix/src/libexpr/attr-path.cc b/third_party/nix/src/libexpr/attr-path.cc new file mode 100644 index 000000000000..b0f80db32a88 --- /dev/null +++ b/third_party/nix/src/libexpr/attr-path.cc @@ -0,0 +1,96 @@ +#include "attr-path.hh" +#include "eval-inline.hh" +#include "util.hh" + + +namespace nix { + + +static Strings parseAttrPath(const string & s) +{ + Strings res; + string cur; + string::const_iterator i = s.begin(); + while (i != s.end()) { + if (*i == '.') { + res.push_back(cur); + cur.clear(); + } else if (*i == '"') { + ++i; + while (1) { + if (i == s.end()) + throw Error(format("missing closing quote in selection path '%1%'") % s); + if (*i == '"') break; + cur.push_back(*i++); + } + } else + cur.push_back(*i); + ++i; + } + if (!cur.empty()) res.push_back(cur); + return res; +} + + +Value * findAlongAttrPath(EvalState & state, const string & attrPath, + Bindings & autoArgs, Value & vIn) +{ + Strings tokens = parseAttrPath(attrPath); + + Error attrError = + Error(format("attribute selection path '%1%' does not match expression") % attrPath); + + Value * v = &vIn; + + for (auto & attr : tokens) { + + /* Is i an index (integer) or a normal attribute name? */ + enum { apAttr, apIndex } apType = apAttr; + unsigned int attrIndex; + if (string2Int(attr, attrIndex)) apType = apIndex; + + /* Evaluate the expression. */ + Value * vNew = state.allocValue(); + state.autoCallFunction(autoArgs, *v, *vNew); + v = vNew; + state.forceValue(*v); + + /* It should evaluate to either a set or an expression, + according to what is specified in the attrPath. */ + + if (apType == apAttr) { + + if (v->type != tAttrs) + throw TypeError( + format("the expression selected by the selection path '%1%' should be a set but is %2%") + % attrPath % showType(*v)); + + if (attr.empty()) + throw Error(format("empty attribute name in selection path '%1%'") % attrPath); + + Bindings::iterator a = v->attrs->find(state.symbols.create(attr)); + if (a == v->attrs->end()) + throw Error(format("attribute '%1%' in selection path '%2%' not found") % attr % attrPath); + v = &*a->value; + } + + else if (apType == apIndex) { + + if (!v->isList()) + throw TypeError( + format("the expression selected by the selection path '%1%' should be a list but is %2%") + % attrPath % showType(*v)); + + if (attrIndex >= v->listSize()) + throw Error(format("list index %1% in selection path '%2%' is out of range") % attrIndex % attrPath); + + v = v->listElems()[attrIndex]; + } + + } + + return v; +} + + +} diff --git a/third_party/nix/src/libexpr/attr-path.hh b/third_party/nix/src/libexpr/attr-path.hh new file mode 100644 index 000000000000..46a341950939 --- /dev/null +++ b/third_party/nix/src/libexpr/attr-path.hh @@ -0,0 +1,13 @@ +#pragma once + +#include "eval.hh" + +#include <string> +#include <map> + +namespace nix { + +Value * findAlongAttrPath(EvalState & state, const string & attrPath, + Bindings & autoArgs, Value & vIn); + +} diff --git a/third_party/nix/src/libexpr/attr-set.cc b/third_party/nix/src/libexpr/attr-set.cc new file mode 100644 index 000000000000..0785897d2513 --- /dev/null +++ b/third_party/nix/src/libexpr/attr-set.cc @@ -0,0 +1,52 @@ +#include "attr-set.hh" +#include "eval-inline.hh" + +#include <algorithm> + + +namespace nix { + + +/* Allocate a new array of attributes for an attribute set with a specific + capacity. The space is implicitly reserved after the Bindings + structure. */ +Bindings * EvalState::allocBindings(size_t capacity) +{ + if (capacity > std::numeric_limits<Bindings::size_t>::max()) + throw Error("attribute set of size %d is too big", capacity); + return new (allocBytes(sizeof(Bindings) + sizeof(Attr) * capacity)) Bindings((Bindings::size_t) capacity); +} + + +void EvalState::mkAttrs(Value & v, size_t capacity) +{ + if (capacity == 0) { + v = vEmptySet; + return; + } + clearValue(v); + v.type = tAttrs; + v.attrs = allocBindings(capacity); + nrAttrsets++; + nrAttrsInAttrsets += capacity; +} + + +/* Create a new attribute named 'name' on an existing attribute set stored + in 'vAttrs' and return the newly allocated Value which is associated with + this attribute. */ +Value * EvalState::allocAttr(Value & vAttrs, const Symbol & name) +{ + Value * v = allocValue(); + vAttrs.attrs->push_back(Attr(name, v)); + return v; +} + + +void Bindings::sort() +{ + std::sort(begin(), end()); +} + + +} diff --git a/third_party/nix/src/libexpr/attr-set.hh b/third_party/nix/src/libexpr/attr-set.hh new file mode 100644 index 000000000000..3119a1848af2 --- /dev/null +++ b/third_party/nix/src/libexpr/attr-set.hh @@ -0,0 +1,95 @@ +#pragma once + +#include "nixexpr.hh" +#include "symbol-table.hh" + +#include <algorithm> + +namespace nix { + + +class EvalState; +struct Value; + +/* Map one attribute name to its value. */ +struct Attr +{ + Symbol name; + Value * value; + Pos * pos; + Attr(Symbol name, Value * value, Pos * pos = &noPos) + : name(name), value(value), pos(pos) { }; + Attr() : pos(&noPos) { }; + bool operator < (const Attr & a) const + { + return name < a.name; + } +}; + +/* Bindings contains all the attributes of an attribute set. It is defined + by its size and its capacity, the capacity being the number of Attr + elements allocated after this structure, while the size corresponds to + the number of elements already inserted in this structure. */ +class Bindings +{ +public: + typedef uint32_t size_t; + +private: + size_t size_, capacity_; + Attr attrs[0]; + + Bindings(size_t capacity) : size_(0), capacity_(capacity) { } + Bindings(const Bindings & bindings) = delete; + +public: + size_t size() const { return size_; } + + bool empty() const { return !size_; } + + typedef Attr * iterator; + + void push_back(const Attr & attr) + { + assert(size_ < capacity_); + attrs[size_++] = attr; + } + + iterator find(const Symbol & name) + { + Attr key(name, 0); + iterator i = std::lower_bound(begin(), end(), key); + if (i != end() && i->name == name) return i; + return end(); + } + + iterator begin() { return &attrs[0]; } + iterator end() { return &attrs[size_]; } + + Attr & operator[](size_t pos) + { + return attrs[pos]; + } + + void sort(); + + size_t capacity() { return capacity_; } + + /* Returns the attributes in lexicographically sorted order. */ + std::vector<const Attr *> lexicographicOrder() const + { + std::vector<const Attr *> res; + res.reserve(size_); + for (size_t n = 0; n < size_; n++) + res.emplace_back(&attrs[n]); + std::sort(res.begin(), res.end(), [](const Attr * a, const Attr * b) { + return (const string &) a->name < (const string &) b->name; + }); + return res; + } + + friend class EvalState; +}; + + +} diff --git a/third_party/nix/src/libexpr/common-eval-args.cc b/third_party/nix/src/libexpr/common-eval-args.cc new file mode 100644 index 000000000000..13950ab8d169 --- /dev/null +++ b/third_party/nix/src/libexpr/common-eval-args.cc @@ -0,0 +1,59 @@ +#include "common-eval-args.hh" +#include "shared.hh" +#include "download.hh" +#include "util.hh" +#include "eval.hh" + +namespace nix { + +MixEvalArgs::MixEvalArgs() +{ + mkFlag() + .longName("arg") + .description("argument to be passed to Nix functions") + .labels({"name", "expr"}) + .handler([&](std::vector<std::string> ss) { autoArgs[ss[0]] = 'E' + ss[1]; }); + + mkFlag() + .longName("argstr") + .description("string-valued argument to be passed to Nix functions") + .labels({"name", "string"}) + .handler([&](std::vector<std::string> ss) { autoArgs[ss[0]] = 'S' + ss[1]; }); + + mkFlag() + .shortName('I') + .longName("include") + .description("add a path to the list of locations used to look up <...> file names") + .label("path") + .handler([&](std::string s) { searchPath.push_back(s); }); +} + +Bindings * MixEvalArgs::getAutoArgs(EvalState & state) +{ + Bindings * res = state.allocBindings(autoArgs.size()); + for (auto & i : autoArgs) { + Value * v = state.allocValue(); + if (i.second[0] == 'E') + state.mkThunk_(*v, state.parseExprFromString(string(i.second, 1), absPath("."))); + else + mkString(*v, string(i.second, 1)); + res->push_back(Attr(state.symbols.create(i.first), v)); + } + res->sort(); + return res; +} + +Path lookupFileArg(EvalState & state, string s) +{ + if (isUri(s)) { + CachedDownloadRequest request(s); + request.unpack = true; + return getDownloader()->downloadCached(state.store, request).path; + } else if (s.size() > 2 && s.at(0) == '<' && s.at(s.size() - 1) == '>') { + Path p = s.substr(1, s.size() - 2); + return state.findFile(p); + } else + return absPath(s); +} + +} diff --git a/third_party/nix/src/libexpr/common-eval-args.hh b/third_party/nix/src/libexpr/common-eval-args.hh new file mode 100644 index 000000000000..be7fda783783 --- /dev/null +++ b/third_party/nix/src/libexpr/common-eval-args.hh @@ -0,0 +1,26 @@ +#pragma once + +#include "args.hh" + +namespace nix { + +class Store; +class EvalState; +class Bindings; + +struct MixEvalArgs : virtual Args +{ + MixEvalArgs(); + + Bindings * getAutoArgs(EvalState & state); + + Strings searchPath; + +private: + + std::map<std::string, std::string> autoArgs; +}; + +Path lookupFileArg(EvalState & state, string s); + +} diff --git a/third_party/nix/src/libexpr/eval-inline.hh b/third_party/nix/src/libexpr/eval-inline.hh new file mode 100644 index 000000000000..c27116e3b448 --- /dev/null +++ b/third_party/nix/src/libexpr/eval-inline.hh @@ -0,0 +1,95 @@ +#pragma once + +#include "eval.hh" + +#define LocalNoInline(f) static f __attribute__((noinline)); f +#define LocalNoInlineNoReturn(f) static f __attribute__((noinline, noreturn)); f + +namespace nix { + +LocalNoInlineNoReturn(void throwEvalError(const char * s, const Pos & pos)) +{ + throw EvalError(format(s) % pos); +} + +LocalNoInlineNoReturn(void throwTypeError(const char * s, const Value & v)) +{ + throw TypeError(format(s) % showType(v)); +} + + +LocalNoInlineNoReturn(void throwTypeError(const char * s, const Value & v, const Pos & pos)) +{ + throw TypeError(format(s) % showType(v) % pos); +} + + +void EvalState::forceValue(Value & v, const Pos & pos) +{ + if (v.type == tThunk) { + Env * env = v.thunk.env; + Expr * expr = v.thunk.expr; + try { + v.type = tBlackhole; + //checkInterrupt(); + expr->eval(*this, *env, v); + } catch (...) { + v.type = tThunk; + v.thunk.env = env; + v.thunk.expr = expr; + throw; + } + } + else if (v.type == tApp) + callFunction(*v.app.left, *v.app.right, v, noPos); + else if (v.type == tBlackhole) + throwEvalError("infinite recursion encountered, at %1%", pos); +} + + +inline void EvalState::forceAttrs(Value & v) +{ + forceValue(v); + if (v.type != tAttrs) + throwTypeError("value is %1% while a set was expected", v); +} + + +inline void EvalState::forceAttrs(Value & v, const Pos & pos) +{ + forceValue(v); + if (v.type != tAttrs) + throwTypeError("value is %1% while a set was expected, at %2%", v, pos); +} + + +inline void EvalState::forceList(Value & v) +{ + forceValue(v); + if (!v.isList()) + throwTypeError("value is %1% while a list was expected", v); +} + + +inline void EvalState::forceList(Value & v, const Pos & pos) +{ + forceValue(v); + if (!v.isList()) + throwTypeError("value is %1% while a list was expected, at %2%", v, pos); +} + +/* Note: Various places expect the allocated memory to be zeroed. */ +inline void * allocBytes(size_t n) +{ + void * p; +#if HAVE_BOEHMGC + p = GC_MALLOC(n); +#else + p = calloc(n, 1); +#endif + if (!p) throw std::bad_alloc(); + return p; +} + + +} diff --git a/third_party/nix/src/libexpr/eval.cc b/third_party/nix/src/libexpr/eval.cc new file mode 100644 index 000000000000..3426afb6cf6e --- /dev/null +++ b/third_party/nix/src/libexpr/eval.cc @@ -0,0 +1,1991 @@ +#include "eval.hh" +#include "hash.hh" +#include "util.hh" +#include "store-api.hh" +#include "derivations.hh" +#include "globals.hh" +#include "eval-inline.hh" +#include "download.hh" +#include "json.hh" +#include "function-trace.hh" + +#include <algorithm> +#include <chrono> +#include <cstring> +#include <unistd.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <iostream> +#include <fstream> + +#include <sys/resource.h> + +#if HAVE_BOEHMGC + +#include <gc/gc.h> +#include <gc/gc_cpp.h> + +#endif + +namespace nix { + + +static char * dupString(const char * s) +{ + char * t; +#if HAVE_BOEHMGC + t = GC_STRDUP(s); +#else + t = strdup(s); +#endif + if (!t) throw std::bad_alloc(); + return t; +} + + +static void printValue(std::ostream & str, std::set<const Value *> & active, const Value & v) +{ + checkInterrupt(); + + if (active.find(&v) != active.end()) { + str << "<CYCLE>"; + return; + } + active.insert(&v); + + switch (v.type) { + case tInt: + str << v.integer; + break; + case tBool: + str << (v.boolean ? "true" : "false"); + break; + case tString: + str << "\""; + for (const char * i = v.string.s; *i; i++) + if (*i == '\"' || *i == '\\') str << "\\" << *i; + else if (*i == '\n') str << "\\n"; + else if (*i == '\r') str << "\\r"; + else if (*i == '\t') str << "\\t"; + else str << *i; + str << "\""; + break; + case tPath: + str << v.path; // !!! escaping? + break; + case tNull: + str << "null"; + break; + case tAttrs: { + str << "{ "; + for (auto & i : v.attrs->lexicographicOrder()) { + str << i->name << " = "; + printValue(str, active, *i->value); + str << "; "; + } + str << "}"; + break; + } + case tList1: + case tList2: + case tListN: + str << "[ "; + for (unsigned int n = 0; n < v.listSize(); ++n) { + printValue(str, active, *v.listElems()[n]); + str << " "; + } + str << "]"; + break; + case tThunk: + case tApp: + str << "<CODE>"; + break; + case tLambda: + str << "<LAMBDA>"; + break; + case tPrimOp: + str << "<PRIMOP>"; + break; + case tPrimOpApp: + str << "<PRIMOP-APP>"; + break; + case tExternal: + str << *v.external; + break; + case tFloat: + str << v.fpoint; + break; + default: + throw Error("invalid value"); + } + + active.erase(&v); +} + + +std::ostream & operator << (std::ostream & str, const Value & v) +{ + std::set<const Value *> active; + printValue(str, active, v); + return str; +} + + +const Value *getPrimOp(const Value &v) { + const Value * primOp = &v; + while (primOp->type == tPrimOpApp) { + primOp = primOp->primOpApp.left; + } + assert(primOp->type == tPrimOp); + return primOp; +} + + +string showType(const Value & v) +{ + switch (v.type) { + case tInt: return "an integer"; + case tBool: return "a boolean"; + case tString: return v.string.context ? "a string with context" : "a string"; + case tPath: return "a path"; + case tNull: return "null"; + case tAttrs: return "a set"; + case tList1: case tList2: case tListN: return "a list"; + case tThunk: return "a thunk"; + case tApp: return "a function application"; + case tLambda: return "a function"; + case tBlackhole: return "a black hole"; + case tPrimOp: + return fmt("the built-in function '%s'", string(v.primOp->name)); + case tPrimOpApp: + return fmt("the partially applied built-in function '%s'", string(getPrimOp(v)->primOp->name)); + case tExternal: return v.external->showType(); + case tFloat: return "a float"; + } + abort(); +} + + +#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.set()) { + return name.symbol; + } else { + Value nameValue; + name.expr->eval(state, env, nameValue); + state.forceStringNoCtx(nameValue); + return state.symbols.create(nameValue.string.s); + } +} + + +static bool gcInitialised = false; + +void initGC() +{ + if (gcInitialised) 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); + + /* We don't have any roots in data segments, so don't scan from + there. */ + GC_set_no_dls(1); + + GC_INIT(); + + 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")) { + size_t size = 32 * 1024 * 1024; +#if HAVE_SYSCONF && defined(_SC_PAGESIZE) && defined(_SC_PHYS_PAGES) + size_t maxSize = 384 * 1024 * 1024; + long pageSize = sysconf(_SC_PAGESIZE); + long pages = sysconf(_SC_PHYS_PAGES); + if (pageSize != -1) + size = (pageSize * pages) / 4; // 25% of RAM + if (size > maxSize) size = maxSize; +#endif + debug(format("setting initial heap size to %1% bytes") % size); + GC_expand_hp(size); + } + +#endif + + gcInitialised = true; +} + + +/* Very hacky way to parse $NIX_PATH, which is colon-separated, but + can contain URLs (e.g. "nixpkgs=https://bla...:foo=https://"). */ +static Strings parseNixPath(const string & s) +{ + Strings res; + + auto p = s.begin(); + + while (p != s.end()) { + auto start = p; + auto start2 = p; + + while (p != s.end() && *p != ':') { + if (*p == '=') start2 = p + 1; + ++p; + } + + if (p == s.end()) { + if (p != start) res.push_back(std::string(start, p)); + break; + } + + if (*p == ':') { + if (isUri(std::string(start2, s.end()))) { + ++p; + while (p != s.end() && *p != ':') ++p; + } + res.push_back(std::string(start, p)); + if (p == s.end()) break; + } + + ++p; + } + + return res; +} + + +EvalState::EvalState(const Strings & _searchPath, ref<Store> store) + : sWith(symbols.create("<with>")) + , sOutPath(symbols.create("outPath")) + , sDrvPath(symbols.create("drvPath")) + , sType(symbols.create("type")) + , sMeta(symbols.create("meta")) + , sName(symbols.create("name")) + , sValue(symbols.create("value")) + , sSystem(symbols.create("system")) + , sOverrides(symbols.create("__overrides")) + , sOutputs(symbols.create("outputs")) + , sOutputName(symbols.create("outputName")) + , sIgnoreNulls(symbols.create("__ignoreNulls")) + , sFile(symbols.create("file")) + , sLine(symbols.create("line")) + , sColumn(symbols.create("column")) + , sFunctor(symbols.create("__functor")) + , sToString(symbols.create("__toString")) + , sRight(symbols.create("right")) + , sWrong(symbols.create("wrong")) + , sStructuredAttrs(symbols.create("__structuredAttrs")) + , sBuilder(symbols.create("builder")) + , sArgs(symbols.create("args")) + , sOutputHash(symbols.create("outputHash")) + , sOutputHashAlgo(symbols.create("outputHashAlgo")) + , sOutputHashMode(symbols.create("outputHashMode")) + , repair(NoRepair) + , store(store) + , baseEnv(allocEnv(128)) + , staticBaseEnv(false, 0) +{ + countCalls = getEnv("NIX_COUNT_CALLS", "0") != "0"; + + assert(gcInitialised); + + static_assert(sizeof(Env) <= 16, "environment must be <= 16 bytes"); + + /* Initialise the Nix expression search path. */ + if (!evalSettings.pureEval) { + Strings paths = parseNixPath(getEnv("NIX_PATH", "")); + for (auto & i : _searchPath) addToSearchPath(i); + for (auto & i : paths) addToSearchPath(i); + } + addToSearchPath("nix=" + canonPath(settings.nixDataDir + "/nix/corepkgs", true)); + + if (evalSettings.restrictEval || evalSettings.pureEval) { + allowedPaths = PathSet(); + + for (auto & i : searchPath) { + auto r = resolveSearchPathElem(i); + if (!r.first) continue; + + auto path = r.second; + + if (store->isInStore(r.second)) { + PathSet closure; + store->computeFSClosure(store->toStorePath(r.second), closure); + for (auto & path : closure) + allowedPaths->insert(path); + } else + allowedPaths->insert(r.second); + } + } + + clearValue(vEmptySet); + vEmptySet.type = tAttrs; + vEmptySet.attrs = allocBindings(0); + + createBaseEnv(); +} + + +EvalState::~EvalState() +{ +} + + +Path EvalState::checkSourcePath(const Path & path_) +{ + if (!allowedPaths) return path_; + + auto i = resolvedPaths.find(path_); + if (i != resolvedPaths.end()) + return i->second; + + bool found = false; + + /* First canonicalize the path without symlinks, so we make sure an + * attacker can't append ../../... to a path that would be in allowedPaths + * and thus leak symlink targets. + */ + Path abspath = canonPath(path_); + + for (auto & i : *allowedPaths) { + if (isDirOrInDir(abspath, i)) { + found = true; + break; + } + } + + if (!found) + throw RestrictedPathError("access to path '%1%' is forbidden in restricted mode", abspath); + + /* Resolve symlinks. */ + debug(format("checking access to '%s'") % abspath); + Path path = canonPath(abspath, true); + + for (auto & i : *allowedPaths) { + if (isDirOrInDir(path, i)) { + resolvedPaths[path_] = path; + return path; + } + } + + throw RestrictedPathError("access to path '%1%' is forbidden in restricted mode", path); +} + + +void EvalState::checkURI(const std::string & uri) +{ + if (!evalSettings.restrictEval) return; + + /* 'uri' should be equal to a prefix, or in a subdirectory of a + prefix. Thus, the prefix https://github.co does not permit + access to https://github.com. Note: this allows 'http://' and + 'https://' as prefixes for any http/https URI. */ + for (auto & prefix : evalSettings.allowedUris.get()) + if (uri == prefix || + (uri.size() > prefix.size() + && prefix.size() > 0 + && hasPrefix(uri, prefix) + && (prefix[prefix.size() - 1] == '/' || uri[prefix.size()] == '/'))) + return; + + /* If the URI is a path, then check it against allowedPaths as + well. */ + if (hasPrefix(uri, "/")) { + checkSourcePath(uri); + return; + } + + if (hasPrefix(uri, "file://")) { + checkSourcePath(std::string(uri, 7)); + return; + } + + throw RestrictedPathError("access to URI '%s' is forbidden in restricted mode", uri); +} + + +Path EvalState::toRealPath(const Path & path, const PathSet & context) +{ + // FIXME: check whether 'path' is in 'context'. + return + !context.empty() && store->isInStore(path) + ? store->toRealPath(path) + : path; +}; + + +Value * EvalState::addConstant(const string & name, Value & v) +{ + Value * v2 = allocValue(); + *v2 = v; + staticBaseEnv.vars[symbols.create(name)] = baseEnvDispl; + baseEnv.values[baseEnvDispl++] = v2; + string name2 = string(name, 0, 2) == "__" ? string(name, 2) : name; + baseEnv.values[0]->attrs->push_back(Attr(symbols.create(name2), v2)); + return v2; +} + + +Value * EvalState::addPrimOp(const string & name, + size_t arity, PrimOpFun primOp) +{ + if (arity == 0) { + Value v; + primOp(*this, noPos, nullptr, v); + return addConstant(name, v); + } + Value * v = allocValue(); + string name2 = string(name, 0, 2) == "__" ? string(name, 2) : name; + Symbol sym = symbols.create(name2); + v->type = tPrimOp; + v->primOp = new PrimOp(primOp, arity, sym); + staticBaseEnv.vars[symbols.create(name)] = baseEnvDispl; + baseEnv.values[baseEnvDispl++] = v; + baseEnv.values[0]->attrs->push_back(Attr(sym, v)); + return v; +} + + +Value & EvalState::getBuiltin(const string & name) +{ + return *baseEnv.values[0]->attrs->find(symbols.create(name))->value; +} + + +/* Every "format" object (even temporary) takes up a few hundred bytes + of stack space, which is a real killer in the recursive + evaluator. So here are some helper functions for throwing + exceptions. */ + +LocalNoInlineNoReturn(void throwEvalError(const char * s, const string & s2)) +{ + throw EvalError(format(s) % s2); +} + +LocalNoInlineNoReturn(void throwEvalError(const char * s, const string & s2, const Pos & pos)) +{ + throw EvalError(format(s) % s2 % pos); +} + +LocalNoInlineNoReturn(void throwEvalError(const char * s, const string & s2, const string & s3)) +{ + throw EvalError(format(s) % s2 % s3); +} + +LocalNoInlineNoReturn(void throwEvalError(const char * s, const string & s2, const string & s3, const Pos & pos)) +{ + throw EvalError(format(s) % s2 % s3 % pos); +} + +LocalNoInlineNoReturn(void throwEvalError(const char * s, const Symbol & sym, const Pos & p1, const Pos & p2)) +{ + throw EvalError(format(s) % sym % p1 % p2); +} + +LocalNoInlineNoReturn(void throwTypeError(const char * s, const Pos & pos)) +{ + throw TypeError(format(s) % pos); +} + +LocalNoInlineNoReturn(void throwTypeError(const char * s, const string & s1)) +{ + throw TypeError(format(s) % s1); +} + +LocalNoInlineNoReturn(void throwTypeError(const char * s, const ExprLambda & fun, const Symbol & s2, const Pos & pos)) +{ + throw TypeError(format(s) % fun.showNamePos() % s2 % pos); +} + +LocalNoInlineNoReturn(void throwAssertionError(const char * s, const string & s1, const Pos & pos)) +{ + throw AssertionError(format(s) % s1 % pos); +} + +LocalNoInlineNoReturn(void throwUndefinedVarError(const char * s, const string & s1, const Pos & pos)) +{ + throw UndefinedVarError(format(s) % s1 % pos); +} + +LocalNoInline(void addErrorPrefix(Error & e, const char * s, const string & s2)) +{ + e.addPrefix(format(s) % s2); +} + +LocalNoInline(void addErrorPrefix(Error & e, const char * s, const ExprLambda & fun, const Pos & pos)) +{ + e.addPrefix(format(s) % fun.showNamePos() % pos); +} + +LocalNoInline(void addErrorPrefix(Error & e, const char * s, const string & s2, const Pos & pos)) +{ + e.addPrefix(format(s) % s2 % pos); +} + + +void mkString(Value & v, const char * s) +{ + mkStringNoCopy(v, dupString(s)); +} + + +Value & mkString(Value & v, const string & s, const PathSet & context) +{ + mkString(v, s.c_str()); + if (!context.empty()) { + size_t n = 0; + v.string.context = (const char * *) + allocBytes((context.size() + 1) * sizeof(char *)); + for (auto & i : context) + v.string.context[n++] = dupString(i.c_str()); + v.string.context[n] = 0; + } + return v; +} + + +void mkPath(Value & v, const char * s) +{ + mkPathNoCopy(v, dupString(s)); +} + + +inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval) +{ + for (size_t l = var.level; l; --l, env = env->up) ; + + if (!var.fromWith) return env->values[var.displ]; + + while (1) { + if (env->type == Env::HasWithExpr) { + if (noEval) return 0; + Value * v = allocValue(); + evalAttrs(*env->up, (Expr *) env->values[0], *v); + env->values[0] = v; + env->type = Env::HasWithAttrs; + } + Bindings::iterator j = env->values[0]->attrs->find(var.name); + if (j != env->values[0]->attrs->end()) { + if (countCalls && j->pos) attrSelects[*j->pos]++; + return j->value; + } + if (!env->prevWith) + throwUndefinedVarError("undefined variable '%1%' at %2%", var.name, var.pos); + for (size_t l = env->prevWith; l; --l, env = env->up) ; + } +} + + +std::atomic<uint64_t> nrValuesFreed{0}; + +void finalizeValue(void * obj, void * data) +{ + nrValuesFreed++; +} + +Value * EvalState::allocValue() +{ + nrValues++; + auto v = (Value *) allocBytes(sizeof(Value)); + //GC_register_finalizer_no_order(v, finalizeValue, nullptr, nullptr, nullptr); + return v; +} + + +Env & EvalState::allocEnv(size_t size) +{ + if (size > std::numeric_limits<decltype(Env::size)>::max()) + throw Error("environment size %d is too big", size); + + nrEnvs++; + nrValuesInEnvs += size; + Env * env = (Env *) allocBytes(sizeof(Env) + size * sizeof(Value *)); + env->size = (decltype(Env::size)) size; + env->type = Env::Plain; + + /* We assume that env->values has been cleared by the allocator; maybeThunk() and lookupVar fromWith expect this. */ + + return *env; +} + + +void EvalState::mkList(Value & v, size_t size) +{ + clearValue(v); + if (size == 1) + v.type = tList1; + else if (size == 2) + v.type = tList2; + else { + v.type = tListN; + v.bigList.size = size; + v.bigList.elems = size ? (Value * *) allocBytes(size * sizeof(Value *)) : 0; + } + nrListElems += size; +} + + +unsigned long nrThunks = 0; + +static inline void mkThunk(Value & v, Env & env, Expr * expr) +{ + v.type = tThunk; + v.thunk.env = &env; + v.thunk.expr = expr; + nrThunks++; +} + + +void EvalState::mkThunk_(Value & v, Expr * expr) +{ + mkThunk(v, baseEnv, expr); +} + + +void EvalState::mkPos(Value & v, Pos * pos) +{ + if (pos && pos->file.set()) { + mkAttrs(v, 3); + mkString(*allocAttr(v, sFile), pos->file); + mkInt(*allocAttr(v, sLine), pos->line); + mkInt(*allocAttr(v, sColumn), pos->column); + v.attrs->sort(); + } else + mkNull(v); +} + + +/* Create a thunk for the delayed computation of the given expression + in the given environment. But if the expression is a variable, + then look it up right away. This significantly reduces the number + of thunks allocated. */ +Value * Expr::maybeThunk(EvalState & state, Env & env) +{ + Value * v = state.allocValue(); + mkThunk(*v, env, this); + return v; +} + + +unsigned long nrAvoided = 0; + +Value * ExprVar::maybeThunk(EvalState & state, Env & env) +{ + Value * v = state.lookupVar(&env, *this, true); + /* The value might not be initialised in the environment yet. + In that case, ignore it. */ + if (v) { nrAvoided++; return v; } + return Expr::maybeThunk(state, env); +} + + +Value * ExprString::maybeThunk(EvalState & state, Env & env) +{ + nrAvoided++; + return &v; +} + +Value * ExprInt::maybeThunk(EvalState & state, Env & env) +{ + nrAvoided++; + return &v; +} + +Value * ExprFloat::maybeThunk(EvalState & state, Env & env) +{ + nrAvoided++; + return &v; +} + +Value * ExprPath::maybeThunk(EvalState & state, Env & env) +{ + nrAvoided++; + return &v; +} + + +void EvalState::evalFile(const Path & path_, Value & v) +{ + auto path = checkSourcePath(path_); + + FileEvalCache::iterator i; + if ((i = fileEvalCache.find(path)) != fileEvalCache.end()) { + v = i->second; + return; + } + + Path path2 = resolveExprPath(path); + if ((i = fileEvalCache.find(path2)) != fileEvalCache.end()) { + v = i->second; + return; + } + + printTalkative("evaluating file '%1%'", path2); + Expr * e = nullptr; + + auto j = fileParseCache.find(path2); + if (j != fileParseCache.end()) + e = j->second; + + if (!e) + e = parseExprFromFile(checkSourcePath(path2)); + + fileParseCache[path2] = e; + + try { + eval(e, v); + } catch (Error & e) { + addErrorPrefix(e, "while evaluating the file '%1%':\n", path2); + throw; + } + + fileEvalCache[path2] = v; + if (path != path2) fileEvalCache[path] = v; +} + + +void EvalState::resetFileCache() +{ + fileEvalCache.clear(); + fileParseCache.clear(); +} + + +void EvalState::eval(Expr * e, Value & v) +{ + e->eval(*this, baseEnv, v); +} + + +inline bool EvalState::evalBool(Env & env, Expr * e) +{ + Value v; + e->eval(*this, env, v); + if (v.type != tBool) + throwTypeError("value is %1% while a Boolean was expected", v); + return v.boolean; +} + + +inline bool EvalState::evalBool(Env & env, Expr * e, const Pos & pos) +{ + Value v; + e->eval(*this, env, v); + if (v.type != tBool) + throwTypeError("value is %1% while a Boolean was expected, at %2%", v, pos); + return v.boolean; +} + + +inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v) +{ + e->eval(*this, env, v); + if (v.type != tAttrs) + throwTypeError("value is %1% while a set was expected", v); +} + + +void Expr::eval(EvalState & state, Env & env, Value & v) +{ + abort(); +} + + +void ExprInt::eval(EvalState & state, Env & env, Value & v) +{ + v = this->v; +} + + +void ExprFloat::eval(EvalState & state, Env & env, Value & v) +{ + v = this->v; +} + +void ExprString::eval(EvalState & state, Env & env, Value & v) +{ + v = this->v; +} + + +void ExprPath::eval(EvalState & state, Env & env, Value & v) +{ + v = this->v; +} + + +void ExprAttrs::eval(EvalState & state, Env & env, Value & v) +{ + state.mkAttrs(v, attrs.size() + dynamicAttrs.size()); + Env *dynamicEnv = &env; + + if (recursive) { + /* Create a new environment that contains the attributes in + this `rec'. */ + Env & env2(state.allocEnv(attrs.size())); + env2.up = &env; + dynamicEnv = &env2; + + AttrDefs::iterator overrides = attrs.find(state.sOverrides); + bool hasOverrides = overrides != attrs.end(); + + /* The recursive attributes are evaluated in the new + environment, while the inherited attributes are evaluated + in the original environment. */ + size_t displ = 0; + for (auto & i : attrs) { + Value * vAttr; + if (hasOverrides && !i.second.inherited) { + vAttr = state.allocValue(); + mkThunk(*vAttr, env2, i.second.e); + } else + vAttr = i.second.e->maybeThunk(state, i.second.inherited ? env : env2); + 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); + Bindings * newBnds = state.allocBindings(v.attrs->capacity() + vOverrides->attrs->size()); + for (auto & i : *v.attrs) + newBnds->push_back(i); + for (auto & i : *vOverrides->attrs) { + 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.attrs = newBnds; + } + } + + else + for (auto & i : attrs) + v.attrs->push_back(Attr(i.first, i.second.e->maybeThunk(state, env), &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 == tNull) + continue; + state.forceStringNoCtx(nameVal); + Symbol nameSym = state.symbols.create(nameVal.string.s); + Bindings::iterator j = v.attrs->find(nameSym); + if (j != v.attrs->end()) + throwEvalError("dynamic attribute '%1%' at %2% already defined at %3%", nameSym, i.pos, *j->pos); + + 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 + } +} + + +void ExprLet::eval(EvalState & state, Env & env, Value & v) +{ + /* Create a new environment that contains the attributes in this + `let'. */ + Env & env2(state.allocEnv(attrs->attrs.size())); + env2.up = &env; + + /* The recursive attributes are evaluated in the new environment, + while the inherited attributes are evaluated in the original + environment. */ + size_t displ = 0; + for (auto & i : attrs->attrs) + env2.values[displ++] = i.second.e->maybeThunk(state, i.second.inherited ? env : env2); + + body->eval(state, env2, v); +} + + +void ExprList::eval(EvalState & state, Env & env, Value & v) +{ + state.mkList(v, elems.size()); + for (size_t n = 0; n < elems.size(); ++n) + v.listElems()[n] = elems[n]->maybeThunk(state, env); +} + + +void ExprVar::eval(EvalState & state, Env & env, Value & v) +{ + Value * v2 = state.lookupVar(&env, *this, false); + state.forceValue(*v2, pos); + v = *v2; +} + + +static 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 << getName(i, state, env); + } catch (Error & e) { + assert(!i.symbol.set()); + out << "\"${" << *i.expr << "}\""; + } + } + return out.str(); +} + + +unsigned long nrLookups = 0; + +void ExprSelect::eval(EvalState & state, Env & env, Value & v) +{ + Value vTmp; + Pos * pos2 = 0; + Value * vAttrs = &vTmp; + + e->eval(state, env, vTmp); + + try { + + for (auto & i : attrPath) { + nrLookups++; + Bindings::iterator j; + Symbol name = getName(i, state, env); + if (def) { + state.forceValue(*vAttrs, pos); + if (vAttrs->type != tAttrs || + (j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) + { + def->eval(state, env, v); + return; + } + } else { + state.forceAttrs(*vAttrs, pos); + if ((j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) + throwEvalError("attribute '%1%' missing, at %2%", name, pos); + } + vAttrs = j->value; + pos2 = j->pos; + if (state.countCalls && pos2) state.attrSelects[*pos2]++; + } + + state.forceValue(*vAttrs, ( pos2 != NULL ? *pos2 : this->pos ) ); + + } catch (Error & e) { + if (pos2 && pos2->file != state.sDerivationNix) + addErrorPrefix(e, "while evaluating the attribute '%1%' at %2%:\n", + showAttrPath(state, env, attrPath), *pos2); + throw; + } + + v = *vAttrs; +} + + +void ExprOpHasAttr::eval(EvalState & state, Env & env, Value & v) +{ + Value vTmp; + Value * vAttrs = &vTmp; + + e->eval(state, env, vTmp); + + for (auto & i : attrPath) { + state.forceValue(*vAttrs); + Bindings::iterator j; + Symbol name = getName(i, state, env); + if (vAttrs->type != tAttrs || + (j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) + { + mkBool(v, false); + return; + } else { + vAttrs = j->value; + } + } + + mkBool(v, true); +} + + +void ExprLambda::eval(EvalState & state, Env & env, Value & v) +{ + v.type = tLambda; + v.lambda.env = &env; + v.lambda.fun = this; +} + + +void ExprApp::eval(EvalState & state, Env & env, Value & v) +{ + /* FIXME: vFun prevents GCC from doing tail call optimisation. */ + Value vFun; + e1->eval(state, env, vFun); + state.callFunction(vFun, *(e2->maybeThunk(state, env)), v, pos); +} + + +void EvalState::callPrimOp(Value & fun, Value & arg, Value & v, const Pos & pos) +{ + /* Figure out the number of arguments still needed. */ + size_t argsDone = 0; + Value * primOp = &fun; + while (primOp->type == tPrimOpApp) { + argsDone++; + primOp = primOp->primOpApp.left; + } + assert(primOp->type == tPrimOp); + auto arity = primOp->primOp->arity; + auto argsLeft = arity - argsDone; + + if (argsLeft == 1) { + /* We have all the arguments, so call the primop. */ + + /* Put all the arguments in an array. */ + Value * vArgs[arity]; + auto n = arity - 1; + vArgs[n--] = &arg; + for (Value * arg = &fun; arg->type == tPrimOpApp; arg = arg->primOpApp.left) + vArgs[n--] = arg->primOpApp.right; + + /* And call the primop. */ + nrPrimOpCalls++; + if (countCalls) primOpCalls[primOp->primOp->name]++; + primOp->primOp->fun(*this, pos, vArgs, v); + } else { + Value * fun2 = allocValue(); + *fun2 = fun; + v.type = tPrimOpApp; + v.primOpApp.left = fun2; + v.primOpApp.right = &arg; + } +} + +void EvalState::callFunction(Value & fun, Value & arg, Value & v, const Pos & pos) +{ + auto trace = evalSettings.traceFunctionCalls ? std::make_unique<FunctionCallTrace>(pos) : nullptr; + + forceValue(fun, pos); + + if (fun.type == tPrimOp || fun.type == tPrimOpApp) { + callPrimOp(fun, arg, v, pos); + return; + } + + if (fun.type == tAttrs) { + auto found = fun.attrs->find(sFunctor); + if (found != fun.attrs->end()) { + /* fun may be allocated on the stack of the calling function, + * but for functors we may keep a reference, so heap-allocate + * a copy and use that instead. + */ + auto & fun2 = *allocValue(); + fun2 = fun; + /* !!! Should we use the attr pos here? */ + Value v2; + callFunction(*found->value, fun2, v2, pos); + return callFunction(v2, arg, v, pos); + } + } + + if (fun.type != tLambda) + throwTypeError("attempt to call something which is not a function but %1%, at %2%", fun, pos); + + ExprLambda & lambda(*fun.lambda.fun); + + auto size = + (lambda.arg.empty() ? 0 : 1) + + (lambda.matchAttrs ? lambda.formals->formals.size() : 0); + Env & env2(allocEnv(size)); + env2.up = fun.lambda.env; + + size_t displ = 0; + + if (!lambda.matchAttrs) + env2.values[displ++] = &arg; + + else { + forceAttrs(arg, pos); + + if (!lambda.arg.empty()) + env2.values[displ++] = &arg; + + /* For each formal argument, get the actual argument. If + there is no matching actual argument but the formal + argument has a default, use the default. */ + size_t attrsUsed = 0; + for (auto & i : lambda.formals->formals) { + Bindings::iterator j = arg.attrs->find(i.name); + if (j == arg.attrs->end()) { + if (!i.def) throwTypeError("%1% called without required argument '%2%', at %3%", + lambda, i.name, pos); + env2.values[displ++] = i.def->maybeThunk(*this, env2); + } else { + attrsUsed++; + env2.values[displ++] = j->value; + } + } + + /* Check that each actual argument is listed as a formal + argument (unless the attribute match specifies a `...'). */ + if (!lambda.formals->ellipsis && attrsUsed != arg.attrs->size()) { + /* Nope, so show the first unexpected argument to the + user. */ + for (auto & i : *arg.attrs) + if (lambda.formals->argNames.find(i.name) == lambda.formals->argNames.end()) + throwTypeError("%1% called with unexpected argument '%2%', at %3%", lambda, i.name, pos); + abort(); // can't happen + } + } + + nrFunctionCalls++; + if (countCalls) incrFunctionCall(&lambda); + + /* Evaluate the body. This is conditional on showTrace, because + catching exceptions makes this function not tail-recursive. */ + if (settings.showTrace) + try { + lambda.body->eval(*this, env2, v); + } catch (Error & e) { + addErrorPrefix(e, "while evaluating %1%, called from %2%:\n", lambda, pos); + throw; + } + else + fun.lambda.fun->body->eval(*this, env2, v); +} + + +// Lifted out of callFunction() because it creates a temporary that +// prevents tail-call optimisation. +void EvalState::incrFunctionCall(ExprLambda * fun) +{ + functionCalls[fun]++; +} + + +void EvalState::autoCallFunction(Bindings & args, Value & fun, Value & res) +{ + forceValue(fun); + + if (fun.type == tAttrs) { + auto found = fun.attrs->find(sFunctor); + if (found != fun.attrs->end()) { + Value * v = allocValue(); + callFunction(*found->value, fun, *v, noPos); + forceValue(*v); + return autoCallFunction(args, *v, res); + } + } + + if (fun.type != tLambda || !fun.lambda.fun->matchAttrs) { + res = fun; + return; + } + + Value * actualArgs = allocValue(); + mkAttrs(*actualArgs, fun.lambda.fun->formals->formals.size()); + + for (auto & i : fun.lambda.fun->formals->formals) { + Bindings::iterator j = args.find(i.name); + if (j != args.end()) + actualArgs->attrs->push_back(*j); + else if (!i.def) + throwTypeError("cannot auto-call a function that has an argument without a default value ('%1%')", i.name); + } + + actualArgs->attrs->sort(); + + callFunction(fun, *actualArgs, res, noPos); +} + + +void ExprWith::eval(EvalState & state, Env & env, Value & v) +{ + Env & env2(state.allocEnv(1)); + env2.up = &env; + env2.prevWith = prevWith; + env2.type = Env::HasWithExpr; + env2.values[0] = (Value *) attrs; + + body->eval(state, env2, v); +} + + +void ExprIf::eval(EvalState & state, Env & env, Value & v) +{ + (state.evalBool(env, cond) ? then : else_)->eval(state, env, v); +} + + +void ExprAssert::eval(EvalState & state, Env & env, Value & v) +{ + if (!state.evalBool(env, cond, pos)) { + std::ostringstream out; + cond->show(out); + throwAssertionError("assertion %1% failed at %2%", out.str(), pos); + } + body->eval(state, env, v); +} + + +void ExprOpNot::eval(EvalState & state, Env & env, Value & v) +{ + mkBool(v, !state.evalBool(env, e)); +} + + +void ExprOpEq::eval(EvalState & state, Env & env, Value & v) +{ + Value v1; e1->eval(state, env, v1); + Value v2; e2->eval(state, env, v2); + mkBool(v, state.eqValues(v1, v2)); +} + + +void ExprOpNEq::eval(EvalState & state, Env & env, Value & v) +{ + Value v1; e1->eval(state, env, v1); + Value v2; e2->eval(state, env, v2); + mkBool(v, !state.eqValues(v1, v2)); +} + + +void ExprOpAnd::eval(EvalState & state, Env & env, Value & v) +{ + mkBool(v, state.evalBool(env, e1, pos) && state.evalBool(env, e2, pos)); +} + + +void ExprOpOr::eval(EvalState & state, Env & env, Value & v) +{ + mkBool(v, state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos)); +} + + +void ExprOpImpl::eval(EvalState & state, Env & env, Value & v) +{ + mkBool(v, !state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos)); +} + + +void ExprOpUpdate::eval(EvalState & state, Env & env, Value & v) +{ + Value v1, v2; + state.evalAttrs(env, e1, v1); + state.evalAttrs(env, e2, v2); + + state.nrOpUpdates++; + + if (v1.attrs->size() == 0) { v = v2; return; } + if (v2.attrs->size() == 0) { v = v1; return; } + + state.mkAttrs(v, 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) { + v.attrs->push_back(*j); + ++i; ++j; + } + else if (i->name < j->name) + v.attrs->push_back(*i++); + else + v.attrs->push_back(*j++); + } + + while (i != v1.attrs->end()) v.attrs->push_back(*i++); + while (j != v2.attrs->end()) v.attrs->push_back(*j++); + + state.nrOpUpdateValuesCopied += v.attrs->size(); +} + + +void ExprOpConcatLists::eval(EvalState & state, Env & env, Value & v) +{ + Value v1; e1->eval(state, env, v1); + Value v2; e2->eval(state, env, v2); + Value * lists[2] = { &v1, &v2 }; + state.concatLists(v, 2, lists, pos); +} + + +void EvalState::concatLists(Value & v, size_t nrLists, Value * * lists, const Pos & pos) +{ + nrListConcats++; + + Value * nonEmpty = 0; + size_t len = 0; + for (size_t n = 0; n < nrLists; ++n) { + forceList(*lists[n], pos); + auto l = lists[n]->listSize(); + len += l; + if (l) nonEmpty = lists[n]; + } + + if (nonEmpty && len == nonEmpty->listSize()) { + v = *nonEmpty; + return; + } + + mkList(v, len); + auto out = v.listElems(); + for (size_t n = 0, pos = 0; n < nrLists; ++n) { + auto l = lists[n]->listSize(); + if (l) + memcpy(out + pos, lists[n]->listElems(), l * sizeof(Value *)); + pos += l; + } +} + + +void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) +{ + PathSet context; + std::ostringstream s; + NixInt n = 0; + NixFloat nf = 0; + + bool first = !forceString; + ValueType firstType = tString; + + for (auto & i : *es) { + Value vTmp; + i->eval(state, env, vTmp); + + /* If the first element is a path, then the result will also + be a path, we don't copy anything (yet - that's done later, + since paths are copied when they are used in a derivation), + and none of the strings are allowed to have contexts. */ + if (first) { + firstType = vTmp.type; + first = false; + } + + if (firstType == tInt) { + if (vTmp.type == tInt) { + n += vTmp.integer; + } else if (vTmp.type == tFloat) { + // Upgrade the type from int to float; + firstType = tFloat; + nf = n; + nf += vTmp.fpoint; + } else + throwEvalError("cannot add %1% to an integer, at %2%", showType(vTmp), pos); + } else if (firstType == tFloat) { + if (vTmp.type == tInt) { + nf += vTmp.integer; + } else if (vTmp.type == tFloat) { + nf += vTmp.fpoint; + } else + throwEvalError("cannot add %1% to a float, at %2%", showType(vTmp), pos); + } else + s << state.coerceToString(pos, vTmp, context, false, firstType == tString); + } + + if (firstType == tInt) + mkInt(v, n); + else if (firstType == tFloat) + mkFloat(v, nf); + else if (firstType == tPath) { + if (!context.empty()) + throwEvalError("a string that refers to a store path cannot be appended to a path, at %1%", pos); + auto path = canonPath(s.str()); + mkPath(v, path.c_str()); + } else + mkString(v, s.str(), context); +} + + +void ExprPos::eval(EvalState & state, Env & env, Value & v) +{ + state.mkPos(v, &pos); +} + + +void EvalState::forceValueDeep(Value & v) +{ + std::set<const Value *> seen; + + std::function<void(Value & v)> recurse; + + recurse = [&](Value & v) { + if (seen.find(&v) != seen.end()) return; + seen.insert(&v); + + forceValue(v); + + if (v.type == tAttrs) { + for (auto & i : *v.attrs) + try { + recurse(*i.value); + } catch (Error & e) { + addErrorPrefix(e, "while evaluating the attribute '%1%' at %2%:\n", i.name, *i.pos); + throw; + } + } + + else if (v.isList()) { + for (size_t n = 0; n < v.listSize(); ++n) + recurse(*v.listElems()[n]); + } + }; + + recurse(v); +} + + +NixInt EvalState::forceInt(Value & v, const Pos & pos) +{ + forceValue(v, pos); + if (v.type != tInt) + throwTypeError("value is %1% while an integer was expected, at %2%", v, pos); + return v.integer; +} + + +NixFloat EvalState::forceFloat(Value & v, const Pos & pos) +{ + forceValue(v, pos); + if (v.type == tInt) + return v.integer; + else if (v.type != tFloat) + throwTypeError("value is %1% while a float was expected, at %2%", v, pos); + return v.fpoint; +} + + +bool EvalState::forceBool(Value & v, const Pos & pos) +{ + forceValue(v); + if (v.type != tBool) + throwTypeError("value is %1% while a Boolean was expected, at %2%", v, pos); + return v.boolean; +} + + +bool EvalState::isFunctor(Value & fun) +{ + return fun.type == tAttrs && fun.attrs->find(sFunctor) != fun.attrs->end(); +} + + +void EvalState::forceFunction(Value & v, const Pos & pos) +{ + forceValue(v); + if (v.type != tLambda && v.type != tPrimOp && v.type != tPrimOpApp && !isFunctor(v)) + throwTypeError("value is %1% while a function was expected, at %2%", v, pos); +} + + +string EvalState::forceString(Value & v, const Pos & pos) +{ + forceValue(v, pos); + if (v.type != tString) { + if (pos) + throwTypeError("value is %1% while a string was expected, at %2%", v, pos); + else + throwTypeError("value is %1% while a string was expected", v); + } + return string(v.string.s); +} + + +void copyContext(const Value & v, PathSet & context) +{ + if (v.string.context) + for (const char * * p = v.string.context; *p; ++p) + context.insert(*p); +} + + +string EvalState::forceString(Value & v, PathSet & context, const Pos & pos) +{ + string s = forceString(v, pos); + copyContext(v, context); + return s; +} + + +string EvalState::forceStringNoCtx(Value & v, const Pos & pos) +{ + string s = forceString(v, pos); + if (v.string.context) { + if (pos) + throwEvalError("the string '%1%' is not allowed to refer to a store path (such as '%2%'), at %3%", + v.string.s, v.string.context[0], pos); + else + throwEvalError("the string '%1%' is not allowed to refer to a store path (such as '%2%')", + v.string.s, v.string.context[0]); + } + return s; +} + + +bool EvalState::isDerivation(Value & v) +{ + if (v.type != tAttrs) return false; + Bindings::iterator i = v.attrs->find(sType); + if (i == v.attrs->end()) return false; + forceValue(*i->value); + if (i->value->type != tString) return false; + return strcmp(i->value->string.s, "derivation") == 0; +} + + +std::optional<string> EvalState::tryAttrsToString(const Pos & pos, Value & v, + PathSet & context, bool coerceMore, bool copyToStore) +{ + auto i = v.attrs->find(sToString); + if (i != v.attrs->end()) { + Value v1; + callFunction(*i->value, v, v1, pos); + return coerceToString(pos, v1, context, coerceMore, copyToStore); + } + + return {}; +} + +string EvalState::coerceToString(const Pos & pos, Value & v, PathSet & context, + bool coerceMore, bool copyToStore) +{ + forceValue(v); + + string s; + + if (v.type == tString) { + copyContext(v, context); + return v.string.s; + } + + if (v.type == tPath) { + Path path(canonPath(v.path)); + return copyToStore ? copyPathToStore(context, path) : path; + } + + if (v.type == tAttrs) { + auto maybeString = tryAttrsToString(pos, v, context, coerceMore, copyToStore); + if (maybeString) { + return *maybeString; + } + auto i = v.attrs->find(sOutPath); + if (i == v.attrs->end()) throwTypeError("cannot coerce a set to a string, at %1%", pos); + return coerceToString(pos, *i->value, context, coerceMore, copyToStore); + } + + if (v.type == tExternal) + return v.external->coerceToString(pos, context, coerceMore, copyToStore); + + if (coerceMore) { + + /* Note that `false' is represented as an empty string for + shell scripting convenience, just like `null'. */ + if (v.type == tBool && v.boolean) return "1"; + if (v.type == tBool && !v.boolean) return ""; + if (v.type == tInt) return std::to_string(v.integer); + if (v.type == tFloat) return std::to_string(v.fpoint); + if (v.type == tNull) return ""; + + if (v.isList()) { + string result; + for (size_t n = 0; n < v.listSize(); ++n) { + result += coerceToString(pos, *v.listElems()[n], + context, coerceMore, copyToStore); + if (n < v.listSize() - 1 + /* !!! not quite correct */ + && (!v.listElems()[n]->isList() || v.listElems()[n]->listSize() != 0)) + result += " "; + } + return result; + } + } + + throwTypeError("cannot coerce %1% to a string, at %2%", v, pos); +} + + +string EvalState::copyPathToStore(PathSet & context, const Path & path) +{ + if (nix::isDerivation(path)) + throwEvalError("file names are not allowed to end in '%1%'", drvExtension); + + Path dstPath; + if (srcToStore[path] != "") + dstPath = srcToStore[path]; + else { + dstPath = settings.readOnlyMode + ? store->computeStorePathForPath(baseNameOf(path), checkSourcePath(path)).first + : store->addToStore(baseNameOf(path), checkSourcePath(path), true, htSHA256, defaultPathFilter, repair); + srcToStore[path] = dstPath; + printMsg(lvlChatty, format("copied source '%1%' -> '%2%'") + % path % dstPath); + } + + context.insert(dstPath); + return dstPath; +} + + +Path EvalState::coerceToPath(const Pos & pos, Value & v, PathSet & context) +{ + string path = coerceToString(pos, v, context, false, false); + if (path == "" || path[0] != '/') + throwEvalError("string '%1%' doesn't represent an absolute path, at %2%", path, pos); + return path; +} + + +bool EvalState::eqValues(Value & v1, Value & v2) +{ + forceValue(v1); + forceValue(v2); + + /* !!! Hack to support some old broken code that relies on pointer + equality tests between sets. (Specifically, builderDefs calls + uniqList on a list of sets.) Will remove this eventually. */ + if (&v1 == &v2) return true; + + // Special case type-compatibility between float and int + if (v1.type == tInt && v2.type == tFloat) + return v1.integer == v2.fpoint; + if (v1.type == tFloat && v2.type == tInt) + return v1.fpoint == v2.integer; + + // All other types are not compatible with each other. + if (v1.type != v2.type) return false; + + switch (v1.type) { + + case tInt: + return v1.integer == v2.integer; + + case tBool: + return v1.boolean == v2.boolean; + + case tString: + return strcmp(v1.string.s, v2.string.s) == 0; + + case tPath: + return strcmp(v1.path, v2.path) == 0; + + case tNull: + return true; + + case tList1: + case tList2: + case tListN: + if (v1.listSize() != v2.listSize()) return false; + for (size_t n = 0; n < v1.listSize(); ++n) + if (!eqValues(*v1.listElems()[n], *v2.listElems()[n])) return false; + return true; + + case tAttrs: { + /* If both sets denote a derivation (type = "derivation"), + then compare their outPaths. */ + if (isDerivation(v1) && isDerivation(v2)) { + Bindings::iterator i = v1.attrs->find(sOutPath); + Bindings::iterator j = v2.attrs->find(sOutPath); + if (i != v1.attrs->end() && j != v2.attrs->end()) + return eqValues(*i->value, *j->value); + } + + 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)) + return false; + + return true; + } + + /* Functions are incomparable. */ + case tLambda: + case tPrimOp: + case tPrimOpApp: + return false; + + case tExternal: + return *v1.external == *v2.external; + + case tFloat: + return v1.fpoint == v2.fpoint; + + default: + throwEvalError("cannot compare %1% with %2%", showType(v1), showType(v2)); + } +} + +void EvalState::printStats() +{ + bool showStats = getEnv("NIX_SHOW_STATS", "0") != "0"; + + struct rusage buf; + getrusage(RUSAGE_SELF, &buf); + float cpuTime = buf.ru_utime.tv_sec + ((float) buf.ru_utime.tv_usec / 1000000); + + uint64_t bEnvs = nrEnvs * sizeof(Env) + nrValuesInEnvs * sizeof(Value *); + uint64_t bLists = nrListElems * sizeof(Value *); + uint64_t bValues = nrValues * sizeof(Value); + uint64_t bAttrsets = nrAttrsets * sizeof(Bindings) + nrAttrsInAttrsets * sizeof(Attr); + +#if HAVE_BOEHMGC + GC_word heapSize, totalBytes; + GC_get_heap_usage_safe(&heapSize, 0, 0, 0, &totalBytes); +#endif + if (showStats) { + auto outPath = getEnv("NIX_SHOW_STATS_PATH","-"); + std::fstream fs; + if (outPath != "-") + fs.open(outPath, std::fstream::out); + JSONObject topObj(outPath == "-" ? std::cerr : fs, true); + topObj.attr("cpuTime",cpuTime); + { + auto envs = topObj.object("envs"); + envs.attr("number", nrEnvs); + envs.attr("elements", nrValuesInEnvs); + envs.attr("bytes", bEnvs); + } + { + auto lists = topObj.object("list"); + lists.attr("elements", nrListElems); + lists.attr("bytes", bLists); + lists.attr("concats", nrListConcats); + } + { + auto values = topObj.object("values"); + values.attr("number", nrValues); + values.attr("bytes", bValues); + } + { + auto syms = topObj.object("symbols"); + syms.attr("number", symbols.size()); + syms.attr("bytes", symbols.totalSize()); + } + { + auto sets = topObj.object("sets"); + sets.attr("number", nrAttrsets); + sets.attr("bytes", bAttrsets); + sets.attr("elements", nrAttrsInAttrsets); + } + { + auto sizes = topObj.object("sizes"); + sizes.attr("Env", sizeof(Env)); + sizes.attr("Value", sizeof(Value)); + sizes.attr("Bindings", sizeof(Bindings)); + sizes.attr("Attr", sizeof(Attr)); + } + topObj.attr("nrOpUpdates", nrOpUpdates); + topObj.attr("nrOpUpdateValuesCopied", nrOpUpdateValuesCopied); + topObj.attr("nrThunks", nrThunks); + topObj.attr("nrAvoided", nrAvoided); + topObj.attr("nrLookups", nrLookups); + topObj.attr("nrPrimOpCalls", nrPrimOpCalls); + topObj.attr("nrFunctionCalls", nrFunctionCalls); +#if HAVE_BOEHMGC + { + auto gc = topObj.object("gc"); + gc.attr("heapSize", heapSize); + gc.attr("totalBytes", totalBytes); + } +#endif + + if (countCalls) { + { + auto obj = topObj.object("primops"); + for (auto & i : primOpCalls) + obj.attr(i.first, i.second); + } + { + auto list = topObj.list("functions"); + for (auto & i : functionCalls) { + auto obj = list.object(); + if (i.first->name.set()) + obj.attr("name", (const string &) i.first->name); + else + obj.attr("name", nullptr); + if (i.first->pos) { + obj.attr("file", (const string &) i.first->pos.file); + obj.attr("line", i.first->pos.line); + obj.attr("column", i.first->pos.column); + } + obj.attr("count", i.second); + } + } + { + auto list = topObj.list("attributes"); + for (auto & i : attrSelects) { + auto obj = list.object(); + if (i.first) { + obj.attr("file", (const string &) i.first.file); + obj.attr("line", i.first.line); + obj.attr("column", i.first.column); + } + obj.attr("count", i.second); + } + } + } + + if (getEnv("NIX_SHOW_SYMBOLS", "0") != "0") { + auto list = topObj.list("symbols"); + symbols.dump([&](const std::string & s) { list.elem(s); }); + } + } +} + + +size_t valueSize(Value & v) +{ + std::set<const void *> seen; + + auto doString = [&](const char * s) -> size_t { + if (seen.find(s) != seen.end()) return 0; + seen.insert(s); + return strlen(s) + 1; + }; + + std::function<size_t(Value & v)> doValue; + std::function<size_t(Env & v)> doEnv; + + doValue = [&](Value & v) -> size_t { + if (seen.find(&v) != seen.end()) return 0; + seen.insert(&v); + + size_t sz = sizeof(Value); + + switch (v.type) { + case tString: + sz += doString(v.string.s); + if (v.string.context) + for (const char * * p = v.string.context; *p; ++p) + sz += doString(*p); + break; + case tPath: + sz += doString(v.path); + break; + case tAttrs: + if (seen.find(v.attrs) == seen.end()) { + seen.insert(v.attrs); + sz += sizeof(Bindings) + sizeof(Attr) * v.attrs->capacity(); + for (auto & i : *v.attrs) + sz += doValue(*i.value); + } + break; + case tList1: + case tList2: + case tListN: + if (seen.find(v.listElems()) == seen.end()) { + seen.insert(v.listElems()); + sz += v.listSize() * sizeof(Value *); + for (size_t n = 0; n < v.listSize(); ++n) + sz += doValue(*v.listElems()[n]); + } + break; + case tThunk: + sz += doEnv(*v.thunk.env); + break; + case tApp: + sz += doValue(*v.app.left); + sz += doValue(*v.app.right); + break; + case tLambda: + sz += doEnv(*v.lambda.env); + break; + case tPrimOpApp: + sz += doValue(*v.primOpApp.left); + sz += doValue(*v.primOpApp.right); + break; + case tExternal: + if (seen.find(v.external) != seen.end()) break; + seen.insert(v.external); + sz += v.external->valueSize(seen); + break; + default: + ; + } + + return sz; + }; + + doEnv = [&](Env & env) -> size_t { + if (seen.find(&env) != seen.end()) return 0; + seen.insert(&env); + + size_t sz = sizeof(Env) + sizeof(Value *) * env.size; + + if (env.type != Env::HasWithExpr) + for (size_t i = 0; i < env.size; ++i) + if (env.values[i]) + sz += doValue(*env.values[i]); + + if (env.up) sz += doEnv(*env.up); + + return sz; + }; + + return doValue(v); +} + + +string ExternalValueBase::coerceToString(const Pos & pos, PathSet & context, bool copyMore, bool copyToStore) const +{ + throw TypeError(format("cannot coerce %1% to a string, at %2%") % + showType() % pos); +} + + +bool ExternalValueBase::operator==(const ExternalValueBase & b) const +{ + return false; +} + + +std::ostream & operator << (std::ostream & str, const ExternalValueBase & v) { + return v.print(str); +} + + +EvalSettings evalSettings; + +static GlobalConfig::Register r1(&evalSettings); + + +} diff --git a/third_party/nix/src/libexpr/eval.hh b/third_party/nix/src/libexpr/eval.hh new file mode 100644 index 000000000000..8126e4ea50ff --- /dev/null +++ b/third_party/nix/src/libexpr/eval.hh @@ -0,0 +1,363 @@ +#pragma once + +#include "attr-set.hh" +#include "value.hh" +#include "nixexpr.hh" +#include "symbol-table.hh" +#include "hash.hh" +#include "config.hh" + +#include <map> +#include <optional> +#include <unordered_map> + + +namespace nix { + + +class Store; +class EvalState; +enum RepairFlag : bool; + + +typedef void (* PrimOpFun) (EvalState & state, const Pos & pos, Value * * args, Value & v); + + +struct PrimOp +{ + PrimOpFun fun; + size_t arity; + Symbol name; + PrimOp(PrimOpFun fun, size_t arity, Symbol name) + : fun(fun), arity(arity), name(name) { } +}; + + +struct Env +{ + Env * up; + unsigned short size; // used by ‘valueSize’ + unsigned short prevWith:14; // nr of levels up to next `with' environment + enum { Plain = 0, HasWithExpr, HasWithAttrs } type:2; + Value * values[0]; +}; + + +Value & mkString(Value & v, const string & s, const PathSet & context = PathSet()); + +void copyContext(const Value & v, PathSet & context); + + +/* Cache for calls to addToStore(); maps source paths to the store + paths. */ +typedef std::map<Path, Path> SrcToStore; + + +std::ostream & operator << (std::ostream & str, const Value & v); + + +typedef std::pair<std::string, std::string> SearchPathElem; +typedef std::list<SearchPathElem> SearchPath; + + +/* Initialise the Boehm GC, if applicable. */ +void initGC(); + + +class EvalState +{ +public: + SymbolTable symbols; + + const Symbol sWith, sOutPath, sDrvPath, sType, sMeta, sName, sValue, + sSystem, sOverrides, sOutputs, sOutputName, sIgnoreNulls, + sFile, sLine, sColumn, sFunctor, sToString, + sRight, sWrong, sStructuredAttrs, sBuilder, sArgs, + sOutputHash, sOutputHashAlgo, sOutputHashMode; + Symbol sDerivationNix; + + /* If set, force copying files to the Nix store even if they + already exist there. */ + RepairFlag repair; + + /* The allowed filesystem paths in restricted or pure evaluation + mode. */ + std::optional<PathSet> allowedPaths; + + Value vEmptySet; + + const ref<Store> store; + +private: + SrcToStore srcToStore; + + /* A cache from path names to parse trees. */ +#if HAVE_BOEHMGC + typedef std::map<Path, Expr *, std::less<Path>, traceable_allocator<std::pair<const Path, Expr *> > > FileParseCache; +#else + typedef std::map<Path, Expr *> FileParseCache; +#endif + FileParseCache fileParseCache; + + /* A cache from path names to values. */ +#if HAVE_BOEHMGC + typedef std::map<Path, Value, std::less<Path>, traceable_allocator<std::pair<const Path, Value> > > FileEvalCache; +#else + typedef std::map<Path, Value> FileEvalCache; +#endif + FileEvalCache fileEvalCache; + + SearchPath searchPath; + + std::map<std::string, std::pair<bool, std::string>> searchPathResolved; + + /* Cache used by checkSourcePath(). */ + std::unordered_map<Path, Path> resolvedPaths; + +public: + + EvalState(const Strings & _searchPath, ref<Store> store); + ~EvalState(); + + void addToSearchPath(const string & s); + + SearchPath getSearchPath() { return searchPath; } + + Path checkSourcePath(const Path & 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 PathSet & context); + + /* Parse a Nix expression from the specified file. */ + Expr * parseExprFromFile(const Path & path); + Expr * parseExprFromFile(const Path & path, StaticEnv & staticEnv); + + /* Parse a Nix expression from the specified string. */ + Expr * parseExprFromString(const string & s, const Path & basePath, StaticEnv & staticEnv); + Expr * parseExprFromString(const string & s, const Path & basePath); + + Expr * parseStdin(); + + /* Evaluate an expression read from the given file to normal + form. */ + void evalFile(const Path & path, Value & v); + + void resetFileCache(); + + /* Look up a file in the search path. */ + Path findFile(const string & path); + Path findFile(SearchPath & searchPath, const string & path, const Pos & pos = noPos); + + /* If the specified search path element is a URI, download it. */ + std::pair<bool, std::string> resolveSearchPathElem(const SearchPathElem & elem); + + /* Evaluate an expression to normal form, storing the result in + value `v'. */ + 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 bool evalBool(Env & env, Expr * e, const Pos & pos); + inline void evalAttrs(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 Pos & pos = noPos); + + /* 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 Pos & pos); + NixFloat forceFloat(Value & v, const Pos & pos); + bool forceBool(Value & v, const Pos & pos); + inline void forceAttrs(Value & v); + inline void forceAttrs(Value & v, const Pos & pos); + inline void forceList(Value & v); + inline void forceList(Value & v, const Pos & pos); + void forceFunction(Value & v, const Pos & pos); // either lambda or primop + string forceString(Value & v, const Pos & pos = noPos); + string forceString(Value & v, PathSet & context, const Pos & pos = noPos); + string forceStringNoCtx(Value & v, const Pos & pos = noPos); + + /* Return true iff the value `v' denotes a derivation (i.e. a + set with attribute `type = "derivation"'). */ + bool isDerivation(Value & v); + + std::optional<string> tryAttrsToString(const Pos & pos, Value & v, + PathSet & context, bool coerceMore = false, bool copyToStore = true); + + /* String coercion. Converts strings, paths and derivations to a + string. If `coerceMore' is set, also converts nulls, integers, + booleans and lists to a string. If `copyToStore' is set, + referenced paths are copied to the Nix store as a side effect. */ + string coerceToString(const Pos & pos, Value & v, PathSet & context, + bool coerceMore = false, bool copyToStore = true); + + string copyPathToStore(PathSet & context, const Path & path); + + /* 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. */ + Path coerceToPath(const Pos & pos, Value & v, PathSet & context); + +public: + + /* The base environment, containing the builtin functions and + values. */ + Env & baseEnv; + + /* The same, but used during parsing to resolve variables. */ + StaticEnv staticBaseEnv; // !!! should be private + +private: + + unsigned int baseEnvDispl = 0; + + void createBaseEnv(); + + Value * addConstant(const string & name, Value & v); + + Value * addPrimOp(const string & name, + size_t arity, PrimOpFun primOp); + +public: + + Value & getBuiltin(const string & name); + +private: + + inline Value * lookupVar(Env * env, const ExprVar & var, bool noEval); + + friend struct ExprVar; + friend struct ExprAttrs; + friend struct ExprLet; + + Expr * parse(const char * text, const Path & path, + const Path & basePath, StaticEnv & staticEnv); + +public: + + /* Do a deep equality test between two values. That is, list + elements and attributes are compared recursively. */ + bool eqValues(Value & v1, Value & v2); + + bool isFunctor(Value & fun); + + void callFunction(Value & fun, Value & arg, Value & v, const Pos & pos); + void callPrimOp(Value & fun, Value & arg, Value & v, const Pos & 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); + + /* Allocation primitives. */ + Value * allocValue(); + Env & allocEnv(size_t size); + + Value * allocAttr(Value & vAttrs, const Symbol & name); + + Bindings * allocBindings(size_t capacity); + + void mkList(Value & v, size_t length); + void mkAttrs(Value & v, size_t capacity); + void mkThunk_(Value & v, Expr * expr); + void mkPos(Value & v, Pos * pos); + + void concatLists(Value & v, size_t nrLists, Value * * lists, const Pos & pos); + + /* Print statistics. */ + void printStats(); + + void realiseContext(const PathSet & context); + +private: + + unsigned long nrEnvs = 0; + unsigned long nrValuesInEnvs = 0; + unsigned long nrValues = 0; + unsigned long nrListElems = 0; + unsigned long nrAttrsets = 0; + unsigned long nrAttrsInAttrsets = 0; + unsigned long nrOpUpdates = 0; + unsigned long nrOpUpdateValuesCopied = 0; + unsigned long nrListConcats = 0; + unsigned long nrPrimOpCalls = 0; + unsigned long nrFunctionCalls = 0; + + bool countCalls; + + typedef std::map<Symbol, size_t> PrimOpCalls; + PrimOpCalls primOpCalls; + + typedef std::map<ExprLambda *, size_t> FunctionCalls; + FunctionCalls functionCalls; + + void incrFunctionCall(ExprLambda * fun); + + typedef std::map<Pos, size_t> AttrSelects; + AttrSelects attrSelects; + + friend struct ExprOpUpdate; + friend struct ExprOpConcatLists; + friend struct ExprSelect; + friend void prim_getAttr(EvalState & state, const Pos & pos, Value * * args, Value & v); +}; + + +/* Return a string representing the type of the value `v'. */ +string showType(const Value & v); + +/* Decode a context string ‘!<name>!<path>’ into a pair <path, + name>. */ +std::pair<string, string> decodeContext(const string & s); + +/* If `path' refers to a directory, then append "/default.nix". */ +Path resolveExprPath(Path path); + +struct InvalidPathError : EvalError +{ + Path path; + InvalidPathError(const Path & path); +#ifdef EXCEPTION_NEEDS_THROW_SPEC + ~InvalidPathError() throw () { }; +#endif +}; + +struct EvalSettings : Config +{ + Setting<bool> enableNativeCode{this, false, "allow-unsafe-native-code-during-evaluation", + "Whether builtin functions that allow executing native code should be enabled."}; + + Setting<bool> restrictEval{this, false, "restrict-eval", + "Whether to restrict file system access to paths in $NIX_PATH, " + "and network access to the URI prefixes listed in 'allowed-uris'."}; + + Setting<bool> pureEval{this, false, "pure-eval", + "Whether to restrict file system and network access to files specified by cryptographic hash."}; + + Setting<bool> enableImportFromDerivation{this, true, "allow-import-from-derivation", + "Whether the evaluator allows importing the result of a derivation."}; + + Setting<Strings> allowedUris{this, {}, "allowed-uris", + "Prefixes of URIs that builtin functions such as fetchurl and fetchGit are allowed to fetch."}; + + Setting<bool> traceFunctionCalls{this, false, "trace-function-calls", + "Emit log messages for each function entry and exit at the 'vomit' log level (-vvvv)"}; +}; + +extern EvalSettings evalSettings; + +} diff --git a/third_party/nix/src/libexpr/function-trace.cc b/third_party/nix/src/libexpr/function-trace.cc new file mode 100644 index 000000000000..af1486f78973 --- /dev/null +++ b/third_party/nix/src/libexpr/function-trace.cc @@ -0,0 +1,17 @@ +#include "function-trace.hh" + +namespace nix { + +FunctionCallTrace::FunctionCallTrace(const Pos & pos) : pos(pos) { + auto duration = std::chrono::high_resolution_clock::now().time_since_epoch(); + auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(duration); + printMsg(lvlInfo, "function-trace entered %1% at %2%", pos, ns.count()); +} + +FunctionCallTrace::~FunctionCallTrace() { + auto duration = std::chrono::high_resolution_clock::now().time_since_epoch(); + auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(duration); + printMsg(lvlInfo, "function-trace exited %1% at %2%", pos, ns.count()); +} + +} diff --git a/third_party/nix/src/libexpr/function-trace.hh b/third_party/nix/src/libexpr/function-trace.hh new file mode 100644 index 000000000000..472f2045ed65 --- /dev/null +++ b/third_party/nix/src/libexpr/function-trace.hh @@ -0,0 +1,15 @@ +#pragma once + +#include "eval.hh" + +#include <chrono> + +namespace nix { + +struct FunctionCallTrace +{ + const Pos & pos; + FunctionCallTrace(const Pos & pos); + ~FunctionCallTrace(); +}; +} diff --git a/third_party/nix/src/libexpr/get-drvs.cc b/third_party/nix/src/libexpr/get-drvs.cc new file mode 100644 index 000000000000..21a4d7917fce --- /dev/null +++ b/third_party/nix/src/libexpr/get-drvs.cc @@ -0,0 +1,380 @@ +#include "get-drvs.hh" +#include "util.hh" +#include "eval-inline.hh" +#include "derivations.hh" + +#include <cstring> +#include <regex> + + +namespace nix { + + +DrvInfo::DrvInfo(EvalState & state, const string & attrPath, Bindings * attrs) + : state(&state), attrs(attrs), attrPath(attrPath) +{ +} + + +DrvInfo::DrvInfo(EvalState & state, ref<Store> store, const std::string & drvPathWithOutputs) + : state(&state), attrs(nullptr), attrPath("") +{ + auto spec = parseDrvPathWithOutputs(drvPathWithOutputs); + + drvPath = spec.first; + + auto drv = store->derivationFromPath(drvPath); + + name = storePathToName(drvPath); + + if (spec.second.size() > 1) + throw Error("building more than one derivation output is not supported, in '%s'", drvPathWithOutputs); + + outputName = + spec.second.empty() + ? get(drv.env, "outputName", "out") + : *spec.second.begin(); + + auto i = drv.outputs.find(outputName); + if (i == drv.outputs.end()) + throw Error("derivation '%s' does not have output '%s'", drvPath, outputName); + + outPath = i->second.path; +} + + +string DrvInfo::queryName() const +{ + if (name == "" && attrs) { + auto i = attrs->find(state->sName); + if (i == attrs->end()) throw TypeError("derivation name missing"); + name = state->forceStringNoCtx(*i->value); + } + return name; +} + + +string DrvInfo::querySystem() const +{ + if (system == "" && attrs) { + auto i = attrs->find(state->sSystem); + system = i == attrs->end() ? "unknown" : state->forceStringNoCtx(*i->value, *i->pos); + } + return system; +} + + +string DrvInfo::queryDrvPath() const +{ + if (drvPath == "" && attrs) { + Bindings::iterator i = attrs->find(state->sDrvPath); + PathSet context; + drvPath = i != attrs->end() ? state->coerceToPath(*i->pos, *i->value, context) : ""; + } + return drvPath; +} + + +string DrvInfo::queryOutPath() const +{ + if (outPath == "" && attrs) { + Bindings::iterator i = attrs->find(state->sOutPath); + PathSet context; + outPath = i != attrs->end() ? state->coerceToPath(*i->pos, *i->value, context) : ""; + } + return outPath; +} + + +DrvInfo::Outputs DrvInfo::queryOutputs(bool onlyOutputsToInstall) +{ + if (outputs.empty()) { + /* Get the ‘outputs’ list. */ + Bindings::iterator i; + if (attrs && (i = attrs->find(state->sOutputs)) != attrs->end()) { + state->forceList(*i->value, *i->pos); + + /* For each output... */ + for (unsigned int j = 0; j < i->value->listSize(); ++j) { + /* Evaluate the corresponding set. */ + string name = state->forceStringNoCtx(*i->value->listElems()[j], *i->pos); + Bindings::iterator out = attrs->find(state->symbols.create(name)); + if (out == attrs->end()) continue; // FIXME: throw error? + state->forceAttrs(*out->value); + + /* And evaluate its ‘outPath’ attribute. */ + Bindings::iterator outPath = out->value->attrs->find(state->sOutPath); + if (outPath == out->value->attrs->end()) continue; // FIXME: throw error? + PathSet context; + outputs[name] = state->coerceToPath(*outPath->pos, *outPath->value, context); + } + } else + outputs["out"] = queryOutPath(); + } + if (!onlyOutputsToInstall || !attrs) + return outputs; + + /* Check for `meta.outputsToInstall` and return `outputs` reduced to that. */ + const Value * outTI = queryMeta("outputsToInstall"); + if (!outTI) return outputs; + const auto errMsg = Error("this derivation has bad 'meta.outputsToInstall'"); + /* ^ this shows during `nix-env -i` right under the bad derivation */ + if (!outTI->isList()) throw errMsg; + Outputs result; + for (auto i = outTI->listElems(); i != outTI->listElems() + outTI->listSize(); ++i) { + if ((*i)->type != tString) throw errMsg; + auto out = outputs.find((*i)->string.s); + if (out == outputs.end()) throw errMsg; + result.insert(*out); + } + return result; +} + + +string DrvInfo::queryOutputName() const +{ + if (outputName == "" && attrs) { + Bindings::iterator i = attrs->find(state->sOutputName); + outputName = i != attrs->end() ? state->forceStringNoCtx(*i->value) : ""; + } + return outputName; +} + + +Bindings * DrvInfo::getMeta() +{ + if (meta) return meta; + if (!attrs) return 0; + Bindings::iterator a = attrs->find(state->sMeta); + if (a == attrs->end()) return 0; + state->forceAttrs(*a->value, *a->pos); + meta = a->value->attrs; + return meta; +} + + +StringSet DrvInfo::queryMetaNames() +{ + StringSet res; + if (!getMeta()) return res; + for (auto & i : *meta) + res.insert(i.name); + return res; +} + + +bool DrvInfo::checkMeta(Value & v) +{ + state->forceValue(v); + if (v.isList()) { + for (unsigned int n = 0; n < v.listSize(); ++n) + if (!checkMeta(*v.listElems()[n])) return false; + return true; + } + else if (v.type == tAttrs) { + Bindings::iterator i = v.attrs->find(state->sOutPath); + if (i != v.attrs->end()) return false; + for (auto & i : *v.attrs) + if (!checkMeta(*i.value)) return false; + return true; + } + else return v.type == tInt || v.type == tBool || v.type == tString || + v.type == tFloat; +} + + +Value * DrvInfo::queryMeta(const string & name) +{ + if (!getMeta()) return 0; + Bindings::iterator a = meta->find(state->symbols.create(name)); + if (a == meta->end() || !checkMeta(*a->value)) return 0; + return a->value; +} + + +string DrvInfo::queryMetaString(const string & name) +{ + Value * v = queryMeta(name); + if (!v || v->type != tString) return ""; + return v->string.s; +} + + +NixInt DrvInfo::queryMetaInt(const string & name, NixInt def) +{ + Value * v = queryMeta(name); + if (!v) return def; + if (v->type == tInt) return v->integer; + if (v->type == tString) { + /* Backwards compatibility with before we had support for + integer meta fields. */ + NixInt n; + if (string2Int(v->string.s, n)) return n; + } + return def; +} + +NixFloat DrvInfo::queryMetaFloat(const string & name, NixFloat def) +{ + Value * v = queryMeta(name); + if (!v) return def; + if (v->type == tFloat) return v->fpoint; + if (v->type == tString) { + /* Backwards compatibility with before we had support for + float meta fields. */ + NixFloat n; + if (string2Float(v->string.s, n)) return n; + } + return def; +} + + +bool DrvInfo::queryMetaBool(const string & name, bool def) +{ + Value * v = queryMeta(name); + if (!v) return def; + if (v->type == tBool) return v->boolean; + if (v->type == tString) { + /* Backwards compatibility with before we had support for + Boolean meta fields. */ + if (strcmp(v->string.s, "true") == 0) return true; + if (strcmp(v->string.s, "false") == 0) return false; + } + return def; +} + + +void DrvInfo::setMeta(const string & name, Value * v) +{ + getMeta(); + Bindings * old = meta; + meta = state->allocBindings(1 + (old ? old->size() : 0)); + Symbol sym = state->symbols.create(name); + if (old) + for (auto i : *old) + if (i.name != sym) + meta->push_back(i); + if (v) meta->push_back(Attr(sym, v)); + meta->sort(); +} + + +/* Cache for already considered attrsets. */ +typedef set<Bindings *> Done; + + +/* Evaluate value `v'. If it evaluates to a set of type `derivation', + then put information about it in `drvs' (unless it's already in `done'). + The result boolean indicates whether it makes sense + for the caller to recursively search for derivations in `v'. */ +static bool getDerivation(EvalState & state, Value & v, + const string & attrPath, DrvInfos & drvs, Done & done, + bool ignoreAssertionFailures) +{ + try { + state.forceValue(v); + if (!state.isDerivation(v)) return true; + + /* Remove spurious duplicates (e.g., a set like `rec { x = + derivation {...}; y = x;}'. */ + if (done.find(v.attrs) != done.end()) return false; + done.insert(v.attrs); + + DrvInfo drv(state, attrPath, v.attrs); + + drv.queryName(); + + drvs.push_back(drv); + + return false; + + } catch (AssertionError & e) { + if (ignoreAssertionFailures) return false; + throw; + } +} + + +std::optional<DrvInfo> getDerivation(EvalState & state, Value & v, + bool ignoreAssertionFailures) +{ + Done done; + DrvInfos drvs; + getDerivation(state, v, "", drvs, done, ignoreAssertionFailures); + if (drvs.size() != 1) return {}; + return std::move(drvs.front()); +} + + +static string addToPath(const string & s1, const string & s2) +{ + return s1.empty() ? s2 : s1 + "." + s2; +} + + +static std::regex attrRegex("[A-Za-z_][A-Za-z0-9-_+]*"); + + +static void getDerivations(EvalState & state, Value & vIn, + const string & pathPrefix, Bindings & autoArgs, + DrvInfos & drvs, Done & done, + bool ignoreAssertionFailures) +{ + Value v; + state.autoCallFunction(autoArgs, vIn, v); + + /* Process the expression. */ + if (!getDerivation(state, v, pathPrefix, drvs, done, ignoreAssertionFailures)) ; + + else if (v.type == tAttrs) { + + /* !!! undocumented hackery to support combining channels in + nix-env.cc. */ + bool combineChannels = v.attrs->find(state.symbols.create("_combineChannels")) != v.attrs->end(); + + /* Consider the attributes in sorted order to get more + deterministic behaviour in nix-env operations (e.g. when + there are names clashes between derivations, the derivation + bound to the attribute with the "lower" name should take + precedence). */ + for (auto & i : v.attrs->lexicographicOrder()) { + debug("evaluating attribute '%1%'", i->name); + if (!std::regex_match(std::string(i->name), attrRegex)) + continue; + string pathPrefix2 = addToPath(pathPrefix, i->name); + if (combineChannels) + getDerivations(state, *i->value, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); + else if (getDerivation(state, *i->value, pathPrefix2, drvs, done, ignoreAssertionFailures)) { + /* If the value of this attribute is itself a set, + should we recurse into it? => Only if it has a + `recurseForDerivations = true' attribute. */ + if (i->value->type == tAttrs) { + Bindings::iterator j = i->value->attrs->find(state.symbols.create("recurseForDerivations")); + if (j != i->value->attrs->end() && state.forceBool(*j->value, *j->pos)) + getDerivations(state, *i->value, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); + } + } + } + } + + else if (v.isList()) { + for (unsigned int n = 0; n < v.listSize(); ++n) { + string pathPrefix2 = addToPath(pathPrefix, (format("%1%") % n).str()); + if (getDerivation(state, *v.listElems()[n], pathPrefix2, drvs, done, ignoreAssertionFailures)) + getDerivations(state, *v.listElems()[n], pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); + } + } + + else throw TypeError("expression does not evaluate to a derivation (or a set or list of those)"); +} + + +void getDerivations(EvalState & state, Value & v, const string & pathPrefix, + Bindings & autoArgs, DrvInfos & drvs, bool ignoreAssertionFailures) +{ + Done done; + getDerivations(state, v, pathPrefix, autoArgs, drvs, done, ignoreAssertionFailures); +} + + +} diff --git a/third_party/nix/src/libexpr/get-drvs.hh b/third_party/nix/src/libexpr/get-drvs.hh new file mode 100644 index 000000000000..d7860fc6a4bc --- /dev/null +++ b/third_party/nix/src/libexpr/get-drvs.hh @@ -0,0 +1,89 @@ +#pragma once + +#include "eval.hh" + +#include <string> +#include <map> + + +namespace nix { + + +struct DrvInfo +{ +public: + typedef std::map<string, Path> Outputs; + +private: + EvalState * state; + + mutable string name; + mutable string system; + mutable string drvPath; + mutable string outPath; + mutable string outputName; + Outputs outputs; + + bool failed = false; // set if we get an AssertionError + + Bindings * attrs = nullptr, * meta = nullptr; + + Bindings * getMeta(); + + bool checkMeta(Value & v); + +public: + string attrPath; /* path towards the derivation */ + + DrvInfo(EvalState & state) : state(&state) { }; + DrvInfo(EvalState & state, const string & attrPath, Bindings * attrs); + DrvInfo(EvalState & state, ref<Store> store, const std::string & drvPathWithOutputs); + + string queryName() const; + string querySystem() const; + string queryDrvPath() const; + string queryOutPath() const; + string queryOutputName() const; + /** Return the list of outputs. The "outputs to install" are determined by `meta.outputsToInstall`. */ + Outputs queryOutputs(bool onlyOutputsToInstall = false); + + StringSet queryMetaNames(); + Value * queryMeta(const string & name); + string queryMetaString(const string & name); + NixInt queryMetaInt(const string & name, NixInt def); + NixFloat queryMetaFloat(const string & name, NixFloat def); + bool queryMetaBool(const string & name, bool def); + void setMeta(const string & name, Value * v); + + /* + MetaInfo queryMetaInfo(EvalState & state) const; + MetaValue queryMetaInfo(EvalState & state, const string & name) const; + */ + + void setName(const string & s) { name = s; } + void setDrvPath(const string & s) { drvPath = s; } + void setOutPath(const string & s) { outPath = s; } + + void setFailed() { failed = true; }; + bool hasFailed() { return failed; }; +}; + + +#if HAVE_BOEHMGC +typedef list<DrvInfo, traceable_allocator<DrvInfo> > DrvInfos; +#else +typedef list<DrvInfo> DrvInfos; +#endif + + +/* If value `v' denotes a derivation, return a DrvInfo object + describing it. Otherwise return nothing. */ +std::optional<DrvInfo> getDerivation(EvalState & state, + Value & v, bool ignoreAssertionFailures); + +void getDerivations(EvalState & state, Value & v, const string & pathPrefix, + Bindings & autoArgs, DrvInfos & drvs, + bool ignoreAssertionFailures); + + +} diff --git a/third_party/nix/src/libexpr/json-to-value.cc b/third_party/nix/src/libexpr/json-to-value.cc new file mode 100644 index 000000000000..8bae986f97fc --- /dev/null +++ b/third_party/nix/src/libexpr/json-to-value.cc @@ -0,0 +1,149 @@ +#include "json-to-value.hh" + +#include <cstring> + +namespace nix { + + +static void skipWhitespace(const char * & s) +{ + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; +} + + +static string parseJSONString(const char * & s) +{ + string res; + if (*s++ != '"') throw JSONParseError("expected JSON string"); + while (*s != '"') { + if (!*s) throw JSONParseError("got end-of-string in JSON string"); + if (*s == '\\') { + s++; + if (*s == '"') res += '"'; + else if (*s == '\\') res += '\\'; + else if (*s == '/') res += '/'; + else if (*s == '/') res += '/'; + else if (*s == 'b') res += '\b'; + else if (*s == 'f') res += '\f'; + else if (*s == 'n') res += '\n'; + else if (*s == 'r') res += '\r'; + else if (*s == 't') res += '\t'; + else if (*s == 'u') throw JSONParseError("\\u characters in JSON strings are currently not supported"); + else throw JSONParseError("invalid escaped character in JSON string"); + s++; + } else + res += *s++; + } + s++; + return res; +} + + +static void parseJSON(EvalState & state, const char * & s, Value & v) +{ + skipWhitespace(s); + + if (!*s) throw JSONParseError("expected JSON value"); + + if (*s == '[') { + s++; + ValueVector values; + values.reserve(128); + skipWhitespace(s); + while (1) { + if (values.empty() && *s == ']') break; + Value * v2 = state.allocValue(); + parseJSON(state, s, *v2); + values.push_back(v2); + skipWhitespace(s); + if (*s == ']') break; + if (*s != ',') throw JSONParseError("expected ',' or ']' after JSON array element"); + s++; + } + s++; + state.mkList(v, values.size()); + for (size_t n = 0; n < values.size(); ++n) + v.listElems()[n] = values[n]; + } + + else if (*s == '{') { + s++; + ValueMap attrs; + while (1) { + skipWhitespace(s); + if (attrs.empty() && *s == '}') break; + string name = parseJSONString(s); + skipWhitespace(s); + if (*s != ':') throw JSONParseError("expected ':' in JSON object"); + s++; + Value * v2 = state.allocValue(); + parseJSON(state, s, *v2); + attrs[state.symbols.create(name)] = v2; + skipWhitespace(s); + if (*s == '}') break; + if (*s != ',') throw JSONParseError("expected ',' or '}' after JSON member"); + s++; + } + state.mkAttrs(v, attrs.size()); + for (auto & i : attrs) + v.attrs->push_back(Attr(i.first, i.second)); + v.attrs->sort(); + s++; + } + + else if (*s == '"') { + mkString(v, parseJSONString(s)); + } + + else if (isdigit(*s) || *s == '-' || *s == '.' ) { + // Buffer into a string first, then use built-in C++ conversions + std::string tmp_number; + ValueType number_type = tInt; + + while (isdigit(*s) || *s == '-' || *s == '.' || *s == 'e' || *s == 'E') { + if (*s == '.' || *s == 'e' || *s == 'E') + number_type = tFloat; + tmp_number += *s++; + } + + try { + if (number_type == tFloat) + mkFloat(v, stod(tmp_number)); + else + mkInt(v, stol(tmp_number)); + } catch (std::invalid_argument & e) { + throw JSONParseError("invalid JSON number"); + } catch (std::out_of_range & e) { + throw JSONParseError("out-of-range JSON number"); + } + } + + else if (strncmp(s, "true", 4) == 0) { + s += 4; + mkBool(v, true); + } + + else if (strncmp(s, "false", 5) == 0) { + s += 5; + mkBool(v, false); + } + + else if (strncmp(s, "null", 4) == 0) { + s += 4; + mkNull(v); + } + + else throw JSONParseError("unrecognised JSON value"); +} + + +void parseJSON(EvalState & state, const string & s_, Value & v) +{ + const char * s = s_.c_str(); + parseJSON(state, s, v); + skipWhitespace(s); + if (*s) throw JSONParseError(format("expected end-of-string while parsing JSON value: %1%") % s); +} + + +} diff --git a/third_party/nix/src/libexpr/json-to-value.hh b/third_party/nix/src/libexpr/json-to-value.hh new file mode 100644 index 000000000000..33f35b16ce89 --- /dev/null +++ b/third_party/nix/src/libexpr/json-to-value.hh @@ -0,0 +1,13 @@ +#pragma once + +#include "eval.hh" + +#include <string> + +namespace nix { + +MakeError(JSONParseError, EvalError) + +void parseJSON(EvalState & state, const string & s, Value & v); + +} diff --git a/third_party/nix/src/libexpr/lexer.l b/third_party/nix/src/libexpr/lexer.l new file mode 100644 index 000000000000..c34e5c383923 --- /dev/null +++ b/third_party/nix/src/libexpr/lexer.l @@ -0,0 +1,222 @@ +%option reentrant bison-bridge bison-locations +%option noyywrap +%option never-interactive +%option stack +%option nodefault +%option nounput noyy_top_state + + +%s DEFAULT +%x STRING +%x IND_STRING + + +%{ +#include <boost/lexical_cast.hpp> + +#include "nixexpr.hh" +#include "parser-tab.hh" + +using namespace nix; + +namespace nix { + + +static void initLoc(YYLTYPE * loc) +{ + loc->first_line = loc->last_line = 1; + loc->first_column = loc->last_column = 1; +} + + +static void adjustLoc(YYLTYPE * loc, const char * s, size_t len) +{ + loc->first_line = loc->last_line; + loc->first_column = loc->last_column; + + while (len--) { + switch (*s++) { + case '\r': + if (*s == '\n') /* cr/lf */ + s++; + /* fall through */ + case '\n': + ++loc->last_line; + loc->last_column = 1; + break; + default: + ++loc->last_column; + } + } +} + + +static Expr * unescapeStr(SymbolTable & symbols, const char * s, size_t length) +{ + string t; + t.reserve(length); + char c; + while ((c = *s++)) { + if (c == '\\') { + assert(*s); + c = *s++; + if (c == 'n') t += '\n'; + else if (c == 'r') t += '\r'; + else if (c == 't') t += '\t'; + else t += c; + } + else if (c == '\r') { + /* Normalise CR and CR/LF into LF. */ + t += '\n'; + if (*s == '\n') s++; /* cr/lf */ + } + else t += c; + } + return new ExprString(symbols.create(t)); +} + + +} + +#define YY_USER_INIT initLoc(yylloc) +#define YY_USER_ACTION adjustLoc(yylloc, yytext, yyleng); + +#define PUSH_STATE(state) yy_push_state(state, yyscanner) +#define POP_STATE() yy_pop_state(yyscanner) + +%} + + +ANY .|\n +ID [a-zA-Z\_][a-zA-Z0-9\_\'\-]* +INT [0-9]+ +FLOAT (([1-9][0-9]*\.[0-9]*)|(0?\.[0-9]+))([Ee][+-]?[0-9]+)? +PATH [a-zA-Z0-9\.\_\-\+]*(\/[a-zA-Z0-9\.\_\-\+]+)+\/? +HPATH \~(\/[a-zA-Z0-9\.\_\-\+]+)+\/? +SPATH \<[a-zA-Z0-9\.\_\-\+]+(\/[a-zA-Z0-9\.\_\-\+]+)*\> +URI [a-zA-Z][a-zA-Z0-9\+\-\.]*\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*\']+ + + +%% + + +if { return IF; } +then { return THEN; } +else { return ELSE; } +assert { return ASSERT; } +with { return WITH; } +let { return LET; } +in { return IN; } +rec { return REC; } +inherit { return INHERIT; } +or { return OR_KW; } +\.\.\. { return ELLIPSIS; } + +\=\= { return EQ; } +\!\= { return NEQ; } +\<\= { return LEQ; } +\>\= { return GEQ; } +\&\& { return AND; } +\|\| { return OR; } +\-\> { return IMPL; } +\/\/ { return UPDATE; } +\+\+ { return CONCAT; } + +{ID} { yylval->id = strdup(yytext); return ID; } +{INT} { errno = 0; + try { + yylval->n = boost::lexical_cast<int64_t>(yytext); + } catch (const boost::bad_lexical_cast &) { + throw ParseError(format("invalid integer '%1%'") % yytext); + } + return INT; + } +{FLOAT} { errno = 0; + yylval->nf = strtod(yytext, 0); + if (errno != 0) + throw ParseError(format("invalid float '%1%'") % yytext); + return FLOAT; + } + +\$\{ { PUSH_STATE(DEFAULT); return DOLLAR_CURLY; } + +\} { /* State INITIAL only exists at the bottom of the stack and is + used as a marker. DEFAULT replaces it everywhere else. + Popping when in INITIAL state causes an empty stack exception, + so don't */ + if (YYSTATE != INITIAL) + POP_STATE(); + return '}'; + } +\{ { PUSH_STATE(DEFAULT); return '{'; } + +\" { PUSH_STATE(STRING); return '"'; } +<STRING>([^\$\"\\]|\$[^\{\"\\]|\\{ANY}|\$\\{ANY})*\$/\" | +<STRING>([^\$\"\\]|\$[^\{\"\\]|\\{ANY}|\$\\{ANY})+ { + /* It is impossible to match strings ending with '$' with one + regex because trailing contexts are only valid at the end + of a rule. (A sane but undocumented limitation.) */ + yylval->e = unescapeStr(data->symbols, yytext, yyleng); + return STR; + } +<STRING>\$\{ { PUSH_STATE(DEFAULT); return DOLLAR_CURLY; } +<STRING>\" { POP_STATE(); return '"'; } +<STRING>\$|\\|\$\\ { + /* This can only occur when we reach EOF, otherwise the above + (...|\$[^\{\"\\]|\\.|\$\\.)+ would have triggered. + This is technically invalid, but we leave the problem to the + parser who fails with exact location. */ + return STR; + } + +\'\'(\ *\n)? { PUSH_STATE(IND_STRING); return IND_STRING_OPEN; } +<IND_STRING>([^\$\']|\$[^\{\']|\'[^\'\$])+ { + yylval->e = new ExprIndStr(yytext); + return IND_STR; + } +<IND_STRING>\'\'\$ | +<IND_STRING>\$ { + yylval->e = new ExprIndStr("$"); + return IND_STR; + } +<IND_STRING>\'\'\' { + yylval->e = new ExprIndStr("''"); + return IND_STR; + } +<IND_STRING>\'\'\\{ANY} { + yylval->e = unescapeStr(data->symbols, yytext + 2, yyleng - 2); + return IND_STR; + } +<IND_STRING>\$\{ { PUSH_STATE(DEFAULT); return DOLLAR_CURLY; } +<IND_STRING>\'\' { POP_STATE(); return IND_STRING_CLOSE; } +<IND_STRING>\' { + yylval->e = new ExprIndStr("'"); + return IND_STR; + } + + +{PATH} { if (yytext[yyleng-1] == '/') + throw ParseError("path '%s' has a trailing slash", yytext); + yylval->path = strdup(yytext); + return PATH; + } +{HPATH} { if (yytext[yyleng-1] == '/') + throw ParseError("path '%s' has a trailing slash", yytext); + yylval->path = strdup(yytext); + return HPATH; + } +{SPATH} { yylval->path = strdup(yytext); return SPATH; } +{URI} { yylval->uri = strdup(yytext); return URI; } + +[ \t\r\n]+ /* eat up whitespace */ +\#[^\r\n]* /* single-line comments */ +\/\*([^*]|\*+[^*/])*\*+\/ /* long comments */ + +{ANY} { + /* Don't return a negative number, as this will cause + Bison to stop parsing without an error. */ + return (unsigned char) yytext[0]; + } + +%% + diff --git a/third_party/nix/src/libexpr/local.mk b/third_party/nix/src/libexpr/local.mk new file mode 100644 index 000000000000..ccd5293e4e5e --- /dev/null +++ b/third_party/nix/src/libexpr/local.mk @@ -0,0 +1,33 @@ +libraries += libexpr + +libexpr_NAME = libnixexpr + +libexpr_DIR := $(d) + +libexpr_SOURCES := $(wildcard $(d)/*.cc) $(wildcard $(d)/primops/*.cc) $(d)/lexer-tab.cc $(d)/parser-tab.cc + +libexpr_LIBS = libutil libstore + +libexpr_LDFLAGS = +ifneq ($(OS), FreeBSD) + libexpr_LDFLAGS += -ldl +endif + +# The dependency on libgc must be propagated (i.e. meaning that +# programs/libraries that use libexpr must explicitly pass -lgc), +# because inline functions in libexpr's header files call libgc. +libexpr_LDFLAGS_PROPAGATED = $(BDW_GC_LIBS) + +libexpr_ORDER_AFTER := $(d)/parser-tab.cc $(d)/parser-tab.hh $(d)/lexer-tab.cc $(d)/lexer-tab.hh + +$(d)/parser-tab.cc $(d)/parser-tab.hh: $(d)/parser.y + $(trace-gen) bison -v -o $(libexpr_DIR)/parser-tab.cc $< -d + +$(d)/lexer-tab.cc $(d)/lexer-tab.hh: $(d)/lexer.l + $(trace-gen) flex --outfile $(libexpr_DIR)/lexer-tab.cc --header-file=$(libexpr_DIR)/lexer-tab.hh $< + +clean-files += $(d)/parser-tab.cc $(d)/parser-tab.hh $(d)/lexer-tab.cc $(d)/lexer-tab.hh + +dist-files += $(d)/parser-tab.cc $(d)/parser-tab.hh $(d)/lexer-tab.cc $(d)/lexer-tab.hh + +$(eval $(call install-file-in, $(d)/nix-expr.pc, $(prefix)/lib/pkgconfig, 0644)) diff --git a/third_party/nix/src/libexpr/names.cc b/third_party/nix/src/libexpr/names.cc new file mode 100644 index 000000000000..382088c78872 --- /dev/null +++ b/third_party/nix/src/libexpr/names.cc @@ -0,0 +1,107 @@ +#include "names.hh" +#include "util.hh" + + +namespace nix { + + +DrvName::DrvName() +{ + name = ""; +} + + +/* Parse a derivation name. The `name' part of a derivation name is + everything up to but not including the first dash *not* followed by + a letter. The `version' part is the rest (excluding the separating + dash). E.g., `apache-httpd-2.0.48' is parsed to (`apache-httpd', + '2.0.48'). */ +DrvName::DrvName(const string & s) : hits(0) +{ + name = fullName = s; + for (unsigned int i = 0; i < s.size(); ++i) { + /* !!! isalpha/isdigit are affected by the locale. */ + if (s[i] == '-' && i + 1 < s.size() && !isalpha(s[i + 1])) { + name = string(s, 0, i); + version = string(s, i + 1); + break; + } + } +} + + +bool DrvName::matches(DrvName & n) +{ + if (name != "*") { + if (!regex) regex = std::unique_ptr<std::regex>(new std::regex(name, std::regex::extended)); + if (!std::regex_match(n.name, *regex)) return false; + } + if (version != "" && version != n.version) return false; + return true; +} + + +string nextComponent(string::const_iterator & p, + const string::const_iterator end) +{ + /* Skip any dots and dashes (component separators). */ + while (p != end && (*p == '.' || *p == '-')) ++p; + + if (p == end) return ""; + + /* If the first character is a digit, consume the longest sequence + of digits. Otherwise, consume the longest sequence of + non-digit, non-separator characters. */ + string s; + if (isdigit(*p)) + while (p != end && isdigit(*p)) s += *p++; + else + while (p != end && (!isdigit(*p) && *p != '.' && *p != '-')) + s += *p++; + + return s; +} + + +static bool componentsLT(const string & c1, const string & c2) +{ + int n1, n2; + bool c1Num = string2Int(c1, n1), c2Num = string2Int(c2, n2); + + if (c1Num && c2Num) return n1 < n2; + else if (c1 == "" && c2Num) return true; + else if (c1 == "pre" && c2 != "pre") return true; + else if (c2 == "pre") return false; + /* Assume that `2.3a' < `2.3.1'. */ + else if (c2Num) return true; + else if (c1Num) return false; + else return c1 < c2; +} + + +int compareVersions(const string & v1, const string & v2) +{ + string::const_iterator p1 = v1.begin(); + string::const_iterator p2 = v2.begin(); + + while (p1 != v1.end() || p2 != v2.end()) { + string c1 = nextComponent(p1, v1.end()); + string c2 = nextComponent(p2, v2.end()); + if (componentsLT(c1, c2)) return -1; + else if (componentsLT(c2, c1)) return 1; + } + + return 0; +} + + +DrvNames drvNamesFromArgs(const Strings & opArgs) +{ + DrvNames result; + for (auto & i : opArgs) + result.push_back(DrvName(i)); + return result; +} + + +} diff --git a/third_party/nix/src/libexpr/names.hh b/third_party/nix/src/libexpr/names.hh new file mode 100644 index 000000000000..13c3093e77b0 --- /dev/null +++ b/third_party/nix/src/libexpr/names.hh @@ -0,0 +1,32 @@ +#pragma once + +#include <memory> + +#include "types.hh" +#include <regex> + +namespace nix { + +struct DrvName +{ + string fullName; + string name; + string version; + unsigned int hits; + + DrvName(); + DrvName(const string & s); + bool matches(DrvName & n); + +private: + std::unique_ptr<std::regex> regex; +}; + +typedef list<DrvName> DrvNames; + +string nextComponent(string::const_iterator & p, + const string::const_iterator end); +int compareVersions(const string & v1, const string & v2); +DrvNames drvNamesFromArgs(const Strings & opArgs); + +} diff --git a/third_party/nix/src/libexpr/nix-expr.pc.in b/third_party/nix/src/libexpr/nix-expr.pc.in new file mode 100644 index 000000000000..80f7a492b1a1 --- /dev/null +++ b/third_party/nix/src/libexpr/nix-expr.pc.in @@ -0,0 +1,10 @@ +prefix=@prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: Nix +Description: Nix Package Manager +Version: @PACKAGE_VERSION@ +Requires: nix-store bdw-gc +Libs: -L${libdir} -lnixexpr +Cflags: -I${includedir}/nix -std=c++17 diff --git a/third_party/nix/src/libexpr/nixexpr.cc b/third_party/nix/src/libexpr/nixexpr.cc new file mode 100644 index 000000000000..63cbef1ddf84 --- /dev/null +++ b/third_party/nix/src/libexpr/nixexpr.cc @@ -0,0 +1,438 @@ +#include "nixexpr.hh" +#include "derivations.hh" +#include "util.hh" + +#include <cstdlib> + + +namespace nix { + + +/* Displaying abstract syntax trees. */ + +std::ostream & operator << (std::ostream & str, const Expr & e) +{ + e.show(str); + return str; +} + +static void showString(std::ostream & str, const string & s) +{ + str << '"'; + for (auto c : (string) s) + if (c == '"' || c == '\\' || c == '$') str << "\\" << c; + else if (c == '\n') str << "\\n"; + else if (c == '\r') str << "\\r"; + else if (c == '\t') str << "\\t"; + else str << c; + str << '"'; +} + +static void showId(std::ostream & str, const string & s) +{ + if (s.empty()) + str << "\"\""; + else if (s == "if") // FIXME: handle other keywords + str << '"' << s << '"'; + else { + char c = s[0]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')) { + showString(str, s); + return; + } + for (auto c : s) + if (!((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '_' || c == '\'' || c == '-')) { + showString(str, s); + return; + } + str << s; + } +} + +std::ostream & operator << (std::ostream & str, const Symbol & sym) +{ + showId(str, *sym.s); + return str; +} + +void Expr::show(std::ostream & str) const +{ + abort(); +} + +void ExprInt::show(std::ostream & str) const +{ + str << n; +} + +void ExprFloat::show(std::ostream & str) const +{ + str << nf; +} + +void ExprString::show(std::ostream & str) const +{ + showString(str, s); +} + +void ExprPath::show(std::ostream & str) const +{ + str << s; +} + +void ExprVar::show(std::ostream & str) const +{ + str << name; +} + +void ExprSelect::show(std::ostream & str) const +{ + str << "(" << *e << ")." << showAttrPath(attrPath); + if (def) str << " or (" << *def << ")"; +} + +void ExprOpHasAttr::show(std::ostream & str) const +{ + str << "((" << *e << ") ? " << showAttrPath(attrPath) << ")"; +} + +void ExprAttrs::show(std::ostream & str) const +{ + if (recursive) str << "rec "; + str << "{ "; + for (auto & i : attrs) + if (i.second.inherited) + str << "inherit " << i.first << " " << "; "; + else + str << i.first << " = " << *i.second.e << "; "; + for (auto & i : dynamicAttrs) + str << "\"${" << *i.nameExpr << "}\" = " << *i.valueExpr << "; "; + str << "}"; +} + +void ExprList::show(std::ostream & str) const +{ + str << "[ "; + for (auto & i : elems) + str << "(" << *i << ") "; + str << "]"; +} + +void ExprLambda::show(std::ostream & str) const +{ + str << "("; + if (matchAttrs) { + str << "{ "; + bool first = true; + for (auto & i : formals->formals) { + if (first) first = false; else str << ", "; + str << i.name; + if (i.def) str << " ? " << *i.def; + } + if (formals->ellipsis) { + if (!first) str << ", "; + str << "..."; + } + str << " }"; + if (!arg.empty()) str << " @ "; + } + if (!arg.empty()) str << arg; + str << ": " << *body << ")"; +} + +void ExprLet::show(std::ostream & str) const +{ + str << "(let "; + for (auto & i : attrs->attrs) + if (i.second.inherited) { + str << "inherit " << i.first << "; "; + } + else + str << i.first << " = " << *i.second.e << "; "; + str << "in " << *body << ")"; +} + +void ExprWith::show(std::ostream & str) const +{ + str << "(with " << *attrs << "; " << *body << ")"; +} + +void ExprIf::show(std::ostream & str) const +{ + str << "(if " << *cond << " then " << *then << " else " << *else_ << ")"; +} + +void ExprAssert::show(std::ostream & str) const +{ + str << "assert " << *cond << "; " << *body; +} + +void ExprOpNot::show(std::ostream & str) const +{ + str << "(! " << *e << ")"; +} + +void ExprConcatStrings::show(std::ostream & str) const +{ + bool first = true; + str << "("; + for (auto & i : *es) { + if (first) first = false; else str << " + "; + str << *i; + } + str << ")"; +} + +void ExprPos::show(std::ostream & str) const +{ + str << "__curPos"; +} + + +std::ostream & operator << (std::ostream & str, const Pos & pos) +{ + if (!pos) + str << "undefined position"; + else + str << (format(ANSI_BOLD "%1%" ANSI_NORMAL ":%2%:%3%") % (string) pos.file % pos.line % pos.column).str(); + return str; +} + + +string showAttrPath(const AttrPath & attrPath) +{ + std::ostringstream out; + bool first = true; + for (auto & i : attrPath) { + if (!first) out << '.'; else first = false; + if (i.symbol.set()) + out << i.symbol; + else + out << "\"${" << *i.expr << "}\""; + } + return out.str(); +} + + +Pos noPos; + + +/* Computing levels/displacements for variables. */ + +void Expr::bindVars(const StaticEnv & env) +{ + abort(); +} + +void ExprInt::bindVars(const StaticEnv & env) +{ +} + +void ExprFloat::bindVars(const StaticEnv & env) +{ +} + +void ExprString::bindVars(const StaticEnv & env) +{ +} + +void ExprPath::bindVars(const StaticEnv & env) +{ +} + +void ExprVar::bindVars(const StaticEnv & env) +{ + /* Check whether the variable appears in the environment. If so, + set its level and displacement. */ + const StaticEnv * curEnv; + unsigned int level; + int withLevel = -1; + for (curEnv = &env, level = 0; curEnv; curEnv = curEnv->up, level++) { + if (curEnv->isWith) { + if (withLevel == -1) withLevel = level; + } else { + StaticEnv::Vars::const_iterator i = curEnv->vars.find(name); + if (i != curEnv->vars.end()) { + fromWith = false; + this->level = level; + displ = i->second; + return; + } + } + } + + /* Otherwise, the variable must be obtained from the nearest + enclosing `with'. If there is no `with', then we can issue an + "undefined variable" error now. */ + if (withLevel == -1) throw UndefinedVarError(format("undefined variable '%1%' at %2%") % name % pos); + + fromWith = true; + this->level = withLevel; +} + +void ExprSelect::bindVars(const StaticEnv & env) +{ + e->bindVars(env); + if (def) def->bindVars(env); + for (auto & i : attrPath) + if (!i.symbol.set()) + i.expr->bindVars(env); +} + +void ExprOpHasAttr::bindVars(const StaticEnv & env) +{ + e->bindVars(env); + for (auto & i : attrPath) + if (!i.symbol.set()) + i.expr->bindVars(env); +} + +void ExprAttrs::bindVars(const StaticEnv & env) +{ + const StaticEnv * dynamicEnv = &env; + StaticEnv newEnv(false, &env); + + if (recursive) { + dynamicEnv = &newEnv; + + unsigned int displ = 0; + for (auto & i : attrs) + newEnv.vars[i.first] = i.second.displ = displ++; + + for (auto & i : attrs) + i.second.e->bindVars(i.second.inherited ? env : newEnv); + } + + else + for (auto & i : attrs) + i.second.e->bindVars(env); + + for (auto & i : dynamicAttrs) { + i.nameExpr->bindVars(*dynamicEnv); + i.valueExpr->bindVars(*dynamicEnv); + } +} + +void ExprList::bindVars(const StaticEnv & env) +{ + for (auto & i : elems) + i->bindVars(env); +} + +void ExprLambda::bindVars(const StaticEnv & env) +{ + StaticEnv newEnv(false, &env); + + unsigned int displ = 0; + + if (!arg.empty()) newEnv.vars[arg] = displ++; + + if (matchAttrs) { + for (auto & i : formals->formals) + newEnv.vars[i.name] = displ++; + + for (auto & i : formals->formals) + if (i.def) i.def->bindVars(newEnv); + } + + body->bindVars(newEnv); +} + +void ExprLet::bindVars(const StaticEnv & env) +{ + StaticEnv newEnv(false, &env); + + unsigned int displ = 0; + for (auto & i : attrs->attrs) + newEnv.vars[i.first] = i.second.displ = displ++; + + for (auto & i : attrs->attrs) + i.second.e->bindVars(i.second.inherited ? env : newEnv); + + body->bindVars(newEnv); +} + +void ExprWith::bindVars(const StaticEnv & env) +{ + /* Does this `with' have an enclosing `with'? If so, record its + level so that `lookupVar' can look up variables in the previous + `with' if this one doesn't contain the desired attribute. */ + const StaticEnv * curEnv; + unsigned int level; + prevWith = 0; + for (curEnv = &env, level = 1; curEnv; curEnv = curEnv->up, level++) + if (curEnv->isWith) { + prevWith = level; + break; + } + + attrs->bindVars(env); + StaticEnv newEnv(true, &env); + body->bindVars(newEnv); +} + +void ExprIf::bindVars(const StaticEnv & env) +{ + cond->bindVars(env); + then->bindVars(env); + else_->bindVars(env); +} + +void ExprAssert::bindVars(const StaticEnv & env) +{ + cond->bindVars(env); + body->bindVars(env); +} + +void ExprOpNot::bindVars(const StaticEnv & env) +{ + e->bindVars(env); +} + +void ExprConcatStrings::bindVars(const StaticEnv & env) +{ + for (auto & i : *es) + i->bindVars(env); +} + +void ExprPos::bindVars(const StaticEnv & env) +{ +} + + +/* Storing function names. */ + +void Expr::setName(Symbol & name) +{ +} + + +void ExprLambda::setName(Symbol & name) +{ + this->name = name; + body->setName(name); +} + + +string ExprLambda::showNamePos() const +{ + return (format("%1% at %2%") % (name.set() ? "'" + (string) name + "'" : "anonymous function") % pos).str(); +} + + + +/* Symbol table. */ + +size_t SymbolTable::totalSize() const +{ + size_t n = 0; + for (auto & i : symbols) + n += i.size(); + return n; +} + + +} diff --git a/third_party/nix/src/libexpr/nixexpr.hh b/third_party/nix/src/libexpr/nixexpr.hh new file mode 100644 index 000000000000..665a42987dc1 --- /dev/null +++ b/third_party/nix/src/libexpr/nixexpr.hh @@ -0,0 +1,342 @@ +#pragma once + +#include "value.hh" +#include "symbol-table.hh" + +#include <map> + + +namespace nix { + + +MakeError(EvalError, Error) +MakeError(ParseError, Error) +MakeError(AssertionError, EvalError) +MakeError(ThrownError, AssertionError) +MakeError(Abort, EvalError) +MakeError(TypeError, EvalError) +MakeError(UndefinedVarError, Error) +MakeError(RestrictedPathError, Error) + + +/* Position objects. */ + +struct Pos +{ + Symbol file; + unsigned int line, column; + Pos() : line(0), column(0) { }; + Pos(const Symbol & file, unsigned int line, unsigned int column) + : file(file), line(line), column(column) { }; + operator bool() const + { + return line != 0; + } + bool operator < (const Pos & p2) const + { + if (!line) return p2.line; + if (!p2.line) return false; + int d = ((string) file).compare((string) p2.file); + if (d < 0) return true; + if (d > 0) return false; + if (line < p2.line) return true; + if (line > p2.line) return false; + return column < p2.column; + } +}; + +extern Pos noPos; + +std::ostream & operator << (std::ostream & str, const Pos & pos); + + +struct Env; +struct Value; +class EvalState; +struct StaticEnv; + + +/* An attribute path is a sequence of attribute names. */ +struct AttrName +{ + Symbol symbol; + Expr * expr; + AttrName(const Symbol & s) : symbol(s) {}; + AttrName(Expr * e) : expr(e) {}; +}; + +typedef std::vector<AttrName> AttrPath; + +string showAttrPath(const AttrPath & attrPath); + + +/* Abstract syntax of Nix expressions. */ + +struct Expr +{ + virtual ~Expr() { }; + virtual void show(std::ostream & str) const; + virtual void bindVars(const StaticEnv & env); + virtual void eval(EvalState & state, Env & env, Value & v); + virtual Value * maybeThunk(EvalState & state, Env & env); + virtual void setName(Symbol & name); +}; + +std::ostream & operator << (std::ostream & str, const Expr & e); + +#define COMMON_METHODS \ + void show(std::ostream & str) const; \ + void eval(EvalState & state, Env & env, Value & v); \ + void bindVars(const StaticEnv & env); + +struct ExprInt : Expr +{ + NixInt n; + Value v; + ExprInt(NixInt n) : n(n) { mkInt(v, n); }; + COMMON_METHODS + Value * maybeThunk(EvalState & state, Env & env); +}; + +struct ExprFloat : Expr +{ + NixFloat nf; + Value v; + ExprFloat(NixFloat nf) : nf(nf) { mkFloat(v, nf); }; + COMMON_METHODS + Value * maybeThunk(EvalState & state, Env & env); +}; + +struct ExprString : Expr +{ + Symbol s; + Value v; + ExprString(const Symbol & s) : s(s) { mkString(v, s); }; + COMMON_METHODS + Value * maybeThunk(EvalState & state, Env & env); +}; + +/* Temporary class used during parsing of indented strings. */ +struct ExprIndStr : Expr +{ + string s; + ExprIndStr(const string & s) : s(s) { }; +}; + +struct ExprPath : Expr +{ + string s; + Value v; + ExprPath(const string & s) : s(s) { mkPathNoCopy(v, this->s.c_str()); }; + COMMON_METHODS + Value * maybeThunk(EvalState & state, Env & env); +}; + +struct ExprVar : Expr +{ + Pos pos; + Symbol name; + + /* Whether the variable comes from an environment (e.g. a rec, let + or function argument) or from a "with". */ + bool fromWith; + + /* In the former case, the value is obtained by going `level' + levels up from the current environment and getting the + `displ'th value in that environment. In the latter case, the + value is obtained by getting the attribute named `name' from + the set stored in the environment that is `level' levels up + from the current one.*/ + unsigned int level; + unsigned int displ; + + ExprVar(const Symbol & name) : name(name) { }; + ExprVar(const Pos & pos, const Symbol & name) : pos(pos), name(name) { }; + COMMON_METHODS + Value * maybeThunk(EvalState & state, Env & env); +}; + +struct ExprSelect : Expr +{ + Pos pos; + Expr * e, * def; + AttrPath attrPath; + ExprSelect(const Pos & pos, Expr * e, const AttrPath & attrPath, Expr * def) : pos(pos), e(e), def(def), attrPath(attrPath) { }; + ExprSelect(const Pos & pos, Expr * e, const Symbol & name) : pos(pos), e(e), def(0) { attrPath.push_back(AttrName(name)); }; + COMMON_METHODS +}; + +struct ExprOpHasAttr : Expr +{ + Expr * e; + AttrPath attrPath; + ExprOpHasAttr(Expr * e, const AttrPath & attrPath) : e(e), attrPath(attrPath) { }; + COMMON_METHODS +}; + +struct ExprAttrs : Expr +{ + bool recursive; + struct AttrDef { + bool inherited; + Expr * e; + Pos pos; + unsigned int displ; // displacement + AttrDef(Expr * e, const Pos & pos, bool inherited=false) + : inherited(inherited), e(e), pos(pos) { }; + AttrDef() { }; + }; + typedef std::map<Symbol, AttrDef> AttrDefs; + AttrDefs attrs; + struct DynamicAttrDef { + Expr * nameExpr, * valueExpr; + Pos pos; + DynamicAttrDef(Expr * nameExpr, Expr * valueExpr, const Pos & pos) + : nameExpr(nameExpr), valueExpr(valueExpr), pos(pos) { }; + }; + typedef std::vector<DynamicAttrDef> DynamicAttrDefs; + DynamicAttrDefs dynamicAttrs; + ExprAttrs() : recursive(false) { }; + COMMON_METHODS +}; + +struct ExprList : Expr +{ + std::vector<Expr *> elems; + ExprList() { }; + COMMON_METHODS +}; + +struct Formal +{ + Symbol name; + Expr * def; + Formal(const Symbol & name, Expr * def) : name(name), def(def) { }; +}; + +struct Formals +{ + typedef std::list<Formal> Formals_; + Formals_ formals; + std::set<Symbol> argNames; // used during parsing + bool ellipsis; +}; + +struct ExprLambda : Expr +{ + Pos pos; + Symbol name; + Symbol arg; + bool matchAttrs; + Formals * formals; + Expr * body; + ExprLambda(const Pos & pos, const Symbol & arg, bool matchAttrs, Formals * formals, Expr * body) + : pos(pos), arg(arg), matchAttrs(matchAttrs), formals(formals), body(body) + { + if (!arg.empty() && formals && formals->argNames.find(arg) != formals->argNames.end()) + throw ParseError(format("duplicate formal function argument '%1%' at %2%") + % arg % pos); + }; + void setName(Symbol & name); + string showNamePos() const; + COMMON_METHODS +}; + +struct ExprLet : Expr +{ + ExprAttrs * attrs; + Expr * body; + ExprLet(ExprAttrs * attrs, Expr * body) : attrs(attrs), body(body) { }; + COMMON_METHODS +}; + +struct ExprWith : Expr +{ + Pos pos; + Expr * attrs, * body; + size_t prevWith; + ExprWith(const Pos & pos, Expr * attrs, Expr * body) : pos(pos), attrs(attrs), body(body) { }; + COMMON_METHODS +}; + +struct ExprIf : Expr +{ + Expr * cond, * then, * else_; + ExprIf(Expr * cond, Expr * then, Expr * else_) : cond(cond), then(then), else_(else_) { }; + COMMON_METHODS +}; + +struct ExprAssert : Expr +{ + Pos pos; + Expr * cond, * body; + ExprAssert(const Pos & pos, Expr * cond, Expr * body) : pos(pos), cond(cond), body(body) { }; + COMMON_METHODS +}; + +struct ExprOpNot : Expr +{ + Expr * e; + ExprOpNot(Expr * e) : e(e) { }; + COMMON_METHODS +}; + +#define MakeBinOp(name, s) \ + struct name : Expr \ + { \ + Pos pos; \ + Expr * e1, * e2; \ + name(Expr * e1, Expr * e2) : e1(e1), e2(e2) { }; \ + name(const Pos & pos, Expr * e1, Expr * e2) : pos(pos), e1(e1), e2(e2) { }; \ + void show(std::ostream & str) const \ + { \ + str << "(" << *e1 << " " s " " << *e2 << ")"; \ + } \ + void bindVars(const StaticEnv & env) \ + { \ + e1->bindVars(env); e2->bindVars(env); \ + } \ + void eval(EvalState & state, Env & env, Value & v); \ + }; + +MakeBinOp(ExprApp, "") +MakeBinOp(ExprOpEq, "==") +MakeBinOp(ExprOpNEq, "!=") +MakeBinOp(ExprOpAnd, "&&") +MakeBinOp(ExprOpOr, "||") +MakeBinOp(ExprOpImpl, "->") +MakeBinOp(ExprOpUpdate, "//") +MakeBinOp(ExprOpConcatLists, "++") + +struct ExprConcatStrings : Expr +{ + Pos pos; + bool forceString; + vector<Expr *> * es; + ExprConcatStrings(const Pos & pos, bool forceString, vector<Expr *> * es) + : pos(pos), forceString(forceString), es(es) { }; + COMMON_METHODS +}; + +struct ExprPos : Expr +{ + Pos pos; + ExprPos(const Pos & pos) : pos(pos) { }; + COMMON_METHODS +}; + + +/* Static environments are used to map variable names onto (level, + displacement) pairs used to obtain the value of the variable at + runtime. */ +struct StaticEnv +{ + bool isWith; + const StaticEnv * up; + typedef std::map<Symbol, unsigned int> Vars; + Vars vars; + StaticEnv(bool isWith, const StaticEnv * up) : isWith(isWith), up(up) { }; +}; + + +} diff --git a/third_party/nix/src/libexpr/parser.y b/third_party/nix/src/libexpr/parser.y new file mode 100644 index 000000000000..967c88d9bc80 --- /dev/null +++ b/third_party/nix/src/libexpr/parser.y @@ -0,0 +1,704 @@ +%glr-parser +%pure-parser +%locations +%define parse.error verbose +%defines +/* %no-lines */ +%parse-param { void * scanner } +%parse-param { nix::ParseData * data } +%lex-param { void * scanner } +%lex-param { nix::ParseData * data } +%expect 1 +%expect-rr 1 + +%code requires { + +#ifndef BISON_HEADER +#define BISON_HEADER + +#include "util.hh" + +#include "nixexpr.hh" +#include "eval.hh" + +namespace nix { + + struct ParseData + { + EvalState & state; + SymbolTable & symbols; + Expr * result; + Path basePath; + Symbol path; + string error; + Symbol sLetBody; + ParseData(EvalState & state) + : state(state) + , symbols(state.symbols) + , sLetBody(symbols.create("<let-body>")) + { }; + }; + +} + +#define YY_DECL int yylex \ + (YYSTYPE * yylval_param, YYLTYPE * yylloc_param, yyscan_t yyscanner, nix::ParseData * data) + +#endif + +} + +%{ + +#include "parser-tab.hh" +#include "lexer-tab.hh" + +YY_DECL; + +using namespace nix; + + +namespace nix { + + +static void dupAttr(const AttrPath & attrPath, const Pos & pos, const Pos & prevPos) +{ + throw ParseError(format("attribute '%1%' at %2% already defined at %3%") + % showAttrPath(attrPath) % pos % prevPos); +} + + +static void dupAttr(Symbol attr, const Pos & pos, const Pos & prevPos) +{ + throw ParseError(format("attribute '%1%' at %2% already defined at %3%") + % attr % pos % prevPos); +} + + +static void addAttr(ExprAttrs * attrs, AttrPath & attrPath, + Expr * e, const Pos & pos) +{ + AttrPath::iterator i; + // All attrpaths have at least one attr + assert(!attrPath.empty()); + // Checking attrPath validity. + // =========================== + for (i = attrPath.begin(); i + 1 < attrPath.end(); i++) { + if (i->symbol.set()) { + ExprAttrs::AttrDefs::iterator j = attrs->attrs.find(i->symbol); + if (j != attrs->attrs.end()) { + if (!j->second.inherited) { + ExprAttrs * attrs2 = dynamic_cast<ExprAttrs *>(j->second.e); + if (!attrs2) dupAttr(attrPath, pos, j->second.pos); + attrs = attrs2; + } else + dupAttr(attrPath, pos, j->second.pos); + } else { + ExprAttrs * nested = new ExprAttrs; + attrs->attrs[i->symbol] = ExprAttrs::AttrDef(nested, pos); + attrs = nested; + } + } else { + ExprAttrs *nested = new ExprAttrs; + attrs->dynamicAttrs.push_back(ExprAttrs::DynamicAttrDef(i->expr, nested, pos)); + attrs = nested; + } + } + // Expr insertion. + // ========================== + if (i->symbol.set()) { + ExprAttrs::AttrDefs::iterator j = attrs->attrs.find(i->symbol); + if (j != attrs->attrs.end()) { + // This attr path is already defined. However, if both + // e and the expr pointed by the attr path are two attribute sets, + // we want to merge them. + // Otherwise, throw an error. + auto ae = dynamic_cast<ExprAttrs *>(e); + auto jAttrs = dynamic_cast<ExprAttrs *>(j->second.e); + if (jAttrs && ae) { + for (auto & ad : ae->attrs) { + auto j2 = jAttrs->attrs.find(ad.first); + if (j2 != jAttrs->attrs.end()) // Attr already defined in iAttrs, error. + dupAttr(ad.first, j2->second.pos, ad.second.pos); + jAttrs->attrs[ad.first] = ad.second; + } + } else { + dupAttr(attrPath, pos, j->second.pos); + } + } else { + // This attr path is not defined. Let's create it. + attrs->attrs[i->symbol] = ExprAttrs::AttrDef(e, pos); + e->setName(i->symbol); + } + } else { + attrs->dynamicAttrs.push_back(ExprAttrs::DynamicAttrDef(i->expr, e, pos)); + } +} + + +static void addFormal(const Pos & pos, Formals * formals, const Formal & formal) +{ + if (formals->argNames.find(formal.name) != formals->argNames.end()) + throw ParseError(format("duplicate formal function argument '%1%' at %2%") + % formal.name % pos); + formals->formals.push_front(formal); + formals->argNames.insert(formal.name); +} + + +static Expr * stripIndentation(const Pos & pos, SymbolTable & symbols, vector<Expr *> & es) +{ + if (es.empty()) return new ExprString(symbols.create("")); + + /* Figure out the minimum indentation. Note that by design + whitespace-only final lines are not taken into account. (So + the " " in "\n ''" is ignored, but the " " in "\n foo''" is.) */ + bool atStartOfLine = true; /* = seen only whitespace in the current line */ + size_t minIndent = 1000000; + size_t curIndent = 0; + for (auto & i : es) { + ExprIndStr * e = dynamic_cast<ExprIndStr *>(i); + if (!e) { + /* Anti-quotations end the current start-of-line whitespace. */ + if (atStartOfLine) { + atStartOfLine = false; + if (curIndent < minIndent) minIndent = curIndent; + } + continue; + } + for (size_t j = 0; j < e->s.size(); ++j) { + if (atStartOfLine) { + if (e->s[j] == ' ') + curIndent++; + else if (e->s[j] == '\n') { + /* Empty line, doesn't influence minimum + indentation. */ + curIndent = 0; + } else { + atStartOfLine = false; + if (curIndent < minIndent) minIndent = curIndent; + } + } else if (e->s[j] == '\n') { + atStartOfLine = true; + curIndent = 0; + } + } + } + + /* Strip spaces from each line. */ + vector<Expr *> * es2 = new vector<Expr *>; + atStartOfLine = true; + size_t curDropped = 0; + size_t n = es.size(); + for (vector<Expr *>::iterator i = es.begin(); i != es.end(); ++i, --n) { + ExprIndStr * e = dynamic_cast<ExprIndStr *>(*i); + if (!e) { + atStartOfLine = false; + curDropped = 0; + es2->push_back(*i); + continue; + } + + string s2; + for (size_t j = 0; j < e->s.size(); ++j) { + if (atStartOfLine) { + if (e->s[j] == ' ') { + if (curDropped++ >= minIndent) + s2 += e->s[j]; + } + else if (e->s[j] == '\n') { + curDropped = 0; + s2 += e->s[j]; + } else { + atStartOfLine = false; + curDropped = 0; + s2 += e->s[j]; + } + } else { + s2 += e->s[j]; + if (e->s[j] == '\n') atStartOfLine = true; + } + } + + /* Remove the last line if it is empty and consists only of + spaces. */ + if (n == 1) { + string::size_type p = s2.find_last_of('\n'); + if (p != string::npos && s2.find_first_not_of(' ', p + 1) == string::npos) + s2 = string(s2, 0, p + 1); + } + + es2->push_back(new ExprString(symbols.create(s2))); + } + + /* If this is a single string, then don't do a concatenation. */ + return es2->size() == 1 && dynamic_cast<ExprString *>((*es2)[0]) ? (*es2)[0] : new ExprConcatStrings(pos, true, es2); +} + + +static inline Pos makeCurPos(const YYLTYPE & loc, ParseData * data) +{ + return Pos(data->path, loc.first_line, loc.first_column); +} + +#define CUR_POS makeCurPos(*yylocp, data) + + +} + + +void yyerror(YYLTYPE * loc, yyscan_t scanner, ParseData * data, const char * error) +{ + data->error = (format("%1%, at %2%") + % error % makeCurPos(*loc, data)).str(); +} + + +%} + +%union { + // !!! We're probably leaking stuff here. + nix::Expr * e; + nix::ExprList * list; + nix::ExprAttrs * attrs; + nix::Formals * formals; + nix::Formal * formal; + nix::NixInt n; + nix::NixFloat nf; + const char * id; // !!! -> Symbol + char * path; + char * uri; + std::vector<nix::AttrName> * attrNames; + std::vector<nix::Expr *> * string_parts; +} + +%type <e> start expr expr_function expr_if expr_op +%type <e> expr_app expr_select expr_simple +%type <list> expr_list +%type <attrs> binds +%type <formals> formals +%type <formal> formal +%type <attrNames> attrs attrpath +%type <string_parts> string_parts_interpolated ind_string_parts +%type <e> string_parts string_attr +%type <id> attr +%token <id> ID ATTRPATH +%token <e> STR IND_STR +%token <n> INT +%token <nf> FLOAT +%token <path> PATH HPATH SPATH +%token <uri> URI +%token IF THEN ELSE ASSERT WITH LET IN REC INHERIT EQ NEQ AND OR IMPL OR_KW +%token DOLLAR_CURLY /* == ${ */ +%token IND_STRING_OPEN IND_STRING_CLOSE +%token ELLIPSIS + +%right IMPL +%left OR +%left AND +%nonassoc EQ NEQ +%nonassoc '<' '>' LEQ GEQ +%right UPDATE +%left NOT +%left '+' '-' +%left '*' '/' +%right CONCAT +%nonassoc '?' +%nonassoc NEGATE + +%% + +start: expr { data->result = $1; }; + +expr: expr_function; + +expr_function + : ID ':' expr_function + { $$ = new ExprLambda(CUR_POS, data->symbols.create($1), false, 0, $3); } + | '{' formals '}' ':' expr_function + { $$ = new ExprLambda(CUR_POS, data->symbols.create(""), true, $2, $5); } + | '{' formals '}' '@' ID ':' expr_function + { $$ = new ExprLambda(CUR_POS, data->symbols.create($5), true, $2, $7); } + | ID '@' '{' formals '}' ':' expr_function + { $$ = new ExprLambda(CUR_POS, data->symbols.create($1), true, $4, $7); } + | ASSERT expr ';' expr_function + { $$ = new ExprAssert(CUR_POS, $2, $4); } + | WITH expr ';' expr_function + { $$ = new ExprWith(CUR_POS, $2, $4); } + | LET binds IN expr_function + { if (!$2->dynamicAttrs.empty()) + throw ParseError(format("dynamic attributes not allowed in let at %1%") + % CUR_POS); + $$ = new ExprLet($2, $4); + } + | expr_if + ; + +expr_if + : IF expr THEN expr ELSE expr { $$ = new ExprIf($2, $4, $6); } + | expr_op + ; + +expr_op + : '!' expr_op %prec NOT { $$ = new ExprOpNot($2); } + | '-' expr_op %prec NEGATE { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__sub")), new ExprInt(0)), $2); } + | expr_op EQ expr_op { $$ = new ExprOpEq($1, $3); } + | expr_op NEQ expr_op { $$ = new ExprOpNEq($1, $3); } + | expr_op '<' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__lessThan")), $1), $3); } + | expr_op LEQ expr_op { $$ = new ExprOpNot(new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__lessThan")), $3), $1)); } + | expr_op '>' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__lessThan")), $3), $1); } + | expr_op GEQ expr_op { $$ = new ExprOpNot(new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__lessThan")), $1), $3)); } + | expr_op AND expr_op { $$ = new ExprOpAnd(CUR_POS, $1, $3); } + | expr_op OR expr_op { $$ = new ExprOpOr(CUR_POS, $1, $3); } + | expr_op IMPL expr_op { $$ = new ExprOpImpl(CUR_POS, $1, $3); } + | expr_op UPDATE expr_op { $$ = new ExprOpUpdate(CUR_POS, $1, $3); } + | expr_op '?' attrpath { $$ = new ExprOpHasAttr($1, *$3); } + | expr_op '+' expr_op + { $$ = new ExprConcatStrings(CUR_POS, false, new vector<Expr *>({$1, $3})); } + | expr_op '-' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__sub")), $1), $3); } + | expr_op '*' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__mul")), $1), $3); } + | expr_op '/' expr_op { $$ = new ExprApp(CUR_POS, new ExprApp(new ExprVar(data->symbols.create("__div")), $1), $3); } + | expr_op CONCAT expr_op { $$ = new ExprOpConcatLists(CUR_POS, $1, $3); } + | expr_app + ; + +expr_app + : expr_app expr_select + { $$ = new ExprApp(CUR_POS, $1, $2); } + | expr_select { $$ = $1; } + ; + +expr_select + : expr_simple '.' attrpath + { $$ = new ExprSelect(CUR_POS, $1, *$3, 0); } + | expr_simple '.' attrpath OR_KW expr_select + { $$ = new ExprSelect(CUR_POS, $1, *$3, $5); } + | /* Backwards compatibility: because Nixpkgs has a rarely used + function named ‘or’, allow stuff like ‘map or [...]’. */ + expr_simple OR_KW + { $$ = new ExprApp(CUR_POS, $1, new ExprVar(CUR_POS, data->symbols.create("or"))); } + | expr_simple { $$ = $1; } + ; + +expr_simple + : ID { + if (strcmp($1, "__curPos") == 0) + $$ = new ExprPos(CUR_POS); + else + $$ = new ExprVar(CUR_POS, data->symbols.create($1)); + } + | INT { $$ = new ExprInt($1); } + | FLOAT { $$ = new ExprFloat($1); } + | '"' string_parts '"' { $$ = $2; } + | IND_STRING_OPEN ind_string_parts IND_STRING_CLOSE { + $$ = stripIndentation(CUR_POS, data->symbols, *$2); + } + | PATH { $$ = new ExprPath(absPath($1, data->basePath)); } + | HPATH { $$ = new ExprPath(getHome() + string{$1 + 1}); } + | SPATH { + string path($1 + 1, strlen($1) - 2); + $$ = new ExprApp(CUR_POS, + new ExprApp(new ExprVar(data->symbols.create("__findFile")), + new ExprVar(data->symbols.create("__nixPath"))), + new ExprString(data->symbols.create(path))); + } + | URI { $$ = new ExprString(data->symbols.create($1)); } + | '(' expr ')' { $$ = $2; } + /* Let expressions `let {..., body = ...}' are just desugared + into `(rec {..., body = ...}).body'. */ + | LET '{' binds '}' + { $3->recursive = true; $$ = new ExprSelect(noPos, $3, data->symbols.create("body")); } + | REC '{' binds '}' + { $3->recursive = true; $$ = $3; } + | '{' binds '}' + { $$ = $2; } + | '[' expr_list ']' { $$ = $2; } + ; + +string_parts + : STR + | string_parts_interpolated { $$ = new ExprConcatStrings(CUR_POS, true, $1); } + | { $$ = new ExprString(data->symbols.create("")); } + ; + +string_parts_interpolated + : string_parts_interpolated STR { $$ = $1; $1->push_back($2); } + | string_parts_interpolated DOLLAR_CURLY expr '}' { $$ = $1; $1->push_back($3); } + | DOLLAR_CURLY expr '}' { $$ = new vector<Expr *>; $$->push_back($2); } + | STR DOLLAR_CURLY expr '}' { + $$ = new vector<Expr *>; + $$->push_back($1); + $$->push_back($3); + } + ; + +ind_string_parts + : ind_string_parts IND_STR { $$ = $1; $1->push_back($2); } + | ind_string_parts DOLLAR_CURLY expr '}' { $$ = $1; $1->push_back($3); } + | { $$ = new vector<Expr *>; } + ; + +binds + : binds attrpath '=' expr ';' { $$ = $1; addAttr($$, *$2, $4, makeCurPos(@2, data)); } + | binds INHERIT attrs ';' + { $$ = $1; + for (auto & i : *$3) { + if ($$->attrs.find(i.symbol) != $$->attrs.end()) + dupAttr(i.symbol, makeCurPos(@3, data), $$->attrs[i.symbol].pos); + Pos pos = makeCurPos(@3, data); + $$->attrs[i.symbol] = ExprAttrs::AttrDef(new ExprVar(CUR_POS, i.symbol), pos, true); + } + } + | binds INHERIT '(' expr ')' attrs ';' + { $$ = $1; + /* !!! Should ensure sharing of the expression in $4. */ + for (auto & i : *$6) { + if ($$->attrs.find(i.symbol) != $$->attrs.end()) + dupAttr(i.symbol, makeCurPos(@6, data), $$->attrs[i.symbol].pos); + $$->attrs[i.symbol] = ExprAttrs::AttrDef(new ExprSelect(CUR_POS, $4, i.symbol), makeCurPos(@6, data)); + } + } + | { $$ = new ExprAttrs; } + ; + +attrs + : attrs attr { $$ = $1; $1->push_back(AttrName(data->symbols.create($2))); } + | attrs string_attr + { $$ = $1; + ExprString * str = dynamic_cast<ExprString *>($2); + if (str) { + $$->push_back(AttrName(str->s)); + delete str; + } else + throw ParseError(format("dynamic attributes not allowed in inherit at %1%") + % makeCurPos(@2, data)); + } + | { $$ = new AttrPath; } + ; + +attrpath + : attrpath '.' attr { $$ = $1; $1->push_back(AttrName(data->symbols.create($3))); } + | attrpath '.' string_attr + { $$ = $1; + ExprString * str = dynamic_cast<ExprString *>($3); + if (str) { + $$->push_back(AttrName(str->s)); + delete str; + } else + $$->push_back(AttrName($3)); + } + | attr { $$ = new vector<AttrName>; $$->push_back(AttrName(data->symbols.create($1))); } + | string_attr + { $$ = new vector<AttrName>; + ExprString *str = dynamic_cast<ExprString *>($1); + if (str) { + $$->push_back(AttrName(str->s)); + delete str; + } else + $$->push_back(AttrName($1)); + } + ; + +attr + : ID { $$ = $1; } + | OR_KW { $$ = "or"; } + ; + +string_attr + : '"' string_parts '"' { $$ = $2; } + | DOLLAR_CURLY expr '}' { $$ = $2; } + ; + +expr_list + : expr_list expr_select { $$ = $1; $1->elems.push_back($2); /* !!! dangerous */ } + | { $$ = new ExprList; } + ; + +formals + : formal ',' formals + { $$ = $3; addFormal(CUR_POS, $$, *$1); } + | formal + { $$ = new Formals; addFormal(CUR_POS, $$, *$1); $$->ellipsis = false; } + | + { $$ = new Formals; $$->ellipsis = false; } + | ELLIPSIS + { $$ = new Formals; $$->ellipsis = true; } + ; + +formal + : ID { $$ = new Formal(data->symbols.create($1), 0); } + | ID '?' expr { $$ = new Formal(data->symbols.create($1), $3); } + ; + +%% + + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> + +#include "eval.hh" +#include "download.hh" +#include "store-api.hh" + + +namespace nix { + + +Expr * EvalState::parse(const char * text, + const Path & path, const Path & basePath, StaticEnv & staticEnv) +{ + yyscan_t scanner; + ParseData data(*this); + data.basePath = basePath; + data.path = data.symbols.create(path); + + yylex_init(&scanner); + yy_scan_string(text, scanner); + int res = yyparse(scanner, &data); + yylex_destroy(scanner); + + if (res) throw ParseError(data.error); + + data.result->bindVars(staticEnv); + + return data.result; +} + + +Path resolveExprPath(Path path) +{ + assert(path[0] == '/'); + + /* If `path' is a symlink, follow it. This is so that relative + path references work. */ + struct stat st; + while (true) { + if (lstat(path.c_str(), &st)) + throw SysError(format("getting status of '%1%'") % path); + if (!S_ISLNK(st.st_mode)) break; + path = absPath(readLink(path), dirOf(path)); + } + + /* If `path' refers to a directory, append `/default.nix'. */ + if (S_ISDIR(st.st_mode)) + path = canonPath(path + "/default.nix"); + + return path; +} + + +Expr * EvalState::parseExprFromFile(const Path & path) +{ + return parseExprFromFile(path, staticBaseEnv); +} + + +Expr * EvalState::parseExprFromFile(const Path & path, StaticEnv & staticEnv) +{ + return parse(readFile(path).c_str(), path, dirOf(path), staticEnv); +} + + +Expr * EvalState::parseExprFromString(const string & s, const Path & basePath, StaticEnv & staticEnv) +{ + return parse(s.c_str(), "(string)", basePath, staticEnv); +} + + +Expr * EvalState::parseExprFromString(const string & s, const Path & basePath) +{ + return parseExprFromString(s, basePath, staticBaseEnv); +} + + +Expr * EvalState::parseStdin() +{ + //Activity act(*logger, lvlTalkative, format("parsing standard input")); + return parseExprFromString(drainFD(0), absPath(".")); +} + + +void EvalState::addToSearchPath(const string & s) +{ + size_t pos = s.find('='); + string prefix; + Path path; + if (pos == string::npos) { + path = s; + } else { + prefix = string(s, 0, pos); + path = string(s, pos + 1); + } + + searchPath.emplace_back(prefix, path); +} + + +Path EvalState::findFile(const string & path) +{ + return findFile(searchPath, path); +} + + +Path EvalState::findFile(SearchPath & searchPath, const string & path, const Pos & pos) +{ + for (auto & i : searchPath) { + std::string suffix; + if (i.first.empty()) + suffix = "/" + path; + else { + auto s = i.first.size(); + if (path.compare(0, s, i.first) != 0 || + (path.size() > s && path[s] != '/')) + continue; + suffix = path.size() == s ? "" : "/" + string(path, s); + } + auto r = resolveSearchPathElem(i); + if (!r.first) continue; + Path res = r.second + suffix; + if (pathExists(res)) return canonPath(res); + } + format f = format( + "file '%1%' was not found in the Nix search path (add it using $NIX_PATH or -I)" + + string(pos ? ", at %2%" : "")); + f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit); + throw ThrownError(f % path % pos); +} + + +std::pair<bool, std::string> EvalState::resolveSearchPathElem(const SearchPathElem & elem) +{ + auto i = searchPathResolved.find(elem.second); + if (i != searchPathResolved.end()) return i->second; + + std::pair<bool, std::string> res; + + if (isUri(elem.second)) { + try { + CachedDownloadRequest request(elem.second); + request.unpack = true; + res = { true, getDownloader()->downloadCached(store, request).path }; + } catch (DownloadError & e) { + printError(format("warning: Nix search path entry '%1%' cannot be downloaded, ignoring") % elem.second); + res = { false, "" }; + } + } else { + auto path = absPath(elem.second); + if (pathExists(path)) + res = { true, path }; + else { + printError(format("warning: Nix search path entry '%1%' does not exist, ignoring") % elem.second); + res = { false, "" }; + } + } + + debug(format("resolved search path element '%s' to '%s'") % elem.second % res.second); + + searchPathResolved[elem.second] = res; + return res; +} + + +} diff --git a/third_party/nix/src/libexpr/primops.cc b/third_party/nix/src/libexpr/primops.cc new file mode 100644 index 000000000000..d4c60f870ed2 --- /dev/null +++ b/third_party/nix/src/libexpr/primops.cc @@ -0,0 +1,2329 @@ +#include "archive.hh" +#include "derivations.hh" +#include "download.hh" +#include "eval-inline.hh" +#include "eval.hh" +#include "globals.hh" +#include "json-to-value.hh" +#include "names.hh" +#include "store-api.hh" +#include "util.hh" +#include "json.hh" +#include "value-to-json.hh" +#include "value-to-xml.hh" +#include "primops.hh" + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <algorithm> +#include <cstring> +#include <regex> +#include <dlfcn.h> + + +namespace nix { + + +/************************************************************* + * Miscellaneous + *************************************************************/ + + +/* Decode a context string ‘!<name>!<path>’ into a pair <path, + name>. */ +std::pair<string, string> decodeContext(const string & s) +{ + if (s.at(0) == '!') { + size_t index = s.find("!", 1); + return std::pair<string, string>(string(s, index + 1), string(s, 1, index - 1)); + } else + return std::pair<string, string>(s.at(0) == '/' ? s : string(s, 1), ""); +} + + +InvalidPathError::InvalidPathError(const Path & path) : + EvalError(format("path '%1%' is not valid") % path), path(path) {} + +void EvalState::realiseContext(const PathSet & context) +{ + PathSet drvs; + + for (auto & i : context) { + std::pair<string, string> decoded = decodeContext(i); + Path ctx = decoded.first; + assert(store->isStorePath(ctx)); + if (!store->isValidPath(ctx)) + throw InvalidPathError(ctx); + if (!decoded.second.empty() && nix::isDerivation(ctx)) { + drvs.insert(decoded.first + "!" + decoded.second); + + /* Add the output of this derivation to the allowed + paths. */ + if (allowedPaths) { + auto drv = store->derivationFromPath(decoded.first); + DerivationOutputs::iterator i = drv.outputs.find(decoded.second); + if (i == drv.outputs.end()) + throw Error("derivation '%s' does not have an output named '%s'", decoded.first, decoded.second); + allowedPaths->insert(i->second.path); + } + } + } + + if (drvs.empty()) return; + + if (!evalSettings.enableImportFromDerivation) + throw EvalError(format("attempted to realize '%1%' during evaluation but 'allow-import-from-derivation' is false") % *(drvs.begin())); + + /* For performance, prefetch all substitute info. */ + PathSet willBuild, willSubstitute, unknown; + unsigned long long downloadSize, narSize; + store->queryMissing(drvs, willBuild, willSubstitute, unknown, downloadSize, narSize); + store->buildPaths(drvs); +} + + +/* Load and evaluate an expression from path specified by the + argument. */ +static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + Path path = state.coerceToPath(pos, *args[1], context); + + try { + state.realiseContext(context); + } catch (InvalidPathError & e) { + throw EvalError(format("cannot import '%1%', since path '%2%' is not valid, at %3%") + % path % e.path % pos); + } + + Path realPath = state.checkSourcePath(state.toRealPath(path, context)); + + if (state.store->isStorePath(path) && state.store->isValidPath(path) && isDerivation(path)) { + Derivation drv = readDerivation(realPath); + Value & w = *state.allocValue(); + state.mkAttrs(w, 3 + drv.outputs.size()); + Value * v2 = state.allocAttr(w, state.sDrvPath); + mkString(*v2, path, {"=" + path}); + v2 = state.allocAttr(w, state.sName); + mkString(*v2, drv.env["name"]); + Value * outputsVal = + state.allocAttr(w, state.symbols.create("outputs")); + state.mkList(*outputsVal, drv.outputs.size()); + unsigned int outputs_index = 0; + + for (const auto & o : drv.outputs) { + v2 = state.allocAttr(w, state.symbols.create(o.first)); + mkString(*v2, o.second.path, {"!" + o.first + "!" + path}); + outputsVal->listElems()[outputs_index] = state.allocValue(); + mkString(*(outputsVal->listElems()[outputs_index++]), o.first); + } + w.attrs->sort(); + Value fun; + state.evalFile(settings.nixDataDir + "/nix/corepkgs/imported-drv-to-derivation.nix", fun); + state.forceFunction(fun, pos); + mkApp(v, fun, w); + state.forceAttrs(v, pos); + } else { + state.forceAttrs(*args[0]); + if (args[0]->attrs->empty()) + state.evalFile(realPath, v); + else { + Env * env = &state.allocEnv(args[0]->attrs->size()); + env->up = &state.baseEnv; + + StaticEnv staticEnv(false, &state.staticBaseEnv); + + unsigned int displ = 0; + for (auto & attr : *args[0]->attrs) { + staticEnv.vars[attr.name] = displ; + env->values[displ++] = attr.value; + } + + printTalkative("evaluating file '%1%'", realPath); + Expr * e = state.parseExprFromFile(resolveExprPath(realPath), staticEnv); + + e->eval(state, *env, v); + } + } +} + + +/* Want reasonable symbol names, so extern C */ +/* !!! Should we pass the Pos or the file name too? */ +extern "C" typedef void (*ValueInitializer)(EvalState & state, Value & v); + +/* Load a ValueInitializer from a DSO and return whatever it initializes */ +void prim_importNative(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + Path path = state.coerceToPath(pos, *args[0], context); + + try { + state.realiseContext(context); + } catch (InvalidPathError & e) { + throw EvalError(format("cannot import '%1%', since path '%2%' is not valid, at %3%") + % path % e.path % pos); + } + + path = state.checkSourcePath(path); + + string sym = state.forceStringNoCtx(*args[1], pos); + + void *handle = dlopen(path.c_str(), RTLD_LAZY | RTLD_LOCAL); + if (!handle) + throw EvalError(format("could not open '%1%': %2%") % path % dlerror()); + + dlerror(); + ValueInitializer func = (ValueInitializer) dlsym(handle, sym.c_str()); + if(!func) { + char *message = dlerror(); + if (message) + throw EvalError(format("could not load symbol '%1%' from '%2%': %3%") % sym % path % message); + else + throw EvalError(format("symbol '%1%' from '%2%' resolved to NULL when a function pointer was expected") + % sym % path); + } + + (func)(state, v); + + /* We don't dlclose because v may be a primop referencing a function in the shared object file */ +} + + +/* Execute a program and parse its output */ +void prim_exec(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceList(*args[0], pos); + auto elems = args[0]->listElems(); + auto count = args[0]->listSize(); + if (count == 0) { + throw EvalError(format("at least one argument to 'exec' required, at %1%") % pos); + } + PathSet context; + auto program = state.coerceToString(pos, *elems[0], context, false, false); + Strings commandArgs; + for (unsigned int i = 1; i < args[0]->listSize(); ++i) { + commandArgs.emplace_back(state.coerceToString(pos, *elems[i], context, false, false)); + } + try { + state.realiseContext(context); + } catch (InvalidPathError & e) { + throw EvalError(format("cannot execute '%1%', since path '%2%' is not valid, at %3%") + % program % e.path % pos); + } + + auto output = runProgram(program, true, commandArgs); + Expr * parsed; + try { + parsed = state.parseExprFromString(output, pos.file); + } catch (Error & e) { + e.addPrefix(format("While parsing the output from '%1%', at %2%\n") % program % pos); + throw; + } + try { + state.eval(parsed, v); + } catch (Error & e) { + e.addPrefix(format("While evaluating the output from '%1%', at %2%\n") % program % pos); + throw; + } +} + + +/* Return a string representing the type of the expression. */ +static void prim_typeOf(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + string t; + switch (args[0]->type) { + case tInt: t = "int"; break; + case tBool: t = "bool"; break; + case tString: t = "string"; break; + case tPath: t = "path"; break; + case tNull: t = "null"; break; + case tAttrs: t = "set"; break; + case tList1: case tList2: case tListN: t = "list"; break; + case tLambda: + case tPrimOp: + case tPrimOpApp: + t = "lambda"; + break; + case tExternal: + t = args[0]->external->typeOf(); + break; + case tFloat: t = "float"; break; + default: abort(); + } + mkString(v, state.symbols.create(t)); +} + + +/* Determine whether the argument is the null value. */ +static void prim_isNull(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + mkBool(v, args[0]->type == tNull); +} + + +/* Determine whether the argument is a function. */ +static void prim_isFunction(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + bool res; + switch (args[0]->type) { + case tLambda: + case tPrimOp: + case tPrimOpApp: + res = true; + break; + default: + res = false; + break; + } + mkBool(v, res); +} + + +/* Determine whether the argument is an integer. */ +static void prim_isInt(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + mkBool(v, args[0]->type == tInt); +} + +/* Determine whether the argument is a float. */ +static void prim_isFloat(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + mkBool(v, args[0]->type == tFloat); +} + +/* Determine whether the argument is a string. */ +static void prim_isString(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + mkBool(v, args[0]->type == tString); +} + + +/* Determine whether the argument is a Boolean. */ +static void prim_isBool(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + mkBool(v, args[0]->type == tBool); +} + +/* Determine whether the argument is a path. */ +static void prim_isPath(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + mkBool(v, args[0]->type == tPath); +} + +struct CompareValues +{ + bool operator () (const Value * v1, const Value * v2) const + { + if (v1->type == tFloat && v2->type == tInt) + return v1->fpoint < v2->integer; + if (v1->type == tInt && v2->type == tFloat) + return v1->integer < v2->fpoint; + if (v1->type != v2->type) + throw EvalError(format("cannot compare %1% with %2%") % showType(*v1) % showType(*v2)); + switch (v1->type) { + case tInt: + return v1->integer < v2->integer; + case tFloat: + return v1->fpoint < v2->fpoint; + case tString: + return strcmp(v1->string.s, v2->string.s) < 0; + case tPath: + return strcmp(v1->path, v2->path) < 0; + default: + throw EvalError(format("cannot compare %1% with %2%") % showType(*v1) % showType(*v2)); + } + } +}; + + +#if HAVE_BOEHMGC +typedef list<Value *, gc_allocator<Value *> > ValueList; +#else +typedef list<Value *> ValueList; +#endif + + +static void prim_genericClosure(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceAttrs(*args[0], pos); + + /* Get the start set. */ + Bindings::iterator startSet = + args[0]->attrs->find(state.symbols.create("startSet")); + if (startSet == args[0]->attrs->end()) + throw EvalError(format("attribute 'startSet' required, at %1%") % pos); + state.forceList(*startSet->value, pos); + + ValueList workSet; + for (unsigned int n = 0; n < startSet->value->listSize(); ++n) + workSet.push_back(startSet->value->listElems()[n]); + + /* Get the operator. */ + Bindings::iterator op = + args[0]->attrs->find(state.symbols.create("operator")); + if (op == args[0]->attrs->end()) + throw EvalError(format("attribute 'operator' required, at %1%") % pos); + state.forceValue(*op->value); + + /* Construct the closure by applying the operator to element of + `workSet', adding the result to `workSet', continuing until + no new elements are found. */ + ValueList res; + // `doneKeys' doesn't need to be a GC root, because its values are + // reachable from res. + set<Value *, CompareValues> doneKeys; + while (!workSet.empty()) { + Value * e = *(workSet.begin()); + workSet.pop_front(); + + state.forceAttrs(*e, pos); + + Bindings::iterator key = + e->attrs->find(state.symbols.create("key")); + if (key == e->attrs->end()) + throw EvalError(format("attribute 'key' required, at %1%") % pos); + state.forceValue(*key->value); + + if (doneKeys.find(key->value) != doneKeys.end()) continue; + doneKeys.insert(key->value); + res.push_back(e); + + /* Call the `operator' function with `e' as argument. */ + Value call; + mkApp(call, *op->value, *e); + state.forceList(call, pos); + + /* Add the values returned by the operator to the work set. */ + for (unsigned int n = 0; n < call.listSize(); ++n) { + state.forceValue(*call.listElems()[n]); + workSet.push_back(call.listElems()[n]); + } + } + + /* Create the result list. */ + state.mkList(v, res.size()); + unsigned int n = 0; + for (auto & i : res) + v.listElems()[n++] = i; +} + + +static void prim_abort(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + string s = state.coerceToString(pos, *args[0], context); + throw Abort(format("evaluation aborted with the following error message: '%1%'") % s); +} + + +static void prim_throw(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + string s = state.coerceToString(pos, *args[0], context); + throw ThrownError(s); +} + + +static void prim_addErrorContext(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + try { + state.forceValue(*args[1]); + v = *args[1]; + } catch (Error & e) { + PathSet context; + e.addPrefix(format("%1%\n") % state.coerceToString(pos, *args[0], context)); + throw; + } +} + + +/* Try evaluating the argument. Success => {success=true; value=something;}, + * else => {success=false; value=false;} */ +static void prim_tryEval(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.mkAttrs(v, 2); + try { + state.forceValue(*args[0]); + v.attrs->push_back(Attr(state.sValue, args[0])); + mkBool(*state.allocAttr(v, state.symbols.create("success")), true); + } catch (AssertionError & e) { + mkBool(*state.allocAttr(v, state.sValue), false); + mkBool(*state.allocAttr(v, state.symbols.create("success")), false); + } + v.attrs->sort(); +} + + +/* Return an environment variable. Use with care. */ +static void prim_getEnv(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string name = state.forceStringNoCtx(*args[0], pos); + mkString(v, evalSettings.restrictEval || evalSettings.pureEval ? "" : getEnv(name)); +} + + +/* Evaluate the first argument, then return the second argument. */ +static void prim_seq(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + state.forceValue(*args[1]); + v = *args[1]; +} + + +/* Evaluate the first argument deeply (i.e. recursing into lists and + attrsets), then return the second argument. */ +static void prim_deepSeq(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValueDeep(*args[0]); + state.forceValue(*args[1]); + v = *args[1]; +} + + +/* Evaluate the first expression and print it on standard error. Then + return the second expression. Useful for debugging. */ +static void prim_trace(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + if (args[0]->type == tString) + printError(format("trace: %1%") % args[0]->string.s); + else + printError(format("trace: %1%") % *args[0]); + state.forceValue(*args[1]); + v = *args[1]; +} + + +void prim_valueSize(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + /* We're not forcing the argument on purpose. */ + mkInt(v, valueSize(*args[0])); +} + + +/************************************************************* + * Derivations + *************************************************************/ + + +/* Construct (as a unobservable side effect) a Nix derivation + expression that performs the derivation described by the argument + set. Returns the original set extended with the following + attributes: `outPath' containing the primary output path of the + derivation; `drvPath' containing the path of the Nix expression; + and `type' set to `derivation' to indicate that this is a + derivation. */ +static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceAttrs(*args[0], pos); + + /* Figure out the name first (for stack backtraces). */ + Bindings::iterator attr = args[0]->attrs->find(state.sName); + if (attr == args[0]->attrs->end()) + throw EvalError(format("required attribute 'name' missing, at %1%") % pos); + string drvName; + Pos & posDrvName(*attr->pos); + try { + drvName = state.forceStringNoCtx(*attr->value, pos); + } catch (Error & e) { + e.addPrefix(format("while evaluating the derivation attribute 'name' at %1%:\n") % posDrvName); + throw; + } + + /* Check whether attributes should be passed as a JSON file. */ + std::ostringstream jsonBuf; + std::unique_ptr<JSONObject> jsonObject; + attr = args[0]->attrs->find(state.sStructuredAttrs); + if (attr != args[0]->attrs->end() && state.forceBool(*attr->value, pos)) + jsonObject = std::make_unique<JSONObject>(jsonBuf); + + /* Check whether null attributes should be ignored. */ + bool ignoreNulls = false; + attr = args[0]->attrs->find(state.sIgnoreNulls); + if (attr != args[0]->attrs->end()) + ignoreNulls = state.forceBool(*attr->value, pos); + + /* Build the derivation expression by processing the attributes. */ + Derivation drv; + + PathSet context; + + std::optional<std::string> outputHash; + std::string outputHashAlgo; + bool outputHashRecursive = false; + + StringSet outputs; + outputs.insert("out"); + + for (auto & i : args[0]->attrs->lexicographicOrder()) { + if (i->name == state.sIgnoreNulls) continue; + const string & key = i->name; + vomit("processing attribute '%1%'", key); + + auto handleHashMode = [&](const std::string & s) { + if (s == "recursive") outputHashRecursive = true; + else if (s == "flat") outputHashRecursive = false; + else throw EvalError("invalid value '%s' for 'outputHashMode' attribute, at %s", s, posDrvName); + }; + + auto handleOutputs = [&](const Strings & ss) { + outputs.clear(); + for (auto & j : ss) { + if (outputs.find(j) != outputs.end()) + throw EvalError(format("duplicate derivation output '%1%', at %2%") % j % posDrvName); + /* !!! Check whether j is a valid attribute + name. */ + /* Derivations cannot be named ‘drv’, because + then we'd have an attribute ‘drvPath’ in + the resulting set. */ + if (j == "drv") + throw EvalError(format("invalid derivation output name 'drv', at %1%") % posDrvName); + outputs.insert(j); + } + if (outputs.empty()) + throw EvalError(format("derivation cannot have an empty set of outputs, at %1%") % posDrvName); + }; + + try { + + if (ignoreNulls) { + state.forceValue(*i->value); + if (i->value->type == tNull) continue; + } + + /* The `args' attribute is special: it supplies the + command-line arguments to the builder. */ + if (i->name == state.sArgs) { + state.forceList(*i->value, pos); + for (unsigned int n = 0; n < i->value->listSize(); ++n) { + string s = state.coerceToString(posDrvName, *i->value->listElems()[n], context, true); + drv.args.push_back(s); + } + } + + /* All other attributes are passed to the builder through + the environment. */ + else { + + if (jsonObject) { + + if (i->name == state.sStructuredAttrs) continue; + + auto placeholder(jsonObject->placeholder(key)); + printValueAsJSON(state, true, *i->value, placeholder, context); + + if (i->name == state.sBuilder) + drv.builder = state.forceString(*i->value, context, posDrvName); + else if (i->name == state.sSystem) + drv.platform = state.forceStringNoCtx(*i->value, posDrvName); + else if (i->name == state.sOutputHash) + outputHash = state.forceStringNoCtx(*i->value, posDrvName); + else if (i->name == state.sOutputHashAlgo) + outputHashAlgo = state.forceStringNoCtx(*i->value, posDrvName); + else if (i->name == state.sOutputHashMode) + handleHashMode(state.forceStringNoCtx(*i->value, posDrvName)); + else if (i->name == state.sOutputs) { + /* Require ‘outputs’ to be a list of strings. */ + state.forceList(*i->value, posDrvName); + Strings ss; + for (unsigned int n = 0; n < i->value->listSize(); ++n) + ss.emplace_back(state.forceStringNoCtx(*i->value->listElems()[n], posDrvName)); + handleOutputs(ss); + } + + } else { + auto s = state.coerceToString(posDrvName, *i->value, context, true); + drv.env.emplace(key, s); + if (i->name == state.sBuilder) drv.builder = s; + else if (i->name == state.sSystem) drv.platform = s; + else if (i->name == state.sOutputHash) outputHash = s; + else if (i->name == state.sOutputHashAlgo) outputHashAlgo = s; + else if (i->name == state.sOutputHashMode) handleHashMode(s); + else if (i->name == state.sOutputs) + handleOutputs(tokenizeString<Strings>(s)); + } + + } + + } catch (Error & e) { + e.addPrefix(format("while evaluating the attribute '%1%' of the derivation '%2%' at %3%:\n") + % key % drvName % posDrvName); + throw; + } + } + + if (jsonObject) { + jsonObject.reset(); + drv.env.emplace("__json", jsonBuf.str()); + } + + /* Everything in the context of the strings in the derivation + attributes should be added as dependencies of the resulting + derivation. */ + for (auto & path : context) { + + /* Paths marked with `=' denote that the path of a derivation + is explicitly passed to the builder. Since that allows the + builder to gain access to every path in the dependency + graph of the derivation (including all outputs), all paths + in the graph must be added to this derivation's list of + inputs to ensure that they are available when the builder + runs. */ + if (path.at(0) == '=') { + /* !!! This doesn't work if readOnlyMode is set. */ + PathSet refs; + state.store->computeFSClosure(string(path, 1), refs); + for (auto & j : refs) { + drv.inputSrcs.insert(j); + if (isDerivation(j)) + drv.inputDrvs[j] = state.store->queryDerivationOutputNames(j); + } + } + + /* Handle derivation outputs of the form ‘!<name>!<path>’. */ + else if (path.at(0) == '!') { + std::pair<string, string> ctx = decodeContext(path); + drv.inputDrvs[ctx.first].insert(ctx.second); + } + + /* Otherwise it's a source file. */ + else + drv.inputSrcs.insert(path); + } + + /* Do we have all required attributes? */ + if (drv.builder == "") + throw EvalError(format("required attribute 'builder' missing, at %1%") % posDrvName); + if (drv.platform == "") + throw EvalError(format("required attribute 'system' missing, at %1%") % posDrvName); + + /* Check whether the derivation name is valid. */ + checkStoreName(drvName); + if (isDerivation(drvName)) + throw EvalError(format("derivation names are not allowed to end in '%1%', at %2%") + % drvExtension % posDrvName); + + if (outputHash) { + /* Handle fixed-output derivations. */ + if (outputs.size() != 1 || *(outputs.begin()) != "out") + throw Error(format("multiple outputs are not supported in fixed-output derivations, at %1%") % posDrvName); + + HashType ht = outputHashAlgo.empty() ? htUnknown : parseHashType(outputHashAlgo); + Hash h(*outputHash, ht); + + Path outPath = state.store->makeFixedOutputPath(outputHashRecursive, h, drvName); + if (!jsonObject) drv.env["out"] = outPath; + drv.outputs["out"] = DerivationOutput(outPath, + (outputHashRecursive ? "r:" : "") + printHashType(h.type), + h.to_string(Base16, false)); + } + + else { + /* Construct the "masked" store derivation, which is the final + one except that in the list of outputs, the output paths + are empty, and the corresponding environment variables have + an empty value. This ensures that changes in the set of + output names do get reflected in the hash. */ + for (auto & i : outputs) { + if (!jsonObject) drv.env[i] = ""; + drv.outputs[i] = DerivationOutput("", "", ""); + } + + /* Use the masked derivation expression to compute the output + path. */ + Hash h = hashDerivationModulo(*state.store, drv); + + for (auto & i : drv.outputs) + if (i.second.path == "") { + Path outPath = state.store->makeOutputPath(i.first, h, drvName); + if (!jsonObject) drv.env[i.first] = outPath; + i.second.path = outPath; + } + } + + /* Write the resulting term into the Nix store directory. */ + Path drvPath = writeDerivation(state.store, drv, drvName, state.repair); + + printMsg(lvlChatty, format("instantiated '%1%' -> '%2%'") + % drvName % drvPath); + + /* Optimisation, but required in read-only mode! because in that + case we don't actually write store derivations, so we can't + read them later. */ + drvHashes[drvPath] = hashDerivationModulo(*state.store, drv); + + state.mkAttrs(v, 1 + drv.outputs.size()); + mkString(*state.allocAttr(v, state.sDrvPath), drvPath, {"=" + drvPath}); + for (auto & i : drv.outputs) { + mkString(*state.allocAttr(v, state.symbols.create(i.first)), + i.second.path, {"!" + i.first + "!" + drvPath}); + } + v.attrs->sort(); +} + + +/* Return a placeholder string for the specified output that will be + substituted by the corresponding output path at build time. For + example, 'placeholder "out"' returns the string + /1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9. At build + time, any occurence of this string in an derivation attribute will + be replaced with the concrete path in the Nix store of the output + ‘out’. */ +static void prim_placeholder(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + mkString(v, hashPlaceholder(state.forceStringNoCtx(*args[0], pos))); +} + + +/************************************************************* + * Paths + *************************************************************/ + + +/* Convert the argument to a path. !!! obsolete? */ +static void prim_toPath(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + Path path = state.coerceToPath(pos, *args[0], context); + mkString(v, canonPath(path), context); +} + + +/* Allow a valid store path to be used in an expression. This is + useful in some generated expressions such as in nix-push, which + generates a call to a function with an already existing store path + as argument. You don't want to use `toPath' here because it copies + the path to the Nix store, which yields a copy like + /nix/store/newhash-oldhash-oldname. In the past, `toPath' had + special case behaviour for store paths, but that created weird + corner cases. */ +static void prim_storePath(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + Path path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context)); + /* Resolve symlinks in ‘path’, unless ‘path’ itself is a symlink + directly in the store. The latter condition is necessary so + e.g. nix-push does the right thing. */ + if (!state.store->isStorePath(path)) path = canonPath(path, true); + if (!state.store->isInStore(path)) + throw EvalError(format("path '%1%' is not in the Nix store, at %2%") % path % pos); + Path path2 = state.store->toStorePath(path); + if (!settings.readOnlyMode) + state.store->ensurePath(path2); + context.insert(path2); + mkString(v, path, context); +} + + +static void prim_pathExists(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + Path path = state.coerceToPath(pos, *args[0], context); + try { + state.realiseContext(context); + } catch (InvalidPathError & e) { + throw EvalError(format( + "cannot check the existence of '%1%', since path '%2%' is not valid, at %3%") + % path % e.path % pos); + } + + try { + mkBool(v, pathExists(state.checkSourcePath(path))); + } catch (SysError & e) { + /* Don't give away info from errors while canonicalising + ‘path’ in restricted mode. */ + mkBool(v, false); + } catch (RestrictedPathError & e) { + mkBool(v, false); + } +} + + +/* Return the base name of the given string, i.e., everything + following the last slash. */ +static void prim_baseNameOf(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + mkString(v, baseNameOf(state.coerceToString(pos, *args[0], context, false, false)), context); +} + + +/* Return the directory of the given path, i.e., everything before the + last slash. Return either a path or a string depending on the type + of the argument. */ +static void prim_dirOf(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + Path dir = dirOf(state.coerceToString(pos, *args[0], context, false, false)); + if (args[0]->type == tPath) mkPath(v, dir.c_str()); else mkString(v, dir, context); +} + + +/* Return the contents of a file as a string. */ +static void prim_readFile(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + Path path = state.coerceToPath(pos, *args[0], context); + try { + state.realiseContext(context); + } catch (InvalidPathError & e) { + throw EvalError(format("cannot read '%1%', since path '%2%' is not valid, at %3%") + % path % e.path % pos); + } + string s = readFile(state.checkSourcePath(state.toRealPath(path, context))); + if (s.find((char) 0) != string::npos) + throw Error(format("the contents of the file '%1%' cannot be represented as a Nix string") % path); + mkString(v, s.c_str()); +} + + +/* Find a file in the Nix search path. Used to implement <x> paths, + which are desugared to 'findFile __nixPath "x"'. */ +static void prim_findFile(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceList(*args[0], pos); + + SearchPath searchPath; + + for (unsigned int n = 0; n < args[0]->listSize(); ++n) { + Value & v2(*args[0]->listElems()[n]); + state.forceAttrs(v2, pos); + + string prefix; + Bindings::iterator i = v2.attrs->find(state.symbols.create("prefix")); + if (i != v2.attrs->end()) + prefix = state.forceStringNoCtx(*i->value, pos); + + i = v2.attrs->find(state.symbols.create("path")); + if (i == v2.attrs->end()) + throw EvalError(format("attribute 'path' missing, at %1%") % pos); + + PathSet context; + string path = state.coerceToString(pos, *i->value, context, false, false); + + try { + state.realiseContext(context); + } catch (InvalidPathError & e) { + throw EvalError(format("cannot find '%1%', since path '%2%' is not valid, at %3%") + % path % e.path % pos); + } + + searchPath.emplace_back(prefix, path); + } + + string path = state.forceStringNoCtx(*args[1], pos); + + mkPath(v, state.checkSourcePath(state.findFile(searchPath, path, pos)).c_str()); +} + +/* Return the cryptographic hash of a file in base-16. */ +static void prim_hashFile(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string type = state.forceStringNoCtx(*args[0], pos); + HashType ht = parseHashType(type); + if (ht == htUnknown) + throw Error(format("unknown hash type '%1%', at %2%") % type % pos); + + PathSet context; // discarded + Path p = state.coerceToPath(pos, *args[1], context); + + mkString(v, hashFile(ht, state.checkSourcePath(p)).to_string(Base16, false), context); +} + +/* Read a directory (without . or ..) */ +static void prim_readDir(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet ctx; + Path path = state.coerceToPath(pos, *args[0], ctx); + try { + state.realiseContext(ctx); + } catch (InvalidPathError & e) { + throw EvalError(format("cannot read '%1%', since path '%2%' is not valid, at %3%") + % path % e.path % pos); + } + + DirEntries entries = readDirectory(state.checkSourcePath(path)); + state.mkAttrs(v, entries.size()); + + for (auto & ent : entries) { + Value * ent_val = state.allocAttr(v, state.symbols.create(ent.name)); + if (ent.type == DT_UNKNOWN) + ent.type = getFileType(path + "/" + ent.name); + mkStringNoCopy(*ent_val, + ent.type == DT_REG ? "regular" : + ent.type == DT_DIR ? "directory" : + ent.type == DT_LNK ? "symlink" : + "unknown"); + } + + v.attrs->sort(); +} + + +/************************************************************* + * Creating files + *************************************************************/ + + +/* Convert the argument (which can be any Nix expression) to an XML + representation returned in a string. Not all Nix expressions can + be sensibly or completely represented (e.g., functions). */ +static void prim_toXML(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + std::ostringstream out; + PathSet context; + printValueAsXML(state, true, false, *args[0], out, context); + mkString(v, out.str(), context); +} + + +/* Convert the argument (which can be any Nix expression) to a JSON + string. Not all Nix expressions can be sensibly or completely + represented (e.g., functions). */ +static void prim_toJSON(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + std::ostringstream out; + PathSet context; + printValueAsJSON(state, true, *args[0], out, context); + mkString(v, out.str(), context); +} + + +/* Parse a JSON string to a value. */ +static void prim_fromJSON(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string s = state.forceStringNoCtx(*args[0], pos); + parseJSON(state, s, v); +} + + +/* Store a string in the Nix store as a source file that can be used + as an input by derivations. */ +static void prim_toFile(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + string name = state.forceStringNoCtx(*args[0], pos); + string contents = state.forceString(*args[1], context, pos); + + PathSet refs; + + for (auto path : context) { + if (path.at(0) != '/') + throw EvalError(format("in 'toFile': the file '%1%' cannot refer to derivation outputs, at %2%") % name % pos); + refs.insert(path); + } + + Path storePath = settings.readOnlyMode + ? state.store->computeStorePathForText(name, contents, refs) + : state.store->addTextToStore(name, contents, refs, state.repair); + + /* Note: we don't need to add `context' to the context of the + result, since `storePath' itself has references to the paths + used in args[1]. */ + + mkString(v, storePath, {storePath}); +} + + +static void addPath(EvalState & state, const Pos & pos, const string & name, const Path & path_, + Value * filterFun, bool recursive, const Hash & expectedHash, Value & v) +{ + const auto path = evalSettings.pureEval && expectedHash ? + path_ : + state.checkSourcePath(path_); + PathFilter filter = filterFun ? ([&](const Path & path) { + auto st = lstat(path); + + /* Call the filter function. The first argument is the path, + the second is a string indicating the type of the file. */ + Value arg1; + mkString(arg1, path); + + Value fun2; + state.callFunction(*filterFun, arg1, fun2, noPos); + + Value arg2; + mkString(arg2, + S_ISREG(st.st_mode) ? "regular" : + S_ISDIR(st.st_mode) ? "directory" : + S_ISLNK(st.st_mode) ? "symlink" : + "unknown" /* not supported, will fail! */); + + Value res; + state.callFunction(fun2, arg2, res, noPos); + + return state.forceBool(res, pos); + }) : defaultPathFilter; + + Path expectedStorePath; + if (expectedHash) { + expectedStorePath = + state.store->makeFixedOutputPath(recursive, expectedHash, name); + } + Path dstPath; + if (!expectedHash || !state.store->isValidPath(expectedStorePath)) { + dstPath = settings.readOnlyMode + ? state.store->computeStorePathForPath(name, path, recursive, htSHA256, filter).first + : state.store->addToStore(name, path, recursive, htSHA256, filter, state.repair); + if (expectedHash && expectedStorePath != dstPath) { + throw Error(format("store path mismatch in (possibly filtered) path added from '%1%'") % path); + } + } else + dstPath = expectedStorePath; + + mkString(v, dstPath, {dstPath}); +} + + +static void prim_filterSource(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + Path path = state.coerceToPath(pos, *args[1], context); + if (!context.empty()) + throw EvalError(format("string '%1%' cannot refer to other paths, at %2%") % path % pos); + + state.forceValue(*args[0]); + if (args[0]->type != tLambda) + throw TypeError(format("first argument in call to 'filterSource' is not a function but %1%, at %2%") % showType(*args[0]) % pos); + + addPath(state, pos, baseNameOf(path), path, args[0], true, Hash(), v); +} + +static void prim_path(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceAttrs(*args[0], pos); + Path path; + string name; + Value * filterFun = nullptr; + auto recursive = true; + Hash expectedHash; + + for (auto & attr : *args[0]->attrs) { + const string & n(attr.name); + if (n == "path") { + PathSet context; + path = state.coerceToPath(*attr.pos, *attr.value, context); + if (!context.empty()) + throw EvalError(format("string '%1%' cannot refer to other paths, at %2%") % path % *attr.pos); + } else if (attr.name == state.sName) + name = state.forceStringNoCtx(*attr.value, *attr.pos); + else if (n == "filter") { + state.forceValue(*attr.value); + filterFun = attr.value; + } else if (n == "recursive") + recursive = state.forceBool(*attr.value, *attr.pos); + else if (n == "sha256") + expectedHash = Hash(state.forceStringNoCtx(*attr.value, *attr.pos), htSHA256); + else + throw EvalError(format("unsupported argument '%1%' to 'addPath', at %2%") % attr.name % *attr.pos); + } + if (path.empty()) + throw EvalError(format("'path' required, at %1%") % pos); + if (name.empty()) + name = baseNameOf(path); + + addPath(state, pos, name, path, filterFun, recursive, expectedHash, v); +} + + +/************************************************************* + * Sets + *************************************************************/ + + +/* Return the names of the attributes in a set as a sorted list of + strings. */ +static void prim_attrNames(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceAttrs(*args[0], pos); + + state.mkList(v, args[0]->attrs->size()); + + size_t n = 0; + for (auto & i : *args[0]->attrs) + mkString(*(v.listElems()[n++] = state.allocValue()), i.name); + + std::sort(v.listElems(), v.listElems() + n, + [](Value * v1, Value * v2) { return strcmp(v1->string.s, v2->string.s) < 0; }); +} + + +/* Return the values of the attributes in a set as a list, in the same + order as attrNames. */ +static void prim_attrValues(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceAttrs(*args[0], pos); + + state.mkList(v, args[0]->attrs->size()); + + unsigned int n = 0; + for (auto & i : *args[0]->attrs) + v.listElems()[n++] = (Value *) &i; + + std::sort(v.listElems(), v.listElems() + n, + [](Value * v1, Value * v2) { return (string) ((Attr *) v1)->name < (string) ((Attr *) v2)->name; }); + + for (unsigned int i = 0; i < n; ++i) + v.listElems()[i] = ((Attr *) v.listElems()[i])->value; +} + + +/* Dynamic version of the `.' operator. */ +void prim_getAttr(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string attr = state.forceStringNoCtx(*args[0], pos); + state.forceAttrs(*args[1], pos); + // !!! Should we create a symbol here or just do a lookup? + Bindings::iterator i = args[1]->attrs->find(state.symbols.create(attr)); + if (i == args[1]->attrs->end()) + throw EvalError(format("attribute '%1%' missing, at %2%") % attr % pos); + // !!! add to stack trace? + if (state.countCalls && i->pos) state.attrSelects[*i->pos]++; + state.forceValue(*i->value); + v = *i->value; +} + + +/* Return position information of the specified attribute. */ +void prim_unsafeGetAttrPos(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string attr = state.forceStringNoCtx(*args[0], pos); + state.forceAttrs(*args[1], pos); + Bindings::iterator i = args[1]->attrs->find(state.symbols.create(attr)); + if (i == args[1]->attrs->end()) + mkNull(v); + else + state.mkPos(v, i->pos); +} + + +/* Dynamic version of the `?' operator. */ +static void prim_hasAttr(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string attr = state.forceStringNoCtx(*args[0], pos); + state.forceAttrs(*args[1], pos); + mkBool(v, args[1]->attrs->find(state.symbols.create(attr)) != args[1]->attrs->end()); +} + + +/* Determine whether the argument is a set. */ +static void prim_isAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + mkBool(v, args[0]->type == tAttrs); +} + + +static void prim_removeAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceAttrs(*args[0], pos); + state.forceList(*args[1], pos); + + /* Get the attribute names to be removed. */ + std::set<Symbol> names; + for (unsigned int i = 0; i < args[1]->listSize(); ++i) { + state.forceStringNoCtx(*args[1]->listElems()[i], pos); + names.insert(state.symbols.create(args[1]->listElems()[i]->string.s)); + } + + /* Copy all attributes not in that set. Note that we don't need + to sort v.attrs because it's a subset of an already sorted + vector. */ + state.mkAttrs(v, args[0]->attrs->size()); + for (auto & i : *args[0]->attrs) { + if (names.find(i.name) == names.end()) + v.attrs->push_back(i); + } +} + + +/* Builds a set from a list specifying (name, value) pairs. To be + precise, a list [{name = "name1"; value = value1;} ... {name = + "nameN"; value = valueN;}] is transformed to {name1 = value1; + ... nameN = valueN;}. In case of duplicate occurences of the same + name, the first takes precedence. */ +static void prim_listToAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceList(*args[0], pos); + + state.mkAttrs(v, args[0]->listSize()); + + std::set<Symbol> seen; + + for (unsigned int i = 0; i < args[0]->listSize(); ++i) { + Value & v2(*args[0]->listElems()[i]); + state.forceAttrs(v2, pos); + + Bindings::iterator j = v2.attrs->find(state.sName); + if (j == v2.attrs->end()) + throw TypeError(format("'name' attribute missing in a call to 'listToAttrs', at %1%") % pos); + string name = state.forceStringNoCtx(*j->value, pos); + + Symbol sym = state.symbols.create(name); + if (seen.find(sym) == seen.end()) { + Bindings::iterator j2 = v2.attrs->find(state.symbols.create(state.sValue)); + if (j2 == v2.attrs->end()) + throw TypeError(format("'value' attribute missing in a call to 'listToAttrs', at %1%") % pos); + + v.attrs->push_back(Attr(sym, j2->value, j2->pos)); + seen.insert(sym); + } + } + + v.attrs->sort(); +} + + +/* Return the right-biased intersection of two sets as1 and as2, + i.e. a set that contains every attribute from as2 that is also a + member of as1. */ +static void prim_intersectAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceAttrs(*args[0], pos); + state.forceAttrs(*args[1], pos); + + state.mkAttrs(v, std::min(args[0]->attrs->size(), args[1]->attrs->size())); + + for (auto & i : *args[0]->attrs) { + Bindings::iterator j = args[1]->attrs->find(i.name); + if (j != args[1]->attrs->end()) + v.attrs->push_back(*j); + } +} + + +/* Collect each attribute named `attr' from a list of attribute sets. + Sets that don't contain the named attribute are ignored. + + Example: + catAttrs "a" [{a = 1;} {b = 0;} {a = 2;}] + => [1 2] +*/ +static void prim_catAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + Symbol attrName = state.symbols.create(state.forceStringNoCtx(*args[0], pos)); + state.forceList(*args[1], pos); + + Value * res[args[1]->listSize()]; + unsigned int found = 0; + + for (unsigned int n = 0; n < args[1]->listSize(); ++n) { + Value & v2(*args[1]->listElems()[n]); + state.forceAttrs(v2, pos); + Bindings::iterator i = v2.attrs->find(attrName); + if (i != v2.attrs->end()) + res[found++] = i->value; + } + + state.mkList(v, found); + for (unsigned int n = 0; n < found; ++n) + v.listElems()[n] = res[n]; +} + + +/* Return a set containing the names of the formal arguments expected + by the function `f'. The value of each attribute is a Boolean + denoting whether the corresponding argument has a default value. For instance, + + functionArgs ({ x, y ? 123}: ...) + => { x = false; y = true; } + + "Formal argument" here refers to the attributes pattern-matched by + the function. Plain lambdas are not included, e.g. + + functionArgs (x: ...) + => { } +*/ +static void prim_functionArgs(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + if (args[0]->type != tLambda) + throw TypeError(format("'functionArgs' requires a function, at %1%") % pos); + + if (!args[0]->lambda.fun->matchAttrs) { + state.mkAttrs(v, 0); + return; + } + + state.mkAttrs(v, args[0]->lambda.fun->formals->formals.size()); + for (auto & i : args[0]->lambda.fun->formals->formals) + // !!! should optimise booleans (allocate only once) + mkBool(*state.allocAttr(v, i.name), i.def); + v.attrs->sort(); +} + + +/* Apply a function to every element of an attribute set. */ +static void prim_mapAttrs(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceAttrs(*args[1], pos); + + state.mkAttrs(v, args[1]->attrs->size()); + + for (auto & i : *args[1]->attrs) { + Value * vName = state.allocValue(); + Value * vFun2 = state.allocValue(); + mkString(*vName, i.name); + mkApp(*vFun2, *args[0], *vName); + mkApp(*state.allocAttr(v, i.name), *vFun2, *i.value); + } +} + + + +/************************************************************* + * Lists + *************************************************************/ + + +/* Determine whether the argument is a list. */ +static void prim_isList(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + mkBool(v, args[0]->isList()); +} + + +static void elemAt(EvalState & state, const Pos & pos, Value & list, int n, Value & v) +{ + state.forceList(list, pos); + if (n < 0 || (unsigned int) n >= list.listSize()) + throw Error(format("list index %1% is out of bounds, at %2%") % n % pos); + state.forceValue(*list.listElems()[n]); + v = *list.listElems()[n]; +} + + +/* Return the n-1'th element of a list. */ +static void prim_elemAt(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + elemAt(state, pos, *args[0], state.forceInt(*args[1], pos), v); +} + + +/* Return the first element of a list. */ +static void prim_head(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + elemAt(state, pos, *args[0], 0, v); +} + + +/* Return a list consisting of everything but the first element of + a list. Warning: this function takes O(n) time, so you probably + don't want to use it! */ +static void prim_tail(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceList(*args[0], pos); + if (args[0]->listSize() == 0) + throw Error(format("'tail' called on an empty list, at %1%") % pos); + state.mkList(v, args[0]->listSize() - 1); + for (unsigned int n = 0; n < v.listSize(); ++n) + v.listElems()[n] = args[0]->listElems()[n + 1]; +} + + +/* Apply a function to every element of a list. */ +static void prim_map(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceList(*args[1], pos); + + state.mkList(v, args[1]->listSize()); + + for (unsigned int n = 0; n < v.listSize(); ++n) + mkApp(*(v.listElems()[n] = state.allocValue()), + *args[0], *args[1]->listElems()[n]); +} + + +/* Filter a list using a predicate; that is, return a list containing + every element from the list for which the predicate function + returns true. */ +static void prim_filter(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); + + // FIXME: putting this on the stack is risky. + Value * vs[args[1]->listSize()]; + unsigned int k = 0; + + bool same = true; + for (unsigned int n = 0; n < args[1]->listSize(); ++n) { + Value res; + state.callFunction(*args[0], *args[1]->listElems()[n], res, noPos); + if (state.forceBool(res, pos)) + vs[k++] = args[1]->listElems()[n]; + else + same = false; + } + + if (same) + v = *args[1]; + else { + state.mkList(v, k); + for (unsigned int n = 0; n < k; ++n) v.listElems()[n] = vs[n]; + } +} + + +/* Return true if a list contains a given element. */ +static void prim_elem(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + bool res = false; + state.forceList(*args[1], pos); + for (unsigned int n = 0; n < args[1]->listSize(); ++n) + if (state.eqValues(*args[0], *args[1]->listElems()[n])) { + res = true; + break; + } + mkBool(v, res); +} + + +/* Concatenate a list of lists. */ +static void prim_concatLists(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceList(*args[0], pos); + state.concatLists(v, args[0]->listSize(), args[0]->listElems(), pos); +} + + +/* Return the length of a list. This is an O(1) time operation. */ +static void prim_length(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceList(*args[0], pos); + mkInt(v, args[0]->listSize()); +} + + +/* Reduce a list by applying a binary operator, from left to + right. The operator is applied strictly. */ +static void prim_foldlStrict(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceFunction(*args[0], pos); + state.forceList(*args[2], pos); + + if (args[2]->listSize()) { + Value * vCur = args[1]; + + for (unsigned int n = 0; n < args[2]->listSize(); ++n) { + Value vTmp; + state.callFunction(*args[0], *vCur, vTmp, pos); + vCur = n == args[2]->listSize() - 1 ? &v : state.allocValue(); + state.callFunction(vTmp, *args[2]->listElems()[n], *vCur, pos); + } + state.forceValue(v); + } else { + state.forceValue(*args[1]); + v = *args[1]; + } +} + + +static void anyOrAll(bool any, EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); + + Value vTmp; + for (unsigned int n = 0; n < args[1]->listSize(); ++n) { + state.callFunction(*args[0], *args[1]->listElems()[n], vTmp, pos); + bool res = state.forceBool(vTmp, pos); + if (res == any) { + mkBool(v, any); + return; + } + } + + mkBool(v, !any); +} + + +static void prim_any(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + anyOrAll(true, state, pos, args, v); +} + + +static void prim_all(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + anyOrAll(false, state, pos, args, v); +} + + +static void prim_genList(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + auto len = state.forceInt(*args[1], pos); + + if (len < 0) + throw EvalError(format("cannot create list of size %1%, at %2%") % len % pos); + + state.mkList(v, len); + + for (unsigned int n = 0; n < (unsigned int) len; ++n) { + Value * arg = state.allocValue(); + mkInt(*arg, n); + mkApp(*(v.listElems()[n] = state.allocValue()), *args[0], *arg); + } +} + + +static void prim_lessThan(EvalState & state, const Pos & pos, Value * * args, Value & v); + + +static void prim_sort(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); + + auto len = args[1]->listSize(); + state.mkList(v, len); + for (unsigned int n = 0; n < len; ++n) { + state.forceValue(*args[1]->listElems()[n]); + v.listElems()[n] = args[1]->listElems()[n]; + } + + + auto comparator = [&](Value * a, Value * b) { + /* Optimization: if the comparator is lessThan, bypass + callFunction. */ + if (args[0]->type == tPrimOp && args[0]->primOp->fun == prim_lessThan) + return CompareValues()(a, b); + + Value vTmp1, vTmp2; + state.callFunction(*args[0], *a, vTmp1, pos); + state.callFunction(vTmp1, *b, vTmp2, pos); + return state.forceBool(vTmp2, pos); + }; + + /* FIXME: std::sort can segfault if the comparator is not a strict + weak ordering. What to do? std::stable_sort() seems more + resilient, but no guarantees... */ + std::stable_sort(v.listElems(), v.listElems() + len, comparator); +} + + +static void prim_partition(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); + + auto len = args[1]->listSize(); + + ValueVector right, wrong; + + for (unsigned int n = 0; n < len; ++n) { + auto vElem = args[1]->listElems()[n]; + state.forceValue(*vElem); + Value res; + state.callFunction(*args[0], *vElem, res, pos); + if (state.forceBool(res, pos)) + right.push_back(vElem); + else + wrong.push_back(vElem); + } + + state.mkAttrs(v, 2); + + Value * vRight = state.allocAttr(v, state.sRight); + auto rsize = right.size(); + state.mkList(*vRight, rsize); + if (rsize) + memcpy(vRight->listElems(), right.data(), sizeof(Value *) * rsize); + + Value * vWrong = state.allocAttr(v, state.sWrong); + auto wsize = wrong.size(); + state.mkList(*vWrong, wsize); + if (wsize) + memcpy(vWrong->listElems(), wrong.data(), sizeof(Value *) * wsize); + + v.attrs->sort(); +} + + +/* concatMap = f: list: concatLists (map f list); */ +/* C++-version is to avoid allocating `mkApp', call `f' eagerly */ +static void prim_concatMap(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceFunction(*args[0], pos); + state.forceList(*args[1], pos); + auto nrLists = args[1]->listSize(); + + Value lists[nrLists]; + size_t len = 0; + + for (unsigned int n = 0; n < nrLists; ++n) { + Value * vElem = args[1]->listElems()[n]; + state.callFunction(*args[0], *vElem, lists[n], pos); + state.forceList(lists[n], pos); + len += lists[n].listSize(); + } + + state.mkList(v, len); + auto out = v.listElems(); + for (unsigned int n = 0, pos = 0; n < nrLists; ++n) { + auto l = lists[n].listSize(); + if (l) + memcpy(out + pos, lists[n].listElems(), l * sizeof(Value *)); + pos += l; + } +} + + +/************************************************************* + * Integer arithmetic + *************************************************************/ + + +static void prim_add(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0], pos); + state.forceValue(*args[1], pos); + if (args[0]->type == tFloat || args[1]->type == tFloat) + mkFloat(v, state.forceFloat(*args[0], pos) + state.forceFloat(*args[1], pos)); + else + mkInt(v, state.forceInt(*args[0], pos) + state.forceInt(*args[1], pos)); +} + + +static void prim_sub(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0], pos); + state.forceValue(*args[1], pos); + if (args[0]->type == tFloat || args[1]->type == tFloat) + mkFloat(v, state.forceFloat(*args[0], pos) - state.forceFloat(*args[1], pos)); + else + mkInt(v, state.forceInt(*args[0], pos) - state.forceInt(*args[1], pos)); +} + + +static void prim_mul(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0], pos); + state.forceValue(*args[1], pos); + if (args[0]->type == tFloat || args[1]->type == tFloat) + mkFloat(v, state.forceFloat(*args[0], pos) * state.forceFloat(*args[1], pos)); + else + mkInt(v, state.forceInt(*args[0], pos) * state.forceInt(*args[1], pos)); +} + + +static void prim_div(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0], pos); + state.forceValue(*args[1], pos); + + NixFloat f2 = state.forceFloat(*args[1], pos); + if (f2 == 0) throw EvalError(format("division by zero, at %1%") % pos); + + if (args[0]->type == tFloat || args[1]->type == tFloat) { + mkFloat(v, state.forceFloat(*args[0], pos) / state.forceFloat(*args[1], pos)); + } else { + NixInt i1 = state.forceInt(*args[0], pos); + NixInt i2 = state.forceInt(*args[1], pos); + /* Avoid division overflow as it might raise SIGFPE. */ + if (i1 == std::numeric_limits<NixInt>::min() && i2 == -1) + throw EvalError(format("overflow in integer division, at %1%") % pos); + mkInt(v, i1 / i2); + } +} + +static void prim_bitAnd(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + mkInt(v, state.forceInt(*args[0], pos) & state.forceInt(*args[1], pos)); +} + +static void prim_bitOr(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + mkInt(v, state.forceInt(*args[0], pos) | state.forceInt(*args[1], pos)); +} + +static void prim_bitXor(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + mkInt(v, state.forceInt(*args[0], pos) ^ state.forceInt(*args[1], pos)); +} + +static void prim_lessThan(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceValue(*args[0]); + state.forceValue(*args[1]); + CompareValues comp; + mkBool(v, comp(args[0], args[1])); +} + + +/************************************************************* + * String manipulation + *************************************************************/ + + +/* Convert the argument to a string. Paths are *not* copied to the + store, so `toString /foo/bar' yields `"/foo/bar"', not + `"/nix/store/whatever..."'. */ +static void prim_toString(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + string s = state.coerceToString(pos, *args[0], context, true, false); + mkString(v, s, context); +} + + +/* `substring start len str' returns the substring of `str' starting + at character position `min(start, stringLength str)' inclusive and + ending at `min(start + len, stringLength str)'. `start' must be + non-negative. */ +static void prim_substring(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + int start = state.forceInt(*args[0], pos); + int len = state.forceInt(*args[1], pos); + PathSet context; + string s = state.coerceToString(pos, *args[2], context); + + if (start < 0) throw EvalError(format("negative start position in 'substring', at %1%") % pos); + + mkString(v, (unsigned int) start >= s.size() ? "" : string(s, start, len), context); +} + + +static void prim_stringLength(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + string s = state.coerceToString(pos, *args[0], context); + mkInt(v, s.size()); +} + + +/* Return the cryptographic hash of a string in base-16. */ +static void prim_hashString(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string type = state.forceStringNoCtx(*args[0], pos); + HashType ht = parseHashType(type); + if (ht == htUnknown) + throw Error(format("unknown hash type '%1%', at %2%") % type % pos); + + PathSet context; // discarded + string s = state.forceString(*args[1], context, pos); + + mkString(v, hashString(ht, s).to_string(Base16, false), context); +} + + +/* Match a regular expression against a string and return either + ‘null’ or a list containing substring matches. */ +static void prim_match(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + auto re = state.forceStringNoCtx(*args[0], pos); + + try { + + std::regex regex(re, std::regex::extended); + + PathSet context; + const std::string str = state.forceString(*args[1], context, pos); + + std::smatch match; + if (!std::regex_match(str, match, regex)) { + mkNull(v); + return; + } + + // the first match is the whole string + const size_t len = match.size() - 1; + state.mkList(v, len); + for (size_t i = 0; i < len; ++i) { + if (!match[i+1].matched) + mkNull(*(v.listElems()[i] = state.allocValue())); + else + mkString(*(v.listElems()[i] = state.allocValue()), match[i + 1].str().c_str()); + } + + } catch (std::regex_error &e) { + if (e.code() == std::regex_constants::error_space) { + // limit is _GLIBCXX_REGEX_STATE_LIMIT for libstdc++ + throw EvalError("memory limit exceeded by regular expression '%s', at %s", re, pos); + } else { + throw EvalError("invalid regular expression '%s', at %s", re, pos); + } + } +} + + +/* Split a string with a regular expression, and return a list of the + non-matching parts interleaved by the lists of the matching groups. */ +static void prim_split(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + auto re = state.forceStringNoCtx(*args[0], pos); + + try { + + std::regex regex(re, std::regex::extended); + + PathSet context; + const std::string str = state.forceString(*args[1], context, pos); + + auto begin = std::sregex_iterator(str.begin(), str.end(), regex); + auto end = std::sregex_iterator(); + + // Any matches results are surrounded by non-matching results. + const size_t len = std::distance(begin, end); + state.mkList(v, 2 * len + 1); + size_t idx = 0; + Value * elem; + + if (len == 0) { + v.listElems()[idx++] = args[1]; + return; + } + + for (std::sregex_iterator i = begin; i != end; ++i) { + assert(idx <= 2 * len + 1 - 3); + std::smatch match = *i; + + // Add a string for non-matched characters. + elem = v.listElems()[idx++] = state.allocValue(); + mkString(*elem, match.prefix().str().c_str()); + + // Add a list for matched substrings. + const size_t slen = match.size() - 1; + elem = v.listElems()[idx++] = state.allocValue(); + + // Start at 1, beacause the first match is the whole string. + state.mkList(*elem, slen); + for (size_t si = 0; si < slen; ++si) { + if (!match[si + 1].matched) + mkNull(*(elem->listElems()[si] = state.allocValue())); + else + mkString(*(elem->listElems()[si] = state.allocValue()), match[si + 1].str().c_str()); + } + + // Add a string for non-matched suffix characters. + if (idx == 2 * len) { + elem = v.listElems()[idx++] = state.allocValue(); + mkString(*elem, match.suffix().str().c_str()); + } + } + assert(idx == 2 * len + 1); + + } catch (std::regex_error &e) { + if (e.code() == std::regex_constants::error_space) { + // limit is _GLIBCXX_REGEX_STATE_LIMIT for libstdc++ + throw EvalError("memory limit exceeded by regular expression '%s', at %s", re, pos); + } else { + throw EvalError("invalid regular expression '%s', at %s", re, pos); + } + } +} + + +static void prim_concatStringSep(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + + auto sep = state.forceString(*args[0], context, pos); + state.forceList(*args[1], pos); + + string res; + res.reserve((args[1]->listSize() + 32) * sep.size()); + bool first = true; + + for (unsigned int n = 0; n < args[1]->listSize(); ++n) { + if (first) first = false; else res += sep; + res += state.coerceToString(pos, *args[1]->listElems()[n], context); + } + + mkString(v, res, context); +} + + +static void prim_replaceStrings(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + state.forceList(*args[0], pos); + state.forceList(*args[1], pos); + if (args[0]->listSize() != args[1]->listSize()) + throw EvalError(format("'from' and 'to' arguments to 'replaceStrings' have different lengths, at %1%") % pos); + + vector<string> from; + from.reserve(args[0]->listSize()); + for (unsigned int n = 0; n < args[0]->listSize(); ++n) + from.push_back(state.forceString(*args[0]->listElems()[n], pos)); + + vector<std::pair<string, PathSet>> to; + to.reserve(args[1]->listSize()); + for (unsigned int n = 0; n < args[1]->listSize(); ++n) { + PathSet ctx; + auto s = state.forceString(*args[1]->listElems()[n], ctx, pos); + to.push_back(std::make_pair(std::move(s), std::move(ctx))); + } + + PathSet context; + auto s = state.forceString(*args[2], context, pos); + + string res; + // Loops one past last character to handle the case where 'from' contains an empty string. + for (size_t p = 0; p <= s.size(); ) { + bool found = false; + auto i = from.begin(); + auto j = to.begin(); + for (; i != from.end(); ++i, ++j) + if (s.compare(p, i->size(), *i) == 0) { + found = true; + res += j->first; + if (i->empty()) { + if (p < s.size()) + res += s[p]; + p++; + } else { + p += i->size(); + } + for (auto& path : j->second) + context.insert(path); + j->second.clear(); + break; + } + if (!found) { + if (p < s.size()) + res += s[p]; + p++; + } + } + + mkString(v, res, context); +} + + +/************************************************************* + * Versions + *************************************************************/ + + +static void prim_parseDrvName(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string name = state.forceStringNoCtx(*args[0], pos); + DrvName parsed(name); + state.mkAttrs(v, 2); + mkString(*state.allocAttr(v, state.sName), parsed.name); + mkString(*state.allocAttr(v, state.symbols.create("version")), parsed.version); + v.attrs->sort(); +} + + +static void prim_compareVersions(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string version1 = state.forceStringNoCtx(*args[0], pos); + string version2 = state.forceStringNoCtx(*args[1], pos); + mkInt(v, compareVersions(version1, version2)); +} + + +static void prim_splitVersion(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + string version = state.forceStringNoCtx(*args[0], pos); + auto iter = version.cbegin(); + Strings components; + while (iter != version.cend()) { + auto component = nextComponent(iter, version.cend()); + if (component.empty()) + break; + components.emplace_back(std::move(component)); + } + state.mkList(v, components.size()); + unsigned int n = 0; + for (auto & component : components) { + auto listElem = v.listElems()[n++] = state.allocValue(); + mkString(*listElem, std::move(component)); + } +} + + +/************************************************************* + * Networking + *************************************************************/ + + +void fetch(EvalState & state, const Pos & pos, Value * * args, Value & v, + const string & who, bool unpack, const std::string & defaultName) +{ + CachedDownloadRequest request(""); + request.unpack = unpack; + request.name = defaultName; + + state.forceValue(*args[0]); + + if (args[0]->type == tAttrs) { + + state.forceAttrs(*args[0], pos); + + for (auto & attr : *args[0]->attrs) { + string n(attr.name); + if (n == "url") + request.uri = state.forceStringNoCtx(*attr.value, *attr.pos); + else if (n == "sha256") + request.expectedHash = Hash(state.forceStringNoCtx(*attr.value, *attr.pos), htSHA256); + else if (n == "name") + request.name = state.forceStringNoCtx(*attr.value, *attr.pos); + else + throw EvalError(format("unsupported argument '%1%' to '%2%', at %3%") % attr.name % who % attr.pos); + } + + if (request.uri.empty()) + throw EvalError(format("'url' argument required, at %1%") % pos); + + } else + request.uri = state.forceStringNoCtx(*args[0], pos); + + state.checkURI(request.uri); + + if (evalSettings.pureEval && !request.expectedHash) + throw Error("in pure evaluation mode, '%s' requires a 'sha256' argument", who); + + auto res = getDownloader()->downloadCached(state.store, request); + + if (state.allowedPaths) + state.allowedPaths->insert(res.path); + + mkString(v, res.storePath, PathSet({res.storePath})); +} + + +static void prim_fetchurl(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + fetch(state, pos, args, v, "fetchurl", false, ""); +} + + +static void prim_fetchTarball(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + fetch(state, pos, args, v, "fetchTarball", true, "source"); +} + + +/************************************************************* + * Primop registration + *************************************************************/ + + +RegisterPrimOp::PrimOps * RegisterPrimOp::primOps; + + +RegisterPrimOp::RegisterPrimOp(std::string name, size_t arity, PrimOpFun fun) +{ + if (!primOps) primOps = new PrimOps; + primOps->emplace_back(name, arity, fun); +} + + +void EvalState::createBaseEnv() +{ + baseEnv.up = 0; + + /* Add global constants such as `true' to the base environment. */ + Value v; + + /* `builtins' must be first! */ + mkAttrs(v, 128); + addConstant("builtins", v); + + mkBool(v, true); + addConstant("true", v); + + mkBool(v, false); + addConstant("false", v); + + mkNull(v); + addConstant("null", v); + + auto vThrow = addPrimOp("throw", 1, prim_throw); + + auto addPurityError = [&](const std::string & name) { + Value * v2 = allocValue(); + mkString(*v2, fmt("'%s' is not allowed in pure evaluation mode", name)); + mkApp(v, *vThrow, *v2); + addConstant(name, v); + }; + + if (!evalSettings.pureEval) { + mkInt(v, time(0)); + addConstant("__currentTime", v); + } + + if (!evalSettings.pureEval) { + mkString(v, settings.thisSystem); + addConstant("__currentSystem", v); + } + + mkString(v, nixVersion); + addConstant("__nixVersion", v); + + mkString(v, store->storeDir); + addConstant("__storeDir", v); + + /* Language version. This should be increased every time a new + language feature gets added. It's not necessary to increase it + when primops get added, because you can just use `builtins ? + primOp' to check. */ + mkInt(v, 5); + addConstant("__langVersion", v); + + // Miscellaneous + auto vScopedImport = addPrimOp("scopedImport", 2, prim_scopedImport); + Value * v2 = allocValue(); + mkAttrs(*v2, 0); + mkApp(v, *vScopedImport, *v2); + forceValue(v); + addConstant("import", v); + if (evalSettings.enableNativeCode) { + addPrimOp("__importNative", 2, prim_importNative); + addPrimOp("__exec", 1, prim_exec); + } + addPrimOp("__typeOf", 1, prim_typeOf); + addPrimOp("isNull", 1, prim_isNull); + addPrimOp("__isFunction", 1, prim_isFunction); + addPrimOp("__isString", 1, prim_isString); + addPrimOp("__isInt", 1, prim_isInt); + addPrimOp("__isFloat", 1, prim_isFloat); + addPrimOp("__isBool", 1, prim_isBool); + addPrimOp("__isPath", 1, prim_isPath); + addPrimOp("__genericClosure", 1, prim_genericClosure); + addPrimOp("abort", 1, prim_abort); + addPrimOp("__addErrorContext", 2, prim_addErrorContext); + addPrimOp("__tryEval", 1, prim_tryEval); + addPrimOp("__getEnv", 1, prim_getEnv); + + // Strictness + addPrimOp("__seq", 2, prim_seq); + addPrimOp("__deepSeq", 2, prim_deepSeq); + + // Debugging + addPrimOp("__trace", 2, prim_trace); + addPrimOp("__valueSize", 1, prim_valueSize); + + // Paths + addPrimOp("__toPath", 1, prim_toPath); + if (evalSettings.pureEval) + addPurityError("__storePath"); + else + addPrimOp("__storePath", 1, prim_storePath); + addPrimOp("__pathExists", 1, prim_pathExists); + addPrimOp("baseNameOf", 1, prim_baseNameOf); + addPrimOp("dirOf", 1, prim_dirOf); + addPrimOp("__readFile", 1, prim_readFile); + addPrimOp("__readDir", 1, prim_readDir); + addPrimOp("__findFile", 2, prim_findFile); + addPrimOp("__hashFile", 2, prim_hashFile); + + // Creating files + addPrimOp("__toXML", 1, prim_toXML); + addPrimOp("__toJSON", 1, prim_toJSON); + addPrimOp("__fromJSON", 1, prim_fromJSON); + addPrimOp("__toFile", 2, prim_toFile); + addPrimOp("__filterSource", 2, prim_filterSource); + addPrimOp("__path", 1, prim_path); + + // Sets + addPrimOp("__attrNames", 1, prim_attrNames); + addPrimOp("__attrValues", 1, prim_attrValues); + addPrimOp("__getAttr", 2, prim_getAttr); + addPrimOp("__unsafeGetAttrPos", 2, prim_unsafeGetAttrPos); + addPrimOp("__hasAttr", 2, prim_hasAttr); + addPrimOp("__isAttrs", 1, prim_isAttrs); + addPrimOp("removeAttrs", 2, prim_removeAttrs); + addPrimOp("__listToAttrs", 1, prim_listToAttrs); + addPrimOp("__intersectAttrs", 2, prim_intersectAttrs); + addPrimOp("__catAttrs", 2, prim_catAttrs); + addPrimOp("__functionArgs", 1, prim_functionArgs); + addPrimOp("__mapAttrs", 2, prim_mapAttrs); + + // Lists + addPrimOp("__isList", 1, prim_isList); + addPrimOp("__elemAt", 2, prim_elemAt); + addPrimOp("__head", 1, prim_head); + addPrimOp("__tail", 1, prim_tail); + addPrimOp("map", 2, prim_map); + addPrimOp("__filter", 2, prim_filter); + addPrimOp("__elem", 2, prim_elem); + addPrimOp("__concatLists", 1, prim_concatLists); + addPrimOp("__length", 1, prim_length); + addPrimOp("__foldl'", 3, prim_foldlStrict); + addPrimOp("__any", 2, prim_any); + addPrimOp("__all", 2, prim_all); + addPrimOp("__genList", 2, prim_genList); + addPrimOp("__sort", 2, prim_sort); + addPrimOp("__partition", 2, prim_partition); + addPrimOp("__concatMap", 2, prim_concatMap); + + // Integer arithmetic + addPrimOp("__add", 2, prim_add); + addPrimOp("__sub", 2, prim_sub); + addPrimOp("__mul", 2, prim_mul); + addPrimOp("__div", 2, prim_div); + addPrimOp("__bitAnd", 2, prim_bitAnd); + addPrimOp("__bitOr", 2, prim_bitOr); + addPrimOp("__bitXor", 2, prim_bitXor); + addPrimOp("__lessThan", 2, prim_lessThan); + + // String manipulation + addPrimOp("toString", 1, prim_toString); + addPrimOp("__substring", 3, prim_substring); + addPrimOp("__stringLength", 1, prim_stringLength); + addPrimOp("__hashString", 2, prim_hashString); + addPrimOp("__match", 2, prim_match); + addPrimOp("__split", 2, prim_split); + addPrimOp("__concatStringsSep", 2, prim_concatStringSep); + addPrimOp("__replaceStrings", 3, prim_replaceStrings); + + // Versions + addPrimOp("__parseDrvName", 1, prim_parseDrvName); + addPrimOp("__compareVersions", 2, prim_compareVersions); + addPrimOp("__splitVersion", 1, prim_splitVersion); + + // Derivations + addPrimOp("derivationStrict", 1, prim_derivationStrict); + addPrimOp("placeholder", 1, prim_placeholder); + + // Networking + addPrimOp("__fetchurl", 1, prim_fetchurl); + addPrimOp("fetchTarball", 1, prim_fetchTarball); + + /* Add a wrapper around the derivation primop that computes the + `drvPath' and `outPath' attributes lazily. */ + string path = canonPath(settings.nixDataDir + "/nix/corepkgs/derivation.nix", true); + sDerivationNix = symbols.create(path); + evalFile(path, v); + addConstant("derivation", v); + + /* Add a value containing the current Nix expression search path. */ + mkList(v, searchPath.size()); + int n = 0; + for (auto & i : searchPath) { + v2 = v.listElems()[n++] = allocValue(); + mkAttrs(*v2, 2); + mkString(*allocAttr(*v2, symbols.create("path")), i.second); + mkString(*allocAttr(*v2, symbols.create("prefix")), i.first); + v2->attrs->sort(); + } + addConstant("__nixPath", v); + + if (RegisterPrimOp::primOps) + for (auto & primOp : *RegisterPrimOp::primOps) + addPrimOp(std::get<0>(primOp), std::get<1>(primOp), std::get<2>(primOp)); + + /* Now that we've added all primops, sort the `builtins' set, + because attribute lookups expect it to be sorted. */ + baseEnv.values[0]->attrs->sort(); +} + + +} diff --git a/third_party/nix/src/libexpr/primops.hh b/third_party/nix/src/libexpr/primops.hh new file mode 100644 index 000000000000..c790b30f6d0b --- /dev/null +++ b/third_party/nix/src/libexpr/primops.hh @@ -0,0 +1,26 @@ +#include "eval.hh" + +#include <tuple> +#include <vector> + +namespace nix { + +struct RegisterPrimOp +{ + typedef std::vector<std::tuple<std::string, size_t, PrimOpFun>> PrimOps; + static PrimOps * primOps; + /* You can register a constant by passing an arity of 0. fun + will get called during EvalState initialization, so there + may be primops not yet added and builtins is not yet sorted. */ + RegisterPrimOp(std::string name, size_t arity, PrimOpFun fun); +}; + +/* These primops are disabled without enableNativeCode, but plugins + may wish to use them in limited contexts without globally enabling + them. */ +/* Load a ValueInitializer from a DSO and return whatever it initializes */ +void prim_importNative(EvalState & state, const Pos & pos, Value * * args, Value & v); +/* Execute a program and parse its output */ +void prim_exec(EvalState & state, const Pos & pos, Value * * args, Value & v); + +} diff --git a/third_party/nix/src/libexpr/primops/context.cc b/third_party/nix/src/libexpr/primops/context.cc new file mode 100644 index 000000000000..2d79739ea047 --- /dev/null +++ b/third_party/nix/src/libexpr/primops/context.cc @@ -0,0 +1,187 @@ +#include "primops.hh" +#include "eval-inline.hh" +#include "derivations.hh" + +namespace nix { + +static void prim_unsafeDiscardStringContext(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + string s = state.coerceToString(pos, *args[0], context); + mkString(v, s, PathSet()); +} + +static RegisterPrimOp r1("__unsafeDiscardStringContext", 1, prim_unsafeDiscardStringContext); + + +static void prim_hasContext(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + state.forceString(*args[0], context, pos); + mkBool(v, !context.empty()); +} + +static RegisterPrimOp r2("__hasContext", 1, prim_hasContext); + + +/* Sometimes we want to pass a derivation path (i.e. pkg.drvPath) to a + builder without causing the derivation to be built (for instance, + in the derivation that builds NARs in nix-push, when doing + source-only deployment). This primop marks the string context so + that builtins.derivation adds the path to drv.inputSrcs rather than + drv.inputDrvs. */ +static void prim_unsafeDiscardOutputDependency(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + string s = state.coerceToString(pos, *args[0], context); + + PathSet context2; + for (auto & p : context) + context2.insert(p.at(0) == '=' ? string(p, 1) : p); + + mkString(v, s, context2); +} + +static RegisterPrimOp r3("__unsafeDiscardOutputDependency", 1, prim_unsafeDiscardOutputDependency); + + +/* Extract the context of a string as a structured Nix value. + + The context is represented as an attribute set whose keys are the + paths in the context set and whose values are attribute sets with + the following keys: + path: True if the relevant path is in the context as a plain store + path (i.e. the kind of context you get when interpolating + a Nix path (e.g. ./.) into a string). False if missing. + allOutputs: True if the relevant path is a derivation and it is + in the context as a drv file with all of its outputs + (i.e. the kind of context you get when referencing + .drvPath of some derivation). False if missing. + outputs: If a non-empty list, the relevant path is a derivation + and the provided outputs are referenced in the context + (i.e. the kind of context you get when referencing + .outPath of some derivation). Empty list if missing. + Note that for a given path any combination of the above attributes + may be present. +*/ +static void prim_getContext(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + struct ContextInfo { + bool path = false; + bool allOutputs = false; + Strings outputs; + }; + PathSet context; + state.forceString(*args[0], context, pos); + auto contextInfos = std::map<Path, ContextInfo>(); + for (const auto & p : context) { + Path drv; + string output; + const Path * path = &p; + if (p.at(0) == '=') { + drv = string(p, 1); + path = &drv; + } else if (p.at(0) == '!') { + std::pair<string, string> ctx = decodeContext(p); + drv = ctx.first; + output = ctx.second; + path = &drv; + } + auto isPath = drv.empty(); + auto isAllOutputs = (!drv.empty()) && output.empty(); + + auto iter = contextInfos.find(*path); + if (iter == contextInfos.end()) { + contextInfos.emplace(*path, ContextInfo{isPath, isAllOutputs, output.empty() ? Strings{} : Strings{std::move(output)}}); + } else { + if (isPath) + iter->second.path = true; + else if (isAllOutputs) + iter->second.allOutputs = true; + else + iter->second.outputs.emplace_back(std::move(output)); + } + } + + state.mkAttrs(v, contextInfos.size()); + + auto sPath = state.symbols.create("path"); + auto sAllOutputs = state.symbols.create("allOutputs"); + for (const auto & info : contextInfos) { + auto & infoVal = *state.allocAttr(v, state.symbols.create(info.first)); + state.mkAttrs(infoVal, 3); + if (info.second.path) + mkBool(*state.allocAttr(infoVal, sPath), true); + if (info.second.allOutputs) + mkBool(*state.allocAttr(infoVal, sAllOutputs), true); + if (!info.second.outputs.empty()) { + auto & outputsVal = *state.allocAttr(infoVal, state.sOutputs); + state.mkList(outputsVal, info.second.outputs.size()); + size_t i = 0; + for (const auto & output : info.second.outputs) { + mkString(*(outputsVal.listElems()[i++] = state.allocValue()), output); + } + } + infoVal.attrs->sort(); + } + v.attrs->sort(); +} + +static RegisterPrimOp r4("__getContext", 1, prim_getContext); + + +/* Append the given context to a given string. + + See the commentary above unsafeGetContext for details of the + context representation. +*/ +static void prim_appendContext(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + PathSet context; + auto orig = state.forceString(*args[0], context, pos); + + state.forceAttrs(*args[1], pos); + + auto sPath = state.symbols.create("path"); + auto sAllOutputs = state.symbols.create("allOutputs"); + for (auto & i : *args[1]->attrs) { + if (!state.store->isStorePath(i.name)) + throw EvalError("Context key '%s' is not a store path, at %s", i.name, i.pos); + if (!settings.readOnlyMode) + state.store->ensurePath(i.name); + state.forceAttrs(*i.value, *i.pos); + auto iter = i.value->attrs->find(sPath); + if (iter != i.value->attrs->end()) { + if (state.forceBool(*iter->value, *iter->pos)) + context.insert(i.name); + } + + iter = i.value->attrs->find(sAllOutputs); + if (iter != i.value->attrs->end()) { + if (state.forceBool(*iter->value, *iter->pos)) { + if (!isDerivation(i.name)) { + throw EvalError("Tried to add all-outputs context of %s, which is not a derivation, to a string, at %s", i.name, i.pos); + } + context.insert("=" + string(i.name)); + } + } + + iter = i.value->attrs->find(state.sOutputs); + if (iter != i.value->attrs->end()) { + state.forceList(*iter->value, *iter->pos); + if (iter->value->listSize() && !isDerivation(i.name)) { + throw EvalError("Tried to add derivation output context of %s, which is not a derivation, to a string, at %s", i.name, i.pos); + } + for (unsigned int n = 0; n < iter->value->listSize(); ++n) { + auto name = state.forceStringNoCtx(*iter->value->listElems()[n], *iter->pos); + context.insert("!" + name + "!" + string(i.name)); + } + } + } + + mkString(v, orig, context); +} + +static RegisterPrimOp r5("__appendContext", 2, prim_appendContext); + +} diff --git a/third_party/nix/src/libexpr/primops/fetchGit.cc b/third_party/nix/src/libexpr/primops/fetchGit.cc new file mode 100644 index 000000000000..90f600284b22 --- /dev/null +++ b/third_party/nix/src/libexpr/primops/fetchGit.cc @@ -0,0 +1,247 @@ +#include "primops.hh" +#include "eval-inline.hh" +#include "download.hh" +#include "store-api.hh" +#include "pathlocks.hh" +#include "hash.hh" + +#include <sys/time.h> + +#include <regex> + +#include <nlohmann/json.hpp> + +using namespace std::string_literals; + +namespace nix { + +struct GitInfo +{ + Path storePath; + std::string rev; + std::string shortRev; + uint64_t revCount = 0; +}; + +std::regex revRegex("^[0-9a-fA-F]{40}$"); + +GitInfo exportGit(ref<Store> store, const std::string & uri, + std::optional<std::string> ref, std::string rev, + const std::string & name) +{ + if (evalSettings.pureEval && rev == "") + throw Error("in pure evaluation mode, 'fetchGit' requires a Git revision"); + + if (!ref && rev == "" && hasPrefix(uri, "/") && pathExists(uri + "/.git")) { + + bool clean = true; + + try { + runProgram("git", true, { "-C", uri, "diff-index", "--quiet", "HEAD", "--" }); + } catch (ExecError & e) { + if (!WIFEXITED(e.status) || WEXITSTATUS(e.status) != 1) throw; + clean = false; + } + + if (!clean) { + + /* This is an unclean working tree. So copy all tracked + files. */ + + GitInfo gitInfo; + gitInfo.rev = "0000000000000000000000000000000000000000"; + gitInfo.shortRev = std::string(gitInfo.rev, 0, 7); + + auto files = tokenizeString<std::set<std::string>>( + runProgram("git", true, { "-C", uri, "ls-files", "-z" }), "\0"s); + + PathFilter filter = [&](const Path & p) -> bool { + assert(hasPrefix(p, uri)); + std::string file(p, uri.size() + 1); + + auto st = lstat(p); + + if (S_ISDIR(st.st_mode)) { + auto prefix = file + "/"; + auto i = files.lower_bound(prefix); + return i != files.end() && hasPrefix(*i, prefix); + } + + return files.count(file); + }; + + gitInfo.storePath = store->addToStore("source", uri, true, htSHA256, filter); + + return gitInfo; + } + + // clean working tree, but no ref or rev specified. Use 'HEAD'. + rev = chomp(runProgram("git", true, { "-C", uri, "rev-parse", "HEAD" })); + ref = "HEAD"s; + } + + if (!ref) ref = "HEAD"s; + + if (rev != "" && !std::regex_match(rev, revRegex)) + throw Error("invalid Git revision '%s'", rev); + + deletePath(getCacheDir() + "/nix/git"); + + Path cacheDir = getCacheDir() + "/nix/gitv2/" + hashString(htSHA256, uri).to_string(Base32, false); + + if (!pathExists(cacheDir)) { + createDirs(dirOf(cacheDir)); + runProgram("git", true, { "init", "--bare", cacheDir }); + } + + Path localRefFile; + if (ref->compare(0, 5, "refs/") == 0) + localRefFile = cacheDir + "/" + *ref; + else + localRefFile = cacheDir + "/refs/heads/" + *ref; + + bool doFetch; + time_t now = time(0); + /* If a rev was specified, we need to fetch if it's not in the + repo. */ + if (rev != "") { + try { + runProgram("git", true, { "-C", cacheDir, "cat-file", "-e", rev }); + doFetch = false; + } catch (ExecError & e) { + if (WIFEXITED(e.status)) { + doFetch = true; + } else { + throw; + } + } + } else { + /* If the local ref is older than ‘tarball-ttl’ seconds, do a + git fetch to update the local ref to the remote ref. */ + struct stat st; + doFetch = stat(localRefFile.c_str(), &st) != 0 || + (uint64_t) st.st_mtime + settings.tarballTtl <= (uint64_t) now; + } + if (doFetch) + { + Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Git repository '%s'", uri)); + + // FIXME: git stderr messes up our progress indicator, so + // we're using --quiet for now. Should process its stderr. + runProgram("git", true, { "-C", cacheDir, "fetch", "--quiet", "--force", "--", uri, fmt("%s:%s", *ref, *ref) }); + + struct timeval times[2]; + times[0].tv_sec = now; + times[0].tv_usec = 0; + times[1].tv_sec = now; + times[1].tv_usec = 0; + + utimes(localRefFile.c_str(), times); + } + + // FIXME: check whether rev is an ancestor of ref. + GitInfo gitInfo; + gitInfo.rev = rev != "" ? rev : chomp(readFile(localRefFile)); + gitInfo.shortRev = std::string(gitInfo.rev, 0, 7); + + printTalkative("using revision %s of repo '%s'", gitInfo.rev, uri); + + std::string storeLinkName = hashString(htSHA512, name + std::string("\0"s) + gitInfo.rev).to_string(Base32, false); + Path storeLink = cacheDir + "/" + storeLinkName + ".link"; + PathLocks storeLinkLock({storeLink}, fmt("waiting for lock on '%1%'...", storeLink)); // FIXME: broken + + try { + auto json = nlohmann::json::parse(readFile(storeLink)); + + assert(json["name"] == name && json["rev"] == gitInfo.rev); + + gitInfo.storePath = json["storePath"]; + + if (store->isValidPath(gitInfo.storePath)) { + gitInfo.revCount = json["revCount"]; + return gitInfo; + } + + } catch (SysError & e) { + if (e.errNo != ENOENT) throw; + } + + // FIXME: should pipe this, or find some better way to extract a + // revision. + auto tar = runProgram("git", true, { "-C", cacheDir, "archive", gitInfo.rev }); + + Path tmpDir = createTempDir(); + AutoDelete delTmpDir(tmpDir, true); + + runProgram("tar", true, { "x", "-C", tmpDir }, tar); + + gitInfo.storePath = store->addToStore(name, tmpDir); + + gitInfo.revCount = std::stoull(runProgram("git", true, { "-C", cacheDir, "rev-list", "--count", gitInfo.rev })); + + nlohmann::json json; + json["storePath"] = gitInfo.storePath; + json["uri"] = uri; + json["name"] = name; + json["rev"] = gitInfo.rev; + json["revCount"] = gitInfo.revCount; + + writeFile(storeLink, json.dump()); + + return gitInfo; +} + +static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + std::string url; + std::optional<std::string> ref; + std::string rev; + std::string name = "source"; + PathSet context; + + state.forceValue(*args[0]); + + if (args[0]->type == tAttrs) { + + state.forceAttrs(*args[0], pos); + + for (auto & attr : *args[0]->attrs) { + string n(attr.name); + if (n == "url") + url = state.coerceToString(*attr.pos, *attr.value, context, false, false); + else if (n == "ref") + ref = state.forceStringNoCtx(*attr.value, *attr.pos); + else if (n == "rev") + rev = state.forceStringNoCtx(*attr.value, *attr.pos); + else if (n == "name") + name = state.forceStringNoCtx(*attr.value, *attr.pos); + else + throw EvalError("unsupported argument '%s' to 'fetchGit', at %s", attr.name, *attr.pos); + } + + if (url.empty()) + throw EvalError(format("'url' argument required, at %1%") % pos); + + } else + url = state.coerceToString(pos, *args[0], context, false, false); + + // FIXME: git externals probably can be used to bypass the URI + // whitelist. Ah well. + state.checkURI(url); + + auto gitInfo = exportGit(state.store, url, ref, rev, name); + + state.mkAttrs(v, 8); + mkString(*state.allocAttr(v, state.sOutPath), gitInfo.storePath, PathSet({gitInfo.storePath})); + mkString(*state.allocAttr(v, state.symbols.create("rev")), gitInfo.rev); + mkString(*state.allocAttr(v, state.symbols.create("shortRev")), gitInfo.shortRev); + mkInt(*state.allocAttr(v, state.symbols.create("revCount")), gitInfo.revCount); + v.attrs->sort(); + + if (state.allowedPaths) + state.allowedPaths->insert(state.store->toRealPath(gitInfo.storePath)); +} + +static RegisterPrimOp r("fetchGit", 1, prim_fetchGit); + +} diff --git a/third_party/nix/src/libexpr/primops/fetchMercurial.cc b/third_party/nix/src/libexpr/primops/fetchMercurial.cc new file mode 100644 index 000000000000..a907d0e1cd82 --- /dev/null +++ b/third_party/nix/src/libexpr/primops/fetchMercurial.cc @@ -0,0 +1,219 @@ +#include "primops.hh" +#include "eval-inline.hh" +#include "download.hh" +#include "store-api.hh" +#include "pathlocks.hh" + +#include <sys/time.h> + +#include <regex> + +#include <nlohmann/json.hpp> + +using namespace std::string_literals; + +namespace nix { + +struct HgInfo +{ + Path storePath; + std::string branch; + std::string rev; + uint64_t revCount = 0; +}; + +std::regex commitHashRegex("^[0-9a-fA-F]{40}$"); + +HgInfo exportMercurial(ref<Store> store, const std::string & uri, + std::string rev, const std::string & name) +{ + if (evalSettings.pureEval && rev == "") + throw Error("in pure evaluation mode, 'fetchMercurial' requires a Mercurial revision"); + + if (rev == "" && hasPrefix(uri, "/") && pathExists(uri + "/.hg")) { + + bool clean = runProgram("hg", true, { "status", "-R", uri, "--modified", "--added", "--removed" }) == ""; + + if (!clean) { + + /* This is an unclean working tree. So copy all tracked + files. */ + + printTalkative("copying unclean Mercurial working tree '%s'", uri); + + HgInfo hgInfo; + hgInfo.rev = "0000000000000000000000000000000000000000"; + hgInfo.branch = chomp(runProgram("hg", true, { "branch", "-R", uri })); + + auto files = tokenizeString<std::set<std::string>>( + runProgram("hg", true, { "status", "-R", uri, "--clean", "--modified", "--added", "--no-status", "--print0" }), "\0"s); + + PathFilter filter = [&](const Path & p) -> bool { + assert(hasPrefix(p, uri)); + std::string file(p, uri.size() + 1); + + auto st = lstat(p); + + if (S_ISDIR(st.st_mode)) { + auto prefix = file + "/"; + auto i = files.lower_bound(prefix); + return i != files.end() && hasPrefix(*i, prefix); + } + + return files.count(file); + }; + + hgInfo.storePath = store->addToStore("source", uri, true, htSHA256, filter); + + return hgInfo; + } + } + + if (rev == "") rev = "default"; + + Path cacheDir = fmt("%s/nix/hg/%s", getCacheDir(), hashString(htSHA256, uri).to_string(Base32, false)); + + Path stampFile = fmt("%s/.hg/%s.stamp", cacheDir, hashString(htSHA512, rev).to_string(Base32, false)); + + /* If we haven't pulled this repo less than ‘tarball-ttl’ seconds, + do so now. */ + time_t now = time(0); + struct stat st; + if (stat(stampFile.c_str(), &st) != 0 || + (uint64_t) st.st_mtime + settings.tarballTtl <= (uint64_t) now) + { + /* Except that if this is a commit hash that we already have, + we don't have to pull again. */ + if (!(std::regex_match(rev, commitHashRegex) + && pathExists(cacheDir) + && runProgram( + RunOptions("hg", { "log", "-R", cacheDir, "-r", rev, "--template", "1" }) + .killStderr(true)).second == "1")) + { + Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Mercurial repository '%s'", uri)); + + if (pathExists(cacheDir)) { + try { + runProgram("hg", true, { "pull", "-R", cacheDir, "--", uri }); + } + catch (ExecError & e) { + string transJournal = cacheDir + "/.hg/store/journal"; + /* hg throws "abandoned transaction" error only if this file exists */ + if (pathExists(transJournal)) { + runProgram("hg", true, { "recover", "-R", cacheDir }); + runProgram("hg", true, { "pull", "-R", cacheDir, "--", uri }); + } else { + throw ExecError(e.status, fmt("'hg pull' %s", statusToString(e.status))); + } + } + } else { + createDirs(dirOf(cacheDir)); + runProgram("hg", true, { "clone", "--noupdate", "--", uri, cacheDir }); + } + } + + writeFile(stampFile, ""); + } + + auto tokens = tokenizeString<std::vector<std::string>>( + runProgram("hg", true, { "log", "-R", cacheDir, "-r", rev, "--template", "{node} {rev} {branch}" })); + assert(tokens.size() == 3); + + HgInfo hgInfo; + hgInfo.rev = tokens[0]; + hgInfo.revCount = std::stoull(tokens[1]); + hgInfo.branch = tokens[2]; + + std::string storeLinkName = hashString(htSHA512, name + std::string("\0"s) + hgInfo.rev).to_string(Base32, false); + Path storeLink = fmt("%s/.hg/%s.link", cacheDir, storeLinkName); + + try { + auto json = nlohmann::json::parse(readFile(storeLink)); + + assert(json["name"] == name && json["rev"] == hgInfo.rev); + + hgInfo.storePath = json["storePath"]; + + if (store->isValidPath(hgInfo.storePath)) { + printTalkative("using cached Mercurial store path '%s'", hgInfo.storePath); + return hgInfo; + } + + } catch (SysError & e) { + if (e.errNo != ENOENT) throw; + } + + Path tmpDir = createTempDir(); + AutoDelete delTmpDir(tmpDir, true); + + runProgram("hg", true, { "archive", "-R", cacheDir, "-r", rev, tmpDir }); + + deletePath(tmpDir + "/.hg_archival.txt"); + + hgInfo.storePath = store->addToStore(name, tmpDir); + + nlohmann::json json; + json["storePath"] = hgInfo.storePath; + json["uri"] = uri; + json["name"] = name; + json["branch"] = hgInfo.branch; + json["rev"] = hgInfo.rev; + json["revCount"] = hgInfo.revCount; + + writeFile(storeLink, json.dump()); + + return hgInfo; +} + +static void prim_fetchMercurial(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + std::string url; + std::string rev; + std::string name = "source"; + PathSet context; + + state.forceValue(*args[0]); + + if (args[0]->type == tAttrs) { + + state.forceAttrs(*args[0], pos); + + for (auto & attr : *args[0]->attrs) { + string n(attr.name); + if (n == "url") + url = state.coerceToString(*attr.pos, *attr.value, context, false, false); + else if (n == "rev") + rev = state.forceStringNoCtx(*attr.value, *attr.pos); + else if (n == "name") + name = state.forceStringNoCtx(*attr.value, *attr.pos); + else + throw EvalError("unsupported argument '%s' to 'fetchMercurial', at %s", attr.name, *attr.pos); + } + + if (url.empty()) + throw EvalError(format("'url' argument required, at %1%") % pos); + + } else + url = state.coerceToString(pos, *args[0], context, false, false); + + // FIXME: git externals probably can be used to bypass the URI + // whitelist. Ah well. + state.checkURI(url); + + auto hgInfo = exportMercurial(state.store, url, rev, name); + + state.mkAttrs(v, 8); + mkString(*state.allocAttr(v, state.sOutPath), hgInfo.storePath, PathSet({hgInfo.storePath})); + mkString(*state.allocAttr(v, state.symbols.create("branch")), hgInfo.branch); + mkString(*state.allocAttr(v, state.symbols.create("rev")), hgInfo.rev); + mkString(*state.allocAttr(v, state.symbols.create("shortRev")), std::string(hgInfo.rev, 0, 12)); + mkInt(*state.allocAttr(v, state.symbols.create("revCount")), hgInfo.revCount); + v.attrs->sort(); + + if (state.allowedPaths) + state.allowedPaths->insert(state.store->toRealPath(hgInfo.storePath)); +} + +static RegisterPrimOp r("fetchMercurial", 1, prim_fetchMercurial); + +} diff --git a/third_party/nix/src/libexpr/primops/fromTOML.cc b/third_party/nix/src/libexpr/primops/fromTOML.cc new file mode 100644 index 000000000000..a84e569e944d --- /dev/null +++ b/third_party/nix/src/libexpr/primops/fromTOML.cc @@ -0,0 +1,90 @@ +#include "primops.hh" +#include "eval-inline.hh" + +#include "cpptoml/cpptoml.h" + +namespace nix { + +static void prim_fromTOML(EvalState & state, const Pos & pos, Value * * args, Value & v) +{ + using namespace cpptoml; + + auto toml = state.forceStringNoCtx(*args[0], pos); + + std::istringstream tomlStream(toml); + + std::function<void(Value &, std::shared_ptr<base>)> visit; + + visit = [&](Value & v, std::shared_ptr<base> t) { + + if (auto t2 = t->as_table()) { + + size_t size = 0; + for (auto & i : *t2) { (void) i; size++; } + + state.mkAttrs(v, size); + + for (auto & i : *t2) { + auto & v2 = *state.allocAttr(v, state.symbols.create(i.first)); + + if (auto i2 = i.second->as_table_array()) { + size_t size2 = i2->get().size(); + state.mkList(v2, size2); + for (size_t j = 0; j < size2; ++j) + visit(*(v2.listElems()[j] = state.allocValue()), i2->get()[j]); + } + else + visit(v2, i.second); + } + + v.attrs->sort(); + } + + else if (auto t2 = t->as_array()) { + size_t size = t2->get().size(); + + state.mkList(v, size); + + for (size_t i = 0; i < size; ++i) + visit(*(v.listElems()[i] = state.allocValue()), t2->get()[i]); + } + + // Handle cases like 'a = [[{ a = true }]]', which IMHO should be + // parsed as a array containing an array containing a table, + // but instead are parsed as an array containing a table array + // containing a table. + else if (auto t2 = t->as_table_array()) { + size_t size = t2->get().size(); + + state.mkList(v, size); + + for (size_t j = 0; j < size; ++j) + visit(*(v.listElems()[j] = state.allocValue()), t2->get()[j]); + } + + else if (t->is_value()) { + if (auto val = t->as<int64_t>()) + mkInt(v, val->get()); + else if (auto val = t->as<NixFloat>()) + mkFloat(v, val->get()); + else if (auto val = t->as<bool>()) + mkBool(v, val->get()); + else if (auto val = t->as<std::string>()) + mkString(v, val->get()); + else + throw EvalError("unsupported value type in TOML"); + } + + else abort(); + }; + + try { + visit(v, parser(tomlStream).parse()); + } catch (std::runtime_error & e) { + throw EvalError("while parsing a TOML string at %s: %s", pos, e.what()); + } +} + +static RegisterPrimOp r("fromTOML", 1, prim_fromTOML); + +} diff --git a/third_party/nix/src/libexpr/symbol-table.hh b/third_party/nix/src/libexpr/symbol-table.hh new file mode 100644 index 000000000000..91faea122ce1 --- /dev/null +++ b/third_party/nix/src/libexpr/symbol-table.hh @@ -0,0 +1,87 @@ +#pragma once + +#include <map> +#include <unordered_set> + +#include "types.hh" + +namespace nix { + +/* Symbol table used by the parser and evaluator to represent and look + up identifiers and attributes efficiently. SymbolTable::create() + converts a string into a symbol. Symbols have the property that + they can be compared efficiently (using a pointer equality test), + because the symbol table stores only one copy of each string. */ + +class Symbol +{ +private: + const string * s; // pointer into SymbolTable + Symbol(const string * s) : s(s) { }; + friend class SymbolTable; + +public: + Symbol() : s(0) { }; + + bool operator == (const Symbol & s2) const + { + return s == s2.s; + } + + bool operator != (const Symbol & s2) const + { + return s != s2.s; + } + + bool operator < (const Symbol & s2) const + { + return s < s2.s; + } + + operator const string & () const + { + return *s; + } + + bool set() const + { + return s; + } + + bool empty() const + { + return s->empty(); + } + + friend std::ostream & operator << (std::ostream & str, const Symbol & sym); +}; + +class SymbolTable +{ +private: + typedef std::unordered_set<string> Symbols; + Symbols symbols; + +public: + Symbol create(const string & s) + { + std::pair<Symbols::iterator, bool> res = symbols.insert(s); + return Symbol(&*res.first); + } + + size_t size() const + { + return symbols.size(); + } + + size_t totalSize() const; + + template<typename T> + void dump(T callback) + { + for (auto & s : symbols) + callback(s); + } +}; + +} diff --git a/third_party/nix/src/libexpr/value-to-json.cc b/third_party/nix/src/libexpr/value-to-json.cc new file mode 100644 index 000000000000..5fe8570adeb4 --- /dev/null +++ b/third_party/nix/src/libexpr/value-to-json.cc @@ -0,0 +1,100 @@ +#include "value-to-json.hh" +#include "json.hh" +#include "eval-inline.hh" +#include "util.hh" + +#include <cstdlib> +#include <iomanip> + + +namespace nix { + +void printValueAsJSON(EvalState & state, bool strict, + Value & v, JSONPlaceholder & out, PathSet & context) +{ + checkInterrupt(); + + if (strict) state.forceValue(v); + + switch (v.type) { + + case tInt: + out.write(v.integer); + break; + + case tBool: + out.write(v.boolean); + break; + + case tString: + copyContext(v, context); + out.write(v.string.s); + break; + + case tPath: + out.write(state.copyPathToStore(context, v.path)); + break; + + case tNull: + out.write(nullptr); + break; + + case tAttrs: { + auto maybeString = state.tryAttrsToString(noPos, v, context, false, false); + if (maybeString) { + out.write(*maybeString); + break; + } + auto i = v.attrs->find(state.sOutPath); + if (i == v.attrs->end()) { + auto obj(out.object()); + StringSet names; + for (auto & j : *v.attrs) + names.insert(j.name); + for (auto & j : names) { + Attr & a(*v.attrs->find(state.symbols.create(j))); + auto placeholder(obj.placeholder(j)); + printValueAsJSON(state, strict, *a.value, placeholder, context); + } + } else + printValueAsJSON(state, strict, *i->value, out, context); + break; + } + + case tList1: case tList2: case tListN: { + auto list(out.list()); + for (unsigned int n = 0; n < v.listSize(); ++n) { + auto placeholder(list.placeholder()); + printValueAsJSON(state, strict, *v.listElems()[n], placeholder, context); + } + break; + } + + case tExternal: + v.external->printValueAsJSON(state, strict, out, context); + break; + + case tFloat: + out.write(v.fpoint); + break; + + default: + throw TypeError(format("cannot convert %1% to JSON") % showType(v)); + } +} + +void printValueAsJSON(EvalState & state, bool strict, + Value & v, std::ostream & str, PathSet & context) +{ + JSONPlaceholder out(str); + printValueAsJSON(state, strict, v, out, context); +} + +void ExternalValueBase::printValueAsJSON(EvalState & state, bool strict, + JSONPlaceholder & out, PathSet & context) const +{ + throw TypeError(format("cannot convert %1% to JSON") % showType()); +} + + +} diff --git a/third_party/nix/src/libexpr/value-to-json.hh b/third_party/nix/src/libexpr/value-to-json.hh new file mode 100644 index 000000000000..67fed6487dd9 --- /dev/null +++ b/third_party/nix/src/libexpr/value-to-json.hh @@ -0,0 +1,19 @@ +#pragma once + +#include "nixexpr.hh" +#include "eval.hh" + +#include <string> +#include <map> + +namespace nix { + +class JSONPlaceholder; + +void printValueAsJSON(EvalState & state, bool strict, + Value & v, JSONPlaceholder & out, PathSet & context); + +void printValueAsJSON(EvalState & state, bool strict, + Value & v, std::ostream & str, PathSet & context); + +} diff --git a/third_party/nix/src/libexpr/value-to-xml.cc b/third_party/nix/src/libexpr/value-to-xml.cc new file mode 100644 index 000000000000..00b1918a82aa --- /dev/null +++ b/third_party/nix/src/libexpr/value-to-xml.cc @@ -0,0 +1,178 @@ +#include "value-to-xml.hh" +#include "xml-writer.hh" +#include "eval-inline.hh" +#include "util.hh" + +#include <cstdlib> + + +namespace nix { + + +static XMLAttrs singletonAttrs(const string & name, const string & value) +{ + XMLAttrs attrs; + attrs[name] = value; + return attrs; +} + + +static void printValueAsXML(EvalState & state, bool strict, bool location, + Value & v, XMLWriter & doc, PathSet & context, PathSet & drvsSeen); + + +static void posToXML(XMLAttrs & xmlAttrs, const Pos & pos) +{ + xmlAttrs["path"] = pos.file; + xmlAttrs["line"] = (format("%1%") % pos.line).str(); + xmlAttrs["column"] = (format("%1%") % pos.column).str(); +} + + +static void showAttrs(EvalState & state, bool strict, bool location, + Bindings & attrs, XMLWriter & doc, PathSet & context, PathSet & drvsSeen) +{ + StringSet names; + + for (auto & i : attrs) + names.insert(i.name); + + for (auto & i : names) { + Attr & a(*attrs.find(state.symbols.create(i))); + + XMLAttrs xmlAttrs; + xmlAttrs["name"] = i; + if (location && a.pos != &noPos) posToXML(xmlAttrs, *a.pos); + + XMLOpenElement _(doc, "attr", xmlAttrs); + printValueAsXML(state, strict, location, + *a.value, doc, context, drvsSeen); + } +} + + +static void printValueAsXML(EvalState & state, bool strict, bool location, + Value & v, XMLWriter & doc, PathSet & context, PathSet & drvsSeen) +{ + checkInterrupt(); + + if (strict) state.forceValue(v); + + switch (v.type) { + + case tInt: + doc.writeEmptyElement("int", singletonAttrs("value", (format("%1%") % v.integer).str())); + break; + + case tBool: + doc.writeEmptyElement("bool", singletonAttrs("value", v.boolean ? "true" : "false")); + break; + + case tString: + /* !!! show the context? */ + copyContext(v, context); + doc.writeEmptyElement("string", singletonAttrs("value", v.string.s)); + break; + + case tPath: + doc.writeEmptyElement("path", singletonAttrs("value", v.path)); + break; + + case tNull: + doc.writeEmptyElement("null"); + break; + + case tAttrs: + if (state.isDerivation(v)) { + XMLAttrs xmlAttrs; + + Bindings::iterator a = v.attrs->find(state.symbols.create("derivation")); + + Path drvPath; + a = v.attrs->find(state.sDrvPath); + if (a != v.attrs->end()) { + if (strict) state.forceValue(*a->value); + if (a->value->type == tString) + xmlAttrs["drvPath"] = drvPath = a->value->string.s; + } + + a = v.attrs->find(state.sOutPath); + if (a != v.attrs->end()) { + if (strict) state.forceValue(*a->value); + if (a->value->type == tString) + xmlAttrs["outPath"] = a->value->string.s; + } + + XMLOpenElement _(doc, "derivation", xmlAttrs); + + if (drvPath != "" && drvsSeen.find(drvPath) == drvsSeen.end()) { + drvsSeen.insert(drvPath); + showAttrs(state, strict, location, *v.attrs, doc, context, drvsSeen); + } else + doc.writeEmptyElement("repeated"); + } + + else { + XMLOpenElement _(doc, "attrs"); + showAttrs(state, strict, location, *v.attrs, doc, context, drvsSeen); + } + + break; + + case tList1: case tList2: case tListN: { + XMLOpenElement _(doc, "list"); + for (unsigned int n = 0; n < v.listSize(); ++n) + printValueAsXML(state, strict, location, *v.listElems()[n], doc, context, drvsSeen); + break; + } + + case tLambda: { + XMLAttrs xmlAttrs; + if (location) posToXML(xmlAttrs, v.lambda.fun->pos); + XMLOpenElement _(doc, "function", xmlAttrs); + + if (v.lambda.fun->matchAttrs) { + XMLAttrs attrs; + if (!v.lambda.fun->arg.empty()) attrs["name"] = v.lambda.fun->arg; + if (v.lambda.fun->formals->ellipsis) attrs["ellipsis"] = "1"; + XMLOpenElement _(doc, "attrspat", attrs); + for (auto & i : v.lambda.fun->formals->formals) + doc.writeEmptyElement("attr", singletonAttrs("name", i.name)); + } else + doc.writeEmptyElement("varpat", singletonAttrs("name", v.lambda.fun->arg)); + + break; + } + + case tExternal: + v.external->printValueAsXML(state, strict, location, doc, context, drvsSeen); + break; + + case tFloat: + doc.writeEmptyElement("float", singletonAttrs("value", (format("%1%") % v.fpoint).str())); + break; + + default: + doc.writeEmptyElement("unevaluated"); + } +} + + +void ExternalValueBase::printValueAsXML(EvalState & state, bool strict, + bool location, XMLWriter & doc, PathSet & context, PathSet & drvsSeen) const +{ + doc.writeEmptyElement("unevaluated"); +} + + +void printValueAsXML(EvalState & state, bool strict, bool location, + Value & v, std::ostream & out, PathSet & context) +{ + XMLWriter doc(true, out); + XMLOpenElement root(doc, "expr"); + PathSet drvsSeen; + printValueAsXML(state, strict, location, v, doc, context, drvsSeen); +} + + +} diff --git a/third_party/nix/src/libexpr/value-to-xml.hh b/third_party/nix/src/libexpr/value-to-xml.hh new file mode 100644 index 000000000000..97657327edba --- /dev/null +++ b/third_party/nix/src/libexpr/value-to-xml.hh @@ -0,0 +1,14 @@ +#pragma once + +#include "nixexpr.hh" +#include "eval.hh" + +#include <string> +#include <map> + +namespace nix { + +void printValueAsXML(EvalState & state, bool strict, bool location, + Value & v, std::ostream & out, PathSet & context); + +} diff --git a/third_party/nix/src/libexpr/value.hh b/third_party/nix/src/libexpr/value.hh new file mode 100644 index 000000000000..e1ec87d3b84c --- /dev/null +++ b/third_party/nix/src/libexpr/value.hh @@ -0,0 +1,274 @@ +#pragma once + +#include "symbol-table.hh" + +#if HAVE_BOEHMGC +#include <gc/gc_allocator.h> +#endif + +namespace nix { + + +typedef enum { + tInt = 1, + tBool, + tString, + tPath, + tNull, + tAttrs, + tList1, + tList2, + tListN, + tThunk, + tApp, + tLambda, + tBlackhole, + tPrimOp, + tPrimOpApp, + tExternal, + tFloat +} ValueType; + + +class Bindings; +struct Env; +struct Expr; +struct ExprLambda; +struct PrimOp; +struct PrimOp; +class Symbol; +struct Pos; +class EvalState; +class XMLWriter; +class JSONPlaceholder; + + +typedef int64_t NixInt; +typedef double NixFloat; + +/* External values must descend from ExternalValueBase, so that + * type-agnostic nix functions (e.g. showType) can be implemented + */ +class ExternalValueBase +{ + friend std::ostream & operator << (std::ostream & str, const ExternalValueBase & v); + protected: + /* Print out the value */ + virtual std::ostream & print(std::ostream & str) const = 0; + + public: + /* Return a simple string describing the type */ + virtual string showType() const = 0; + + /* Return a string to be used in builtins.typeOf */ + virtual string typeOf() const = 0; + + /* How much space does this value take up */ + virtual size_t valueSize(std::set<const void *> & seen) const = 0; + + /* Coerce the value to a string. Defaults to uncoercable, i.e. throws an + * error + */ + virtual string coerceToString(const Pos & pos, PathSet & context, bool copyMore, bool copyToStore) const; + + /* Compare to another value of the same type. Defaults to uncomparable, + * i.e. always false. + */ + virtual bool operator==(const ExternalValueBase & b) const; + + /* Print the value as JSON. Defaults to unconvertable, i.e. throws an error */ + virtual void printValueAsJSON(EvalState & state, bool strict, + JSONPlaceholder & out, PathSet & context) const; + + /* Print the value as XML. Defaults to unevaluated */ + virtual void printValueAsXML(EvalState & state, bool strict, bool location, + XMLWriter & doc, PathSet & context, PathSet & drvsSeen) const; + + virtual ~ExternalValueBase() + { + }; +}; + +std::ostream & operator << (std::ostream & str, const ExternalValueBase & v); + + +struct Value +{ + ValueType type; + union + { + NixInt integer; + bool boolean; + + /* Strings in the evaluator carry a so-called `context' which + is a list of strings representing store paths. This is to + allow users to write things like + + "--with-freetype2-library=" + freetype + "/lib" + + where `freetype' is a derivation (or a source to be copied + to the store). If we just concatenated the strings without + keeping track of the referenced store paths, then if the + string is used as a derivation attribute, the derivation + will not have the correct dependencies in its inputDrvs and + inputSrcs. + + The semantics of the context is as follows: when a string + with context C is used as a derivation attribute, then the + derivations in C will be added to the inputDrvs of the + derivation, and the other store paths in C will be added to + the inputSrcs of the derivations. + + For canonicity, the store paths should be in sorted order. */ + struct { + const char * s; + const char * * context; // must be in sorted order + } string; + + const char * path; + Bindings * attrs; + struct { + size_t size; + Value * * elems; + } bigList; + Value * smallList[2]; + struct { + Env * env; + Expr * expr; + } thunk; + struct { + Value * left, * right; + } app; + struct { + Env * env; + ExprLambda * fun; + } lambda; + PrimOp * primOp; + struct { + Value * left, * right; + } primOpApp; + ExternalValueBase * external; + NixFloat fpoint; + }; + + bool isList() const + { + return type == tList1 || type == tList2 || type == tListN; + } + + Value * * listElems() + { + return type == tList1 || type == tList2 ? smallList : bigList.elems; + } + + const Value * const * listElems() const + { + return type == tList1 || type == tList2 ? smallList : bigList.elems; + } + + size_t listSize() const + { + return type == tList1 ? 1 : type == tList2 ? 2 : bigList.size; + } +}; + + +/* After overwriting an app node, be sure to clear pointers in the + Value to ensure that the target isn't kept alive unnecessarily. */ +static inline void clearValue(Value & v) +{ + v.app.left = v.app.right = 0; +} + + +static inline void mkInt(Value & v, NixInt n) +{ + clearValue(v); + v.type = tInt; + v.integer = n; +} + + +static inline void mkFloat(Value & v, NixFloat n) +{ + clearValue(v); + v.type = tFloat; + v.fpoint = n; +} + + +static inline void mkBool(Value & v, bool b) +{ + clearValue(v); + v.type = tBool; + v.boolean = b; +} + + +static inline void mkNull(Value & v) +{ + clearValue(v); + v.type = tNull; +} + + +static inline void mkApp(Value & v, Value & left, Value & right) +{ + v.type = tApp; + v.app.left = &left; + v.app.right = &right; +} + + +static inline void mkPrimOpApp(Value & v, Value & left, Value & right) +{ + v.type = tPrimOpApp; + v.app.left = &left; + v.app.right = &right; +} + + +static inline void mkStringNoCopy(Value & v, const char * s) +{ + v.type = tString; + v.string.s = s; + v.string.context = 0; +} + + +static inline void mkString(Value & v, const Symbol & s) +{ + mkStringNoCopy(v, ((const string &) s).c_str()); +} + + +void mkString(Value & v, const char * s); + + +static inline void mkPathNoCopy(Value & v, const char * s) +{ + clearValue(v); + v.type = tPath; + v.path = s; +} + + +void mkPath(Value & v, const char * s); + + +/* Compute the size in bytes of the given value, including all values + and environments reachable from it. Static expressions (Exprs) are + not included. */ +size_t valueSize(Value & v); + + +#if HAVE_BOEHMGC +typedef std::vector<Value *, gc_allocator<Value *> > ValueVector; +typedef std::map<Symbol, Value *, std::less<Symbol>, gc_allocator<std::pair<const Symbol, Value *> > > ValueMap; +#else +typedef std::vector<Value *> ValueVector; +typedef std::map<Symbol, Value *> ValueMap; +#endif + + +} |