* Greatly reduced the amount of stack space used by the Nix expression

evaluator.  This was important because the NixOS expressions started
  to hit 2 MB default stack size on Linux.

  GCC is really dumb about stack space: it just adds up all the local
  variables and temporaries of every scope into one huge stack frame.
  This is really bad for deeply recursive functions.  For instance,
  every `throw Error(format("error message"))' causes a format object
  of a few hundred bytes to be allocated on the stack.  As a result,
  every recursive call to evalExpr2() consumed 4680 bytes.  By
  splitting evalExpr2() and by moving the exception-throwing code out
  of the main functions, evalExpr2() now only consumes 40 bytes.
  Similar for evalExpr().
This commit is contained in:
Eelco Dolstra 2007-02-27 19:10:45 +00:00
parent adce01a8d0
commit 044b6482c1

View file

@ -8,6 +8,10 @@
#include "globals.hh" #include "globals.hh"
#define LocalNoInline(f) static f __attribute__((noinline)); f
#define LocalNoInlineNoReturn(f) static f __attribute__((noinline, noreturn)); f
namespace nix { namespace nix {
@ -29,6 +33,42 @@ void EvalState::addPrimOp(const string & name,
} }
/* 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))
{
throw EvalError(s);
}
LocalNoInlineNoReturn(void throwEvalError(const char * s, const string & s2))
{
throw EvalError(format(s) % s2);
}
LocalNoInlineNoReturn(void throwTypeError(const char * s, const string & s2))
{
throw TypeError(format(s) % s2);
}
LocalNoInline(void addErrorPrefix(Error & e, const char * s))
{
e.addPrefix(s);
}
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 string & s2, const string & s3))
{
e.addPrefix(format(s) % s2 % s3);
}
/* Substitute an argument set into the body of a function. */ /* Substitute an argument set into the body of a function. */
static Expr substArgs(EvalState & state, static Expr substArgs(EvalState & state,
Expr body, ATermList formals, Expr arg) Expr body, ATermList formals, Expr arg)
@ -111,7 +151,7 @@ static Expr substArgs(EvalState & state,
to attributes substituted with selection expressions on the to attributes substituted with selection expressions on the
original set. E.g., e = `rec {x = f x y; y = x;}' becomes `{x = f original set. E.g., e = `rec {x = f x y; y = x;}' becomes `{x = f
(e.x) (e.y); y = e.x;}'. */ (e.x) (e.y); y = e.x;}'. */
ATerm expandRec(ATerm e, ATermList rbnds, ATermList nrbnds) LocalNoInline(ATerm expandRec(ATerm e, ATermList rbnds, ATermList nrbnds))
{ {
ATerm name; ATerm name;
Expr e2; Expr e2;
@ -147,7 +187,7 @@ ATerm expandRec(ATerm e, ATermList rbnds, ATermList nrbnds)
} }
static Expr updateAttrs(Expr e1, Expr e2) LocalNoInline(Expr updateAttrs(Expr e1, Expr e2))
{ {
/* Note: e1 and e2 should be in normal form. */ /* Note: e1 and e2 should be in normal form. */
@ -164,7 +204,7 @@ string evalString(EvalState & state, Expr e, PathSet & context)
e = evalExpr(state, e); e = evalExpr(state, e);
string s; string s;
if (!matchStr(e, s, context)) if (!matchStr(e, s, context))
throw TypeError(format("value is %1% while a string was expected") % showType(e)); throwTypeError("value is %1% while a string was expected", showType(e));
return s; return s;
} }
@ -185,7 +225,7 @@ int evalInt(EvalState & state, Expr e)
e = evalExpr(state, e); e = evalExpr(state, e);
int i; int i;
if (!matchInt(e, i)) if (!matchInt(e, i))
throw TypeError(format("value is %1% while an integer was expected") % showType(e)); throwTypeError("value is %1% while an integer was expected", showType(e));
return i; return i;
} }
@ -195,7 +235,7 @@ bool evalBool(EvalState & state, Expr e)
e = evalExpr(state, e); e = evalExpr(state, e);
if (e == eTrue) return true; if (e == eTrue) return true;
else if (e == eFalse) return false; else if (e == eFalse) return false;
else throw TypeError(format("value is %1% while a boolean was expected") % showType(e)); else throwTypeError("value is %1% while a boolean was expected", showType(e));
} }
@ -204,7 +244,7 @@ ATermList evalList(EvalState & state, Expr e)
e = evalExpr(state, e); e = evalExpr(state, e);
ATermList list; ATermList list;
if (!matchList(e, list)) if (!matchList(e, list))
throw TypeError(format("value is %1% while a list was expected") % showType(e)); throwTypeError("value is %1% while a list was expected", showType(e));
return list; return list;
} }
@ -292,7 +332,7 @@ string coerceToString(EvalState & state, Expr e, PathSet & context,
} }
} }
throw TypeError(format("cannot coerce %1% to a string") % showType(e)); throwTypeError("cannot coerce %1% to a string", showType(e));
} }
@ -362,6 +402,215 @@ Expr autoCallFunction(Expr e, const ATermMap & args)
} }
/* Evaluation of various language constructs. These have been taken
out of evalExpr2 to reduce stack space usage. (GCC is really dumb
about stack space: it just adds up all the local variables and
temporaries of every scope into one huge stack frame. This is
really bad for deeply recursive functions.) */
LocalNoInline(Expr evalVar(EvalState & state, ATerm 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);
}
LocalNoInline(Expr evalCall(EvalState & state, Expr fun, Expr arg))
{
ATermList formals;
ATerm pos, name;
Expr body;
/* Evaluate the left-hand side. */
fun = evalExpr(state, fun);
/* Is it a primop or a function? */
int arity;
ATermBlob funBlob;
ATermList args;
if (matchPrimOp(fun, arity, funBlob, args)) {
args = ATinsert(args, arg);
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(funBlob))
(state, args2);
} else
/* Need more arguments, so propagate the primop. */
return makePrimOp(arity, funBlob, args);
}
else if (matchFunction(fun, formals, body, pos)) {
arg = evalExpr(state, arg);
try {
return evalExpr(state, substArgs(state, body, formals, arg));
} catch (Error & e) {
addErrorPrefix(e, "while evaluating the function at %1%:\n",
showPos(pos));
throw;
}
}
else if (matchFunction1(fun, name, body, pos)) {
try {
ATermMap subs(1);
subs.set(name, arg);
return evalExpr(state, substitute(Substitution(0, &subs), body));
} catch (Error & e) {
addErrorPrefix(e, "while evaluating the function at %1%:\n",
showPos(pos));
throw;
}
}
else throwTypeError(
"the left-hand side of the function call is neither a function nor a primop (built-in operation) but %1%",
showType(fun));
}
LocalNoInline(Expr evalSelect(EvalState & state, Expr e, ATerm name))
{
ATerm pos;
string s = aterm2String(name);
Expr a = queryAttr(evalExpr(state, e), s, pos);
if (!a) throwEvalError("attribute `%1%' missing", s);
try {
return evalExpr(state, a);
} catch (Error & e) {
addErrorPrefix(e, "while evaluating the attribute `%1%' at %2%:\n",
s, showPos(pos));
throw;
}
}
LocalNoInline(Expr evalAssert(EvalState & state, Expr cond, Expr body, ATerm pos))
{
if (!evalBool(state, cond))
throw AssertionError(format("assertion failed at %1%") % showPos(pos));
return evalExpr(state, body);
}
LocalNoInline(Expr evalWith(EvalState & state, Expr defs, Expr body, ATerm pos))
{
ATermMap attrs;
try {
defs = evalExpr(state, defs);
queryAllAttrs(defs, attrs);
} catch (Error & e) {
addErrorPrefix(e, "while evaluating the `with' definitions at %1%:\n",
showPos(pos));
throw;
}
try {
body = substitute(Substitution(0, &attrs), body);
checkVarDefs(state.primOps, body);
return evalExpr(state, body);
} catch (Error & e) {
addErrorPrefix(e, "while evaluating the `with' body at %1%:\n",
showPos(pos));
throw;
}
}
LocalNoInline(Expr evalHasAttr(EvalState & state, Expr e, ATerm name))
{
ATermMap attrs;
queryAllAttrs(evalExpr(state, e), attrs);
return makeBool(attrs.get(name) != 0);
}
LocalNoInline(Expr evalPlusConcat(EvalState & state, Expr e))
{
Expr e1, e2;
ATermList 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 if (matchConcatStrings(e, es))
for (ATermIterator i(es); i; ++i) args.push_back(*i);
try {
return concatStrings(state, args);
} catch (Error & e) {
addErrorPrefix(e, "in a string concatenation:\n");
throw;
}
}
LocalNoInline(Expr evalSubPath(EvalState & state, Expr e1, Expr 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, "/");
}
LocalNoInline(Expr evalOpConcat(EvalState & state, Expr e1, Expr e2))
{
try {
ATermList l1 = evalList(state, e1);
ATermList l2 = evalList(state, e2);
return makeList(ATconcat(l1, l2));
} catch (Error & e) {
addErrorPrefix(e, "in a list concatenation:\n");
throw;
}
}
static char * deepestStack = (char *) -1; /* for measuring stack usage */ static char * deepestStack = (char *) -1; /* for measuring stack usage */
@ -370,7 +619,7 @@ Expr evalExpr2(EvalState & state, Expr e)
char x; char x;
if (&x < deepestStack) deepestStack = &x; if (&x < deepestStack) deepestStack = &x;
Expr e1, e2, e3, e4; Expr e1, e2, e3;
ATerm name, pos; ATerm name, pos;
AFun sym = ATgetAFun(e); AFun sym = ATgetAFun(e);
@ -394,91 +643,13 @@ Expr evalExpr2(EvalState & state, Expr e)
/* Any encountered variables must be primops (since undefined /* Any encountered variables must be primops (since undefined
variables are detected after parsing). */ variables are detected after parsing). */
if (matchVar(e, name)) { if (matchVar(e, name)) return evalVar(state, 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. */ /* Function application. */
if (matchCall(e, e1, e2)) { if (matchCall(e, e1, e2)) return evalCall(state, 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. */ /* Attribute selection. */
if (matchSelect(e, e1, name)) { if (matchSelect(e, e1, name)) return evalSelect(state, 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. */ /* Mutually recursive sets. */
ATermList rbnds, nrbnds; ATermList rbnds, nrbnds;
@ -486,41 +657,14 @@ Expr evalExpr2(EvalState & state, Expr e)
return expandRec(e, rbnds, nrbnds); return expandRec(e, rbnds, nrbnds);
/* Conditionals. */ /* Conditionals. */
if (matchIf(e, e1, e2, e3)) { if (matchIf(e, e1, e2, e3))
if (evalBool(state, e1)) return evalExpr(state, evalBool(state, e1) ? e2 : e3);
return evalExpr(state, e2);
else
return evalExpr(state, e3);
}
/* Assertions. */ /* Assertions. */
if (matchAssert(e, e1, e2, pos)) { if (matchAssert(e, e1, e2, pos)) return evalAssert(state, e1, e2, pos);
if (!evalBool(state, e1))
throw AssertionError(format("assertion failed at %1%") % showPos(pos));
return evalExpr(state, e2);
}
/* Withs. */ /* Withs. */
if (matchWith(e, e1, e2, pos)) { if (matchWith(e, e1, e2, pos)) return evalWith(state, e1, e2, pos);
ATermMap attrs;
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 /* Generic equality/inequality. Note that the behaviour on
composite data (lists, attribute sets) and functions is composite data (lists, attribute sets) and functions is
@ -555,88 +699,31 @@ Expr evalExpr2(EvalState & state, Expr e)
return updateAttrs(evalExpr(state, e1), evalExpr(state, e2)); return updateAttrs(evalExpr(state, e1), evalExpr(state, e2));
/* Attribute existence test (?). */ /* Attribute existence test (?). */
if (matchOpHasAttr(e, e1, name)) { if (matchOpHasAttr(e, e1, name)) return evalHasAttr(state, e1, name);
ATermMap attrs;
queryAllAttrs(evalExpr(state, e1), attrs);
return makeBool(attrs.get(name) != 0);
}
/* String or path concatenation. */ /* String or path concatenation. */
ATermList es = ATempty; if (sym == symOpPlus || sym == symConcatStrings)
if (matchOpPlus(e, e1, e2) || matchConcatStrings(e, es)) { return evalPlusConcat(state, e);
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 (~). */ /* Backwards compatability: subpath operator (~). */
if (matchSubPath(e, e1, e2)) { if (matchSubPath(e, e1, e2)) return evalSubPath(state, 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. */ /* List concatenation. */
if (matchOpConcat(e, e1, e2)) { if (matchOpConcat(e, e1, e2)) return evalOpConcat(state, 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. */ /* Barf. */
throw badTerm("invalid expression", e); abort();
} }
Expr evalExpr(EvalState & state, Expr e) Expr evalExpr(EvalState & state, Expr e)
{ {
checkInterrupt(); checkInterrupt();
#if 0
startNest(nest, lvlVomit, startNest(nest, lvlVomit,
format("evaluating expression: %1%") % e); format("evaluating expression: %1%") % e);
#endif
state.nrEvaluated++; state.nrEvaluated++;
@ -645,7 +732,7 @@ Expr evalExpr(EvalState & state, Expr e)
Expr nf = state.normalForms.get(e); Expr nf = state.normalForms.get(e);
if (nf) { if (nf) {
if (nf == makeBlackHole()) if (nf == makeBlackHole())
throw EvalError("infinite recursion encountered"); throwEvalError("infinite recursion encountered");
state.nrCached++; state.nrCached++;
return nf; return nf;
} }
@ -655,7 +742,6 @@ Expr evalExpr(EvalState & state, Expr e)
try { try {
nf = evalExpr2(state, e); nf = evalExpr2(state, e);
} catch (Error & err) { } catch (Error & err) {
debug("removing black hole");
state.normalForms.remove(e); state.normalForms.remove(e);
throw; throw;
} }