lix/src/libexpr/eval.cc
Eelco Dolstra e2ef5e07fd * Refactoring. There is now an abstract interface class StoreAPI
containing functions that operate on the Nix store.  One
  implementation is LocalStore, which operates on the Nix store
  directly.  The next step, to enable secure multi-user Nix, is to
  create a different implementation RemoteStore that talks to a
  privileged daemon process that uses LocalStore to perform the actual
  operations.
2006-11-30 17:43:04 +00:00

743 lines
23 KiB
C++

#include "eval.hh"
#include "parser.hh"
#include "hash.hh"
#include "util.hh"
#include "store-api.hh"
#include "derivations.hh"
#include "nixexpr-ast.hh"
namespace nix {
EvalState::EvalState()
: normalForms(32768), primOps(128)
{
nrEvaluated = nrCached = 0;
initNixExprHelpers();
addPrimOps();
}
void EvalState::addPrimOp(const string & name,
unsigned int arity, PrimOp primOp)
{
primOps.set(toATerm(name), makePrimOpDef(arity, ATmakeBlob(0, (void *) primOp)));
}
/* Substitute an argument set into the body of a function. */
static Expr substArgs(EvalState & state,
Expr body, ATermList formals, Expr arg)
{
unsigned int nrFormals = ATgetLength(formals);
ATermMap subs(nrFormals);
/* Get the actual arguments and put them in the substitution. */
ATermMap args(128); /* !!! fix */
queryAllAttrs(arg, args);
for (ATermMap::const_iterator i = args.begin(); i != args.end(); ++i)
subs.set(i->key, i->value);
/* Get the formal arguments. */
ATermVector defsUsed;
ATermList recAttrs = ATempty;
for (ATermIterator i(formals); i; ++i) {
Expr name, def;
ValidValues valids2;
DefaultValue def2;
if (!matchFormal(*i, name, valids2, def2)) abort(); /* can't happen */
Expr value = subs[name];
if (value == 0) {
if (!matchDefaultValue(def2, def)) def = 0;
if (def == 0) throw TypeError(format("the argument named `%1%' required by the function is missing")
% aterm2String(name));
value = def;
defsUsed.push_back(name);
recAttrs = ATinsert(recAttrs, makeBind(name, def, makeNoPos()));
}
ATermList valids;
if (matchValidValues(valids2, valids)) {
value = evalExpr(state, value);
bool found = false;
for (ATermIterator j(valids); j; ++j) {
Expr v = evalExpr(state, *j);
if (value == v) {
found = true;
break;
}
}
if (!found) throw TypeError(format("the argument named `%1%' has an illegal value")
% aterm2String(name));
}
}
/* Make a recursive attribute set out of the (argument-name,
value) tuples. This is so that we can support default
parameters that refer to each other, e.g. ({x, y ? x + x}: y)
{x = "foo";} evaluates to "foofoo". */
if (defsUsed.size() != 0) {
for (ATermMap::const_iterator i = args.begin(); i != args.end(); ++i)
recAttrs = ATinsert(recAttrs, makeBind(i->key, i->value, makeNoPos()));
Expr rec = makeRec(recAttrs, ATempty);
for (ATermVector::iterator i = defsUsed.begin(); i != defsUsed.end(); ++i)
subs.set(*i, makeSelect(rec, *i));
}
if (subs.size() != nrFormals) {
/* One or more actual arguments were not declared as formal
arguments. Find out which. */
for (ATermIterator i(formals); i; ++i) {
Expr name; ATerm d1, d2;
if (!matchFormal(*i, name, d1, d2)) abort();
subs.remove(name);
}
throw TypeError(format("the function does not expect an argument named `%1%'")
% aterm2String(subs.begin()->key));
}
return substitute(Substitution(0, &subs), body);
}
/* Transform a mutually recursive set into a non-recursive set. Each
attribute is transformed into an expression that has all references
to attributes substituted with selection expressions on the
original set. E.g., e = `rec {x = f x y; y = x;}' becomes `{x = f
(e.x) (e.y); y = e.x;}'. */
ATerm expandRec(ATerm e, ATermList rbnds, ATermList nrbnds)
{
ATerm name;
Expr e2;
Pos pos;
/* Create the substitution list. */
ATermMap subs(ATgetLength(rbnds) + ATgetLength(nrbnds));
for (ATermIterator i(rbnds); i; ++i) {
if (!matchBind(*i, name, e2, pos)) abort(); /* can't happen */
subs.set(name, makeSelect(e, name));
}
for (ATermIterator i(nrbnds); i; ++i) {
if (!matchBind(*i, name, e2, pos)) abort(); /* can't happen */
subs.set(name, e2);
}
Substitution subs_(0, &subs);
/* Create the non-recursive set. */
ATermMap as(ATgetLength(rbnds) + ATgetLength(nrbnds));
for (ATermIterator i(rbnds); i; ++i) {
if (!matchBind(*i, name, e2, pos)) abort(); /* can't happen */
as.set(name, makeAttrRHS(substitute(subs_, e2), pos));
}
/* Copy the non-recursive bindings. !!! inefficient */
for (ATermIterator i(nrbnds); i; ++i) {
if (!matchBind(*i, name, e2, pos)) abort(); /* can't happen */
as.set(name, makeAttrRHS(e2, pos));
}
return makeAttrs(as);
}
static Expr updateAttrs(Expr e1, Expr e2)
{
/* Note: e1 and e2 should be in normal form. */
ATermMap attrs(128); /* !!! */
queryAllAttrs(e1, attrs, true);
queryAllAttrs(e2, attrs, true);
return makeAttrs(attrs);
}
string evalString(EvalState & state, Expr e, PathSet & context)
{
e = evalExpr(state, e);
string s;
if (!matchStr(e, s, context))
throw TypeError(format("value is %1% while a string was expected") % showType(e));
return s;
}
string evalStringNoCtx(EvalState & state, Expr e)
{
PathSet context;
string s = evalString(state, e, context);
if (!context.empty())
throw EvalError(format("the string `%1%' is not allowed to refer to a store path (such as `%2%')")
% s % *(context.begin()));
return s;
}
int evalInt(EvalState & state, Expr e)
{
e = evalExpr(state, e);
int i;
if (!matchInt(e, i))
throw TypeError(format("value is %1% while an integer was expected") % showType(e));
return i;
}
bool evalBool(EvalState & state, Expr e)
{
e = evalExpr(state, e);
if (e == eTrue) return true;
else if (e == eFalse) return false;
else throw TypeError(format("value is %1% while a boolean was expected") % showType(e));
}
ATermList evalList(EvalState & state, Expr e)
{
e = evalExpr(state, e);
ATermList list;
if (!matchList(e, list))
throw TypeError(format("value is %1% while a list was expected") % showType(e));
return list;
}
static void flattenList(EvalState & state, Expr e, ATermList & result)
{
ATermList es;
e = evalExpr(state, e);
if (matchList(e, es))
for (ATermIterator i(es); i; ++i)
flattenList(state, *i, result);
else
result = ATinsert(result, e);
}
ATermList flattenList(EvalState & state, Expr e)
{
ATermList result = ATempty;
flattenList(state, e, result);
return ATreverse(result);
}
string coerceToString(EvalState & state, Expr e, PathSet & context,
bool coerceMore, bool copyToStore)
{
e = evalExpr(state, e);
string s;
if (matchStr(e, s, context)) return s;
ATerm s2;
if (matchPath(e, s2)) {
Path path(canonPath(aterm2String(s2)));
if (!copyToStore) return path;
if (isDerivation(path))
throw EvalError(format("file names are not allowed to end in `%1%'")
% drvExtension);
Path dstPath;
if (state.srcToStore[path] != "")
dstPath = state.srcToStore[path];
else {
dstPath = store->addToStore(path);
state.srcToStore[path] = dstPath;
printMsg(lvlChatty, format("copied source `%1%' -> `%2%'")
% path % dstPath);
}
context.insert(dstPath);
return dstPath;
}
ATermList es;
if (matchAttrs(e, es))
return coerceToString(state, makeSelect(e, toATerm("outPath")),
context, coerceMore, copyToStore);
if (coerceMore) {
/* Note that `false' is represented as an empty string for
shell scripting convenience, just like `null'. */
if (e == eTrue) return "1";
if (e == eFalse) return "";
int n;
if (matchInt(e, n)) return int2String(n);
if (matchNull(e)) return "";
if (matchList(e, es)) {
string result;
es = flattenList(state, e);
bool first = true;
for (ATermIterator i(es); i; ++i) {
if (!first) result += " "; else first = false;
result += coerceToString(state, *i,
context, coerceMore, copyToStore);
}
return result;
}
}
throw TypeError(format("cannot coerce %1% to a string") % showType(e));
}
/* Common implementation of `+', ConcatStrings and `~'. */
static ATerm concatStrings(EvalState & state, ATermVector & args,
string separator = "")
{
if (args.empty()) return makeStr("", PathSet());
PathSet context;
std::ostringstream s;
/* 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. */
ATerm dummy;
args.front() = evalExpr(state, args.front());
bool isPath = matchPath(args.front(), dummy);
for (ATermVector::const_iterator i = args.begin(); i != args.end(); ++i) {
if (i != args.begin()) s << separator;
s << coerceToString(state, *i, context, false, !isPath);
}
if (isPath && !context.empty())
throw EvalError(format("a string that refers to a store path cannot be appended to a path, in `%1%'")
% s.str());
return isPath
? makePath(toATerm(s.str()))
: makeStr(s.str(), context);
}
Path coerceToPath(EvalState & state, Expr e, PathSet & context)
{
string path = coerceToString(state, e, context, false, false);
if (path == "" || path[0] != '/')
throw EvalError(format("string `%1%' doesn't represent an absolute path") % path);
return path;
}
Expr autoCallFunction(Expr e, const ATermMap & args)
{
ATermList formals;
ATerm body, pos;
if (matchFunction(e, formals, body, pos)) {
ATermMap actualArgs(128);
for (ATermIterator i(formals); i; ++i) {
Expr name, def, value; ATerm values, def2;
if (!matchFormal(*i, name, values, def2)) abort();
if ((value = args.get(name)))
actualArgs.set(name, makeAttrRHS(value, makeNoPos()));
else if (!matchDefaultValue(def2, def))
throw TypeError(format("cannot auto-call a function that has an argument without a default value (`%1%')")
% aterm2String(name));
}
e = makeCall(e, makeAttrs(actualArgs));
}
return e;
}
Expr evalExpr2(EvalState & state, Expr e)
{
Expr e1, e2, e3, e4;
ATerm name, pos;
AFun sym = ATgetAFun(e);
/* Normal forms. */
if (sym == symStr ||
sym == symPath ||
sym == symNull ||
sym == symInt ||
sym == symBool ||
sym == symFunction ||
sym == symFunction1 ||
sym == symAttrs ||
sym == symList ||
sym == symPrimOp)
return e;
/* The `Closed' constructor is just a way to prevent substitutions
into expressions not containing free variables. */
if (matchClosed(e, e1))
return evalExpr(state, e1);
/* Any encountered variables must be primops (since undefined
variables are detected after parsing). */
if (matchVar(e, name)) {
ATerm primOp = state.primOps.get(name);
if (!primOp)
throw EvalError(format("impossible: undefined variable `%1%'") % aterm2String(name));
int arity;
ATermBlob fun;
if (!matchPrimOpDef(primOp, arity, fun)) abort();
if (arity == 0)
/* !!! backtrace for primop call */
return ((PrimOp) ATgetBlobData(fun)) (state, ATermVector());
else
return makePrimOp(arity, fun, ATempty);
}
/* Function application. */
if (matchCall(e, e1, e2)) {
ATermList formals;
ATerm pos;
/* Evaluate the left-hand side. */
e1 = evalExpr(state, e1);
/* Is it a primop or a function? */
int arity;
ATermBlob fun;
ATermList args;
if (matchPrimOp(e1, arity, fun, args)) {
args = ATinsert(args, e2);
if (ATgetLength(args) == arity) {
/* Put the arguments in a vector in reverse (i.e.,
actual) order. */
ATermVector args2(arity);
for (ATermIterator i(args); i; ++i)
args2[--arity] = *i;
/* !!! backtrace for primop call */
return ((PrimOp) ATgetBlobData((ATermBlob) fun))
(state, args2);
} else
/* Need more arguments, so propagate the primop. */
return makePrimOp(arity, fun, args);
}
else if (matchFunction(e1, formals, e4, pos)) {
e2 = evalExpr(state, e2);
try {
return evalExpr(state, substArgs(state, e4, formals, e2));
} catch (Error & e) {
e.addPrefix(format("while evaluating the function at %1%:\n")
% showPos(pos));
throw;
}
}
else if (matchFunction1(e1, name, e4, pos)) {
try {
ATermMap subs(1);
subs.set(name, e2);
return evalExpr(state, substitute(Substitution(0, &subs), e4));
} catch (Error & e) {
e.addPrefix(format("while evaluating the function at %1%:\n")
% showPos(pos));
throw;
}
}
else throw TypeError(
format("the left-hand side of the function call is neither a function nor a primop (built-in operation) but %1%")
% showType(e1));
}
/* Attribute selection. */
if (matchSelect(e, e1, name)) {
ATerm pos;
string s1 = aterm2String(name);
Expr a = queryAttr(evalExpr(state, e1), s1, pos);
if (!a) throw EvalError(format("attribute `%1%' missing") % s1);
try {
return evalExpr(state, a);
} catch (Error & e) {
e.addPrefix(format("while evaluating the attribute `%1%' at %2%:\n")
% s1 % showPos(pos));
throw;
}
}
/* Mutually recursive sets. */
ATermList rbnds, nrbnds;
if (matchRec(e, rbnds, nrbnds))
return expandRec(e, rbnds, nrbnds);
/* Conditionals. */
if (matchIf(e, e1, e2, e3)) {
if (evalBool(state, e1))
return evalExpr(state, e2);
else
return evalExpr(state, e3);
}
/* Assertions. */
if (matchAssert(e, e1, e2, pos)) {
if (!evalBool(state, e1))
throw AssertionError(format("assertion failed at %1%") % showPos(pos));
return evalExpr(state, e2);
}
/* Withs. */
if (matchWith(e, e1, e2, pos)) {
ATermMap attrs(128); /* !!! */
try {
e1 = evalExpr(state, e1);
queryAllAttrs(e1, attrs);
} catch (Error & e) {
e.addPrefix(format("while evaluating the `with' definitions at %1%:\n")
% showPos(pos));
throw;
}
try {
e2 = substitute(Substitution(0, &attrs), e2);
checkVarDefs(state.primOps, e2);
return evalExpr(state, e2);
} catch (Error & e) {
e.addPrefix(format("while evaluating the `with' body at %1%:\n")
% showPos(pos));
throw;
}
}
/* Generic equality/inequality. Note that the behaviour on
composite data (lists, attribute sets) and functions is
undefined, since the subterms of those terms are not evaluated.
However, we don't want to make (==) strict, because that would
make operations like `big_derivation == null' very slow (unless
we were to evaluate them side-by-side). */
if (matchOpEq(e, e1, e2))
return makeBool(evalExpr(state, e1) == evalExpr(state, e2));
if (matchOpNEq(e, e1, e2))
return makeBool(evalExpr(state, e1) != evalExpr(state, e2));
/* Negation. */
if (matchOpNot(e, e1))
return makeBool(!evalBool(state, e1));
/* Implication. */
if (matchOpImpl(e, e1, e2))
return makeBool(!evalBool(state, e1) || evalBool(state, e2));
/* Conjunction (logical AND). */
if (matchOpAnd(e, e1, e2))
return makeBool(evalBool(state, e1) && evalBool(state, e2));
/* Disjunction (logical OR). */
if (matchOpOr(e, e1, e2))
return makeBool(evalBool(state, e1) || evalBool(state, e2));
/* Attribute set update (//). */
if (matchOpUpdate(e, e1, e2))
return updateAttrs(evalExpr(state, e1), evalExpr(state, e2));
/* Attribute existence test (?). */
if (matchOpHasAttr(e, e1, name)) {
ATermMap attrs(128); /* !!! */
queryAllAttrs(evalExpr(state, e1), attrs);
return makeBool(attrs.get(name) != 0);
}
/* String or path concatenation. */
ATermList es = ATempty;
if (matchOpPlus(e, e1, e2) || matchConcatStrings(e, es)) {
ATermVector args;
if (matchOpPlus(e, e1, e2)) {
/* !!! Awful compatibility hack for `drv + /path'.
According to regular concatenation, /path should be
copied to the store and its store path should be
appended to the string. However, in Nix <= 0.10, /path
was concatenated. So handle that case separately, but
do print out a warning. This code can go in Nix 0.12,
maybe. */
e1 = evalExpr(state, e1);
e2 = evalExpr(state, e2);
ATermList as;
ATerm p;
if (matchAttrs(e1, as) && matchPath(e2, p)) {
static bool haveWarned = false;
warnOnce(haveWarned, format(
"concatenation of a derivation and a path is deprecated, "
"you should write `drv + \"%1%\"' instead of `drv + %1%'")
% aterm2String(p));
PathSet context;
return makeStr(
coerceToString(state, makeSelect(e1, toATerm("outPath")), context)
+ aterm2String(p), context);
}
args.push_back(e1);
args.push_back(e2);
} else
for (ATermIterator i(es); i; ++i) args.push_back(*i);
try {
return concatStrings(state, args);
} catch (Error & e) {
e.addPrefix(format("in a string concatenation:\n"));
throw;
}
}
/* Backwards compatability: subpath operator (~). */
if (matchSubPath(e, e1, e2)) {
static bool haveWarned = false;
warnOnce(haveWarned, "the subpath operator (~) is deprecated, use string concatenation (+) instead");
ATermVector args;
args.push_back(e1);
args.push_back(e2);
return concatStrings(state, args, "/");
}
/* List concatenation. */
if (matchOpConcat(e, e1, e2)) {
try {
ATermList l1 = evalList(state, e1);
ATermList l2 = evalList(state, e2);
return makeList(ATconcat(l1, l2));
} catch (Error & e) {
e.addPrefix(format("in a list concatenation:\n"));
throw;
}
}
/* Barf. */
throw badTerm("invalid expression", e);
}
Expr evalExpr(EvalState & state, Expr e)
{
checkInterrupt();
startNest(nest, lvlVomit,
format("evaluating expression: %1%") % e);
state.nrEvaluated++;
/* Consult the memo table to quickly get the normal form of
previously evaluated expressions. */
Expr nf = state.normalForms.get(e);
if (nf) {
if (nf == makeBlackHole())
throw EvalError("infinite recursion encountered");
state.nrCached++;
return nf;
}
/* Otherwise, evaluate and memoize. */
state.normalForms.set(e, makeBlackHole());
try {
nf = evalExpr2(state, e);
} catch (Error & err) {
debug("removing black hole");
state.normalForms.remove(e);
throw;
}
state.normalForms.set(e, nf);
return nf;
}
Expr evalFile(EvalState & state, const Path & path)
{
startNest(nest, lvlTalkative, format("evaluating file `%1%'") % path);
Expr e = parseExprFromFile(state, path);
try {
return evalExpr(state, e);
} catch (Error & e) {
e.addPrefix(format("while evaluating the file `%1%':\n")
% path);
throw;
}
}
Expr strictEvalExpr(EvalState & state, Expr e, bool canonicalise)
{
e = evalExpr(state, e);
ATermList as;
if (matchAttrs(e, as)) {
ATermList as2 = ATempty;
for (ATermIterator i(as); i; ++i) {
ATerm name; Expr e; ATerm pos;
if (!matchBind(*i, name, e, pos)) abort(); /* can't happen */
as2 = ATinsert(as2, makeBind(name, strictEvalExpr(state, e, canonicalise),
canonicalise ? makeNoPos() : pos));
}
/* !!! sort attributes if canonicalise == true */
return makeAttrs(ATreverse(as2));
}
ATermList es;
if (matchList(e, es)) {
ATermList es2 = ATempty;
for (ATermIterator i(es); i; ++i)
es2 = ATinsert(es2, strictEvalExpr(state, *i, canonicalise));
return makeList(ATreverse(es2));
}
ATermList formals;
ATerm body, pos;
if (matchFunction(e, formals, body, pos)) {
ATermList formals2 = ATempty;
for (ATermIterator i(formals); i; ++i) {
Expr name; ValidValues valids; ATerm dummy;
if (!matchFormal(*i, name, valids, dummy)) abort();
ATermList valids2;
if (matchValidValues(valids, valids2)) {
ATermList valids3 = ATempty;
for (ATermIterator j(valids2); j; ++j)
valids3 = ATinsert(valids3, strictEvalExpr(state, *j, canonicalise));
valids = makeValidValues(ATreverse(valids3));
}
formals2 = ATinsert(formals2, makeFormal(name, valids, dummy));
}
return makeFunction(ATreverse(formals2), body,
canonicalise ? makeNoPos() : pos);
}
return e;
}
/* Yes, this is a really bad idea... */
extern "C" {
unsigned long AT_calcAllocatedSize();
}
void printEvalStats(EvalState & state)
{
bool showStats = getEnv("NIX_SHOW_STATS", "0") != "0";
printMsg(showStats ? lvlInfo : lvlDebug,
format("evaluated %1% expressions, %2% cache hits, %3%%% efficiency, used %4% ATerm bytes")
% state.nrEvaluated % state.nrCached
% ((float) state.nrCached / (float) state.nrEvaluated * 100)
% AT_calcAllocatedSize());
if (showStats)
printATermMapStats();
}
}