Permalink
Browse files

Add primop ‘scopedImport’

‘scopedImport’ works like ‘import’, except that it takes a set of
attributes to be added to the lexical scope of the expression,
essentially extending or overriding the builtin variables.  For
instance, the expression

  scopedImport { x = 1; } ./foo.nix

where foo.nix contains ‘x’, will evaluate to 1.

This has a few applications:

* It allows getting rid of function argument specifications in package
  expressions. For instance, a package expression like:

    { stdenv, fetchurl, libfoo }:

    stdenv.mkDerivation { ... buildInputs = [ libfoo ]; }

  can now we written as just

    stdenv.mkDerivation { ... buildInputs = [ libfoo ]; }

  and imported in all-packages.nix as:

    bar = scopedImport pkgs ./bar.nix;

  So whereas we once had dependencies listed in three places
  (buildInputs, the function, and the call site), they now only need
  to appear in one place.

* It allows overriding builtin functions. For instance, to trace all
  calls to ‘map’:

  let
    overrides = {
      map = f: xs: builtins.trace "map called!" (map f xs);

      # Ensure that our override gets propagated by calls to
      # import/scopedImport.
      import = fn: scopedImport overrides fn;

      scopedImport = attrs: fn: scopedImport (overrides // attrs) fn;

      # Also update ‘builtins’.
      builtins = builtins // overrides;
    };
  in scopedImport overrides ./bla.nix

* Similarly, it allows extending the set of builtin functions. For
  instance, during Nixpkgs/NixOS evaluation, the Nixpkgs library
  functions could be added to the default scope.

There is a downside: calls to scopedImport are not memoized, unlike
import. So importing a file multiple times leads to multiple parsings
/ evaluations. It would be possible to construct the AST only once,
but that would require careful handling of variables/environments.
  • Loading branch information...
edolstra committed May 26, 2014
1 parent f0fdbd0 commit c273c15cb13bb86420dda1e5341a4e19517532b5
@@ -124,6 +124,7 @@ public:
/* 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);
@@ -336,5 +336,4 @@ struct StaticEnv
};
}
@@ -592,7 +592,13 @@ Path resolveExprPath(Path path)
Expr * EvalState::parseExprFromFile(const Path & path)
{
return parse(readFile(path).c_str(), path, dirOf(path), staticBaseEnv);
return parseExprFromFile(path, staticBaseEnv);
}
Expr * EvalState::parseExprFromFile(const Path & path, StaticEnv & staticEnv)
{
return parse(readFile(path).c_str(), path, dirOf(path), staticEnv);
}
@@ -608,7 +614,7 @@ Expr * EvalState::parseExprFromString(const string & s, const Path & basePath)
}
void EvalState::addToSearchPath(const string & s, bool warn)
void EvalState::addToSearchPath(const string & s, bool warn)
{
size_t pos = s.find('=');
string prefix;
@@ -93,6 +93,30 @@ static void prim_import(EvalState & state, const Pos & pos, Value * * args, Valu
}
static void prim_scopedImport(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
PathSet context;
state.forceAttrs(*args[0]);
Path path = resolveExprPath(state.coerceToPath(pos, *args[1], context));
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;
}
startNest(nest, lvlTalkative, format("evaluating file `%1%'") % path);
Expr * e = state.parseExprFromFile(path, staticEnv);
e->eval(state, *env, v);
}
/* Return a string representing the type of the expression. */
static void prim_typeOf(EvalState & state, const Pos & pos, Value * * args, Value & v)
{
@@ -1247,6 +1271,7 @@ void EvalState::createBaseEnv()
// Miscellaneous
addPrimOp("import", 1, prim_import);
addPrimOp("scopedImport", 2, prim_scopedImport);
addPrimOp("__typeOf", 1, prim_typeOf);
addPrimOp("isNull", 1, prim_isNull);
addPrimOp("__isFunction", 1, prim_isFunction);
@@ -0,0 +1 @@
[ 1 2 3 4 5 6 7 8 9 10 ]
@@ -0,0 +1,11 @@
let
overrides = {
import = fn: scopedImport overrides fn;
scopedImport = attrs: fn: scopedImport (overrides // attrs) fn;
builtins = builtins // overrides;
} // import ./lib.nix;
in scopedImport overrides ./imported.nix
@@ -0,0 +1,3 @@
# The function ‘range’ comes from lib.nix and was added to the lexical
# scope by scopedImport.
range 1 5 ++ import ./imported2.nix
@@ -0,0 +1 @@
range 6 10

4 comments on commit c273c15

@copumpkin

This comment has been minimized.

Member

copumpkin replied Feb 1, 2017

@edolstra is the "not memoized" point still true? Thinking of using this and am not sure how much of a performance impact to expect

@globin

This comment has been minimized.

Member

globin replied Feb 16, 2017

Hmm, I just read this by chance and for the first use case I (and @fpletz) actually like having the dependencies explicitly defined as function arguments to make them clear.

@copumpkin

This comment has been minimized.

Member

copumpkin replied Feb 17, 2017

I like that too, but it's useful for other things... 😄

@globin

This comment has been minimized.

Member

globin replied Feb 17, 2017

Yeah, sure!

Please sign in to comment.