aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/backport.yml2
-rw-r--r--boehmgc-coroutine-sp-fallback.diff77
-rw-r--r--configure.ac6
-rw-r--r--doc/manual/src/command-ref/nix-store.md4
-rw-r--r--doc/manual/src/contributing/hacking.md3
-rw-r--r--doc/manual/src/glossary.md4
-rw-r--r--doc/manual/src/release-notes/rl-2.13.md8
-rw-r--r--doc/manual/src/release-notes/rl-next.md8
-rw-r--r--flake.nix23
-rw-r--r--src/libcmd/installables.cc4
-rw-r--r--src/libcmd/repl.cc8
-rw-r--r--src/libexpr/attr-path.cc2
-rw-r--r--src/libexpr/eval-cache.cc20
-rw-r--r--src/libexpr/eval-inline.hh25
-rw-r--r--src/libexpr/eval.cc584
-rw-r--r--src/libexpr/eval.hh178
-rw-r--r--src/libexpr/flake/flake.cc12
-rw-r--r--src/libexpr/get-drvs.cc24
-rw-r--r--src/libexpr/nixexpr.hh1
-rw-r--r--src/libexpr/parser.y30
-rw-r--r--src/libexpr/primops.cc771
-rw-r--r--src/libexpr/primops/context.cc32
-rw-r--r--src/libexpr/primops/fetchClosure.cc11
-rw-r--r--src/libexpr/primops/fetchMercurial.cc14
-rw-r--r--src/libexpr/primops/fetchTree.cc20
-rw-r--r--src/libexpr/primops/fromTOML.cc2
-rw-r--r--src/libexpr/tests/error_traces.cc1298
-rw-r--r--src/libexpr/tests/primops.cc6
-rw-r--r--src/libmain/shared.cc2
-rw-r--r--src/libstore/daemon.cc3
-rw-r--r--src/libstore/derivations.hh1
-rw-r--r--src/libstore/globals.hh25
-rw-r--r--src/libstore/outputs-spec.cc15
-rw-r--r--src/libstore/path-regex.hh7
-rw-r--r--src/libstore/path.cc6
-rw-r--r--src/libstore/path.hh3
-rw-r--r--src/libstore/realisation.hh2
-rw-r--r--src/libstore/tests/libstoretests.hh23
-rw-r--r--src/libstore/tests/local.mk2
-rw-r--r--src/libstore/tests/outputs-spec.cc7
-rw-r--r--src/libstore/tests/path.cc144
-rw-r--r--src/libutil/error.cc122
-rw-r--r--src/libutil/error.hh18
-rw-r--r--src/libutil/regex-combinators.hh30
-rw-r--r--src/nix-env/user-env.cc4
-rw-r--r--src/nix/bundle.cc6
-rw-r--r--src/nix/eval.cc2
-rw-r--r--src/nix/flake.cc50
-rw-r--r--src/nix/main.cc2
-rw-r--r--src/nix/prefetch.cc16
-rw-r--r--src/nix/show-config.cc31
-rw-r--r--src/nix/upgrade-nix.cc2
-rw-r--r--tests/config.sh5
-rw-r--r--tests/export-graph.sh2
-rw-r--r--tests/lang/eval-okay-context-introspection.exp2
-rw-r--r--tests/lang/eval-okay-context-introspection.nix23
-rw-r--r--tests/lang/eval-okay-readDir.exp2
-rw-r--r--tests/lang/eval-okay-readFileType.exp1
-rw-r--r--tests/lang/eval-okay-readFileType.nix6
l---------tests/lang/readDir/ldir1
l---------tests/lang/readDir/linked1
61 files changed, 2786 insertions, 957 deletions
diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index 9f8d14509..ca5af260f 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -21,7 +21,7 @@ jobs:
fetch-depth: 0
- name: Create backport PRs
# should be kept in sync with `version`
- uses: zeebe-io/backport-action@v1.0.1
+ uses: zeebe-io/backport-action@v1.1.0
with:
# Config README: https://github.com/zeebe-io/backport-action#backport-action
github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/boehmgc-coroutine-sp-fallback.diff b/boehmgc-coroutine-sp-fallback.diff
new file mode 100644
index 000000000..2826486fb
--- /dev/null
+++ b/boehmgc-coroutine-sp-fallback.diff
@@ -0,0 +1,77 @@
+diff --git a/darwin_stop_world.c b/darwin_stop_world.c
+index 3dbaa3fb..36a1d1f7 100644
+--- a/darwin_stop_world.c
++++ b/darwin_stop_world.c
+@@ -352,6 +352,7 @@ GC_INNER void GC_push_all_stacks(void)
+ int nthreads = 0;
+ word total_size = 0;
+ mach_msg_type_number_t listcount = (mach_msg_type_number_t)THREAD_TABLE_SZ;
++ size_t stack_limit;
+ if (!EXPECT(GC_thr_initialized, TRUE))
+ GC_thr_init();
+
+@@ -407,6 +408,19 @@ GC_INNER void GC_push_all_stacks(void)
+ GC_push_all_stack_sections(lo, hi, p->traced_stack_sect);
+ }
+ if (altstack_lo) {
++ // When a thread goes into a coroutine, we lose its original sp until
++ // control flow returns to the thread.
++ // While in the coroutine, the sp points outside the thread stack,
++ // so we can detect this and push the entire thread stack instead,
++ // as an approximation.
++ // We assume that the coroutine has similarly added its entire stack.
++ // This could be made accurate by cooperating with the application
++ // via new functions and/or callbacks.
++ stack_limit = pthread_get_stacksize_np(p->id);
++ if (altstack_lo >= altstack_hi || altstack_lo < altstack_hi - stack_limit) { // sp outside stack
++ altstack_lo = altstack_hi - stack_limit;
++ }
++
+ total_size += altstack_hi - altstack_lo;
+ GC_push_all_stack(altstack_lo, altstack_hi);
+ }
+diff --git a/pthread_stop_world.c b/pthread_stop_world.c
+index b5d71e62..aed7b0bf 100644
+--- a/pthread_stop_world.c
++++ b/pthread_stop_world.c
+@@ -768,6 +768,8 @@ STATIC void GC_restart_handler(int sig)
+ /* world is stopped. Should not fail if it isn't. */
+ GC_INNER void GC_push_all_stacks(void)
+ {
++ size_t stack_limit;
++ pthread_attr_t pattr;
+ GC_bool found_me = FALSE;
+ size_t nthreads = 0;
+ int i;
+@@ -851,6 +853,31 @@ GC_INNER void GC_push_all_stacks(void)
+ hi = p->altstack + p->altstack_size;
+ /* FIXME: Need to scan the normal stack too, but how ? */
+ /* FIXME: Assume stack grows down */
++ } else {
++ if (pthread_getattr_np(p->id, &pattr)) {
++ ABORT("GC_push_all_stacks: pthread_getattr_np failed!");
++ }
++ if (pthread_attr_getstacksize(&pattr, &stack_limit)) {
++ ABORT("GC_push_all_stacks: pthread_attr_getstacksize failed!");
++ }
++ if (pthread_attr_destroy(&pattr)) {
++ ABORT("GC_push_all_stacks: pthread_attr_destroy failed!");
++ }
++ // When a thread goes into a coroutine, we lose its original sp until
++ // control flow returns to the thread.
++ // While in the coroutine, the sp points outside the thread stack,
++ // so we can detect this and push the entire thread stack instead,
++ // as an approximation.
++ // We assume that the coroutine has similarly added its entire stack.
++ // This could be made accurate by cooperating with the application
++ // via new functions and/or callbacks.
++ #ifndef STACK_GROWS_UP
++ if (lo >= hi || lo < hi - stack_limit) { // sp outside stack
++ lo = hi - stack_limit;
++ }
++ #else
++ #error "STACK_GROWS_UP not supported in boost_coroutine2 (as of june 2021), so we don't support it in Nix."
++ #endif
+ }
+ GC_push_all_stack_sections(lo, hi, traced_stack_sect);
+ # ifdef STACK_GROWS_UP
diff --git a/configure.ac b/configure.ac
index 1b0d6fd27..0066bc389 100644
--- a/configure.ac
+++ b/configure.ac
@@ -274,6 +274,12 @@ fi
PKG_CHECK_MODULES([GTEST], [gtest_main])
+# Look for rapidcheck.
+# No pkg-config yet, https://github.com/emil-e/rapidcheck/issues/302
+AC_CHECK_HEADERS([rapidcheck/gtest.h], [], [], [#include <gtest/gtest.h>])
+AC_CHECK_LIB([rapidcheck], [])
+
+
# Look for nlohmann/json.
PKG_CHECK_MODULES([NLOHMANN_JSON], [nlohmann_json >= 3.9])
diff --git a/doc/manual/src/command-ref/nix-store.md b/doc/manual/src/command-ref/nix-store.md
index f6017481c..403cf285d 100644
--- a/doc/manual/src/command-ref/nix-store.md
+++ b/doc/manual/src/command-ref/nix-store.md
@@ -82,8 +82,8 @@ paths. Realisation is a somewhat overloaded term:
produced through substitutes. If there are no (successful)
substitutes, realisation fails.
-[valid]: ../glossary.md#validity
-[substitutes]: ../glossary.md#substitute
+[valid]: ../glossary.md#gloss-validity
+[substitutes]: ../glossary.md#gloss-substitute
The output path of each derivation is printed on standard output. (For
non-derivations argument, the argument itself is printed.)
diff --git a/doc/manual/src/contributing/hacking.md b/doc/manual/src/contributing/hacking.md
index aeb0d41b3..9dbafcc0a 100644
--- a/doc/manual/src/contributing/hacking.md
+++ b/doc/manual/src/contributing/hacking.md
@@ -92,7 +92,8 @@ $ nix develop
The unit-tests for each Nix library (`libexpr`, `libstore`, etc..) are defined
under `src/{library_name}/tests` using the
-[googletest](https://google.github.io/googletest/) framework.
+[googletest](https://google.github.io/googletest/) and
+[rapidcheck](https://github.com/emil-e/rapidcheck) frameworks.
You can run the whole testsuite with `make check`, or the tests for a specific component with `make libfoo-tests_RUN`. Finer-grained filtering is also possible using the [--gtest_filter](https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests) command-line option.
diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md
index 5a49af8dc..6004df833 100644
--- a/doc/manual/src/glossary.md
+++ b/doc/manual/src/glossary.md
@@ -156,6 +156,8 @@
to path `Q`, then `Q` is in the closure of `P`. Further, if `Q`
references `R` then `R` is also in the closure of `P`.
+ [closure]: #gloss-closure
+
- [output path]{#gloss-output-path}\
A [store path] produced by a [derivation].
@@ -172,6 +174,8 @@
- The store path is listed in the Nix database as being valid.
- All paths in the store path's [closure] are valid.
+ [validity]: #gloss-validity
+
- [user environment]{#gloss-user-env}\
An automatically generated store object that consists of a set of
symlinks to “active” applications, i.e., other store paths. These
diff --git a/doc/manual/src/release-notes/rl-2.13.md b/doc/manual/src/release-notes/rl-2.13.md
index 2b79620be..168708113 100644
--- a/doc/manual/src/release-notes/rl-2.13.md
+++ b/doc/manual/src/release-notes/rl-2.13.md
@@ -18,14 +18,18 @@
* Instead of "antiquotation", the more common term [string interpolation](../language/string-interpolation.md) is now used consistently.
Historical release notes were not changed.
+* Error traces have been reworked to provide detailed explanations and more
+ accurate error locations. A short excerpt of the trace is now shown by
+ default when an error occurs.
+
* Allow explicitly selecting outputs in a store derivation installable, just like we can do with other sorts of installables.
For example,
```shell-session
- # nix-build /nix/store/gzaflydcr6sb3567hap9q6srzx8ggdgg-glibc-2.33-78.drv^dev
+ # nix build /nix/store/gzaflydcr6sb3567hap9q6srzx8ggdgg-glibc-2.33-78.drv^dev
```
now works just as
```shell-session
- # nix-build glibc^dev
+ # nix build nixpkgs#glibc^dev
```
does already.
diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md
index 78ae99f4b..8a79703ab 100644
--- a/doc/manual/src/release-notes/rl-next.md
+++ b/doc/manual/src/release-notes/rl-next.md
@@ -1,2 +1,10 @@
# Release X.Y (202?-??-??)
+* A new function `builtins.readFileType` is available. It is similar to
+ `builtins.readDir` but acts on a single file or directory.
+
+* The `builtins.readDir` function has been optimized when encountering not-yet-known
+ file types from POSIX's `readdir`. In such cases the type of each file is/was
+ discovered by making multiple syscalls. This change makes these operations
+ lazy such that these lookups will only be performed if the attribute is used.
+ This optimization affects a minority of filesystems and operating systems.
diff --git a/flake.nix b/flake.nix
index 31d7f1fcf..0325f33b1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -82,7 +82,9 @@
});
configureFlags =
- lib.optionals stdenv.isLinux [
+ [
+ "CXXFLAGS=-I${lib.getDev rapidcheck}/extras/gtest/include"
+ ] ++ lib.optionals stdenv.isLinux [
"--with-boost=${boost}/lib"
"--with-sandbox-shell=${sh}/bin/busybox"
]
@@ -116,6 +118,7 @@
boost
lowdown-nix
gtest
+ rapidcheck
]
++ lib.optionals stdenv.isLinux [libseccomp]
++ lib.optional (stdenv.isLinux || stdenv.isDarwin) libsodium
@@ -128,9 +131,14 @@
});
propagatedDeps =
- [ (boehmgc.override {
+ [ ((boehmgc.override {
enableLargeConfig = true;
+ }).overrideAttrs(o: {
+ patches = (o.patches or []) ++ [
+ ./boehmgc-coroutine-sp-fallback.diff
+ ];
})
+ )
nlohmann_json
];
};
@@ -469,6 +477,10 @@
src = self;
+ configureFlags = [
+ "CXXFLAGS=-I${lib.getDev pkgs.rapidcheck}/extras/gtest/include"
+ ];
+
enableParallelBuilding = true;
nativeBuildInputs = nativeBuildDeps;
@@ -635,6 +647,7 @@
inherit system crossSystem;
overlays = [ self.overlays.default ];
};
+ inherit (nixpkgsCross) lib;
in with commonDeps { pkgs = nixpkgsCross; }; nixpkgsCross.stdenv.mkDerivation {
name = "nix-${version}";
@@ -647,7 +660,11 @@
nativeBuildInputs = nativeBuildDeps;
buildInputs = buildDeps ++ propagatedDeps;
- configureFlags = [ "--sysconfdir=/etc" "--disable-doc-gen" ];
+ configureFlags = [
+ "CXXFLAGS=-I${lib.getDev nixpkgsCross.rapidcheck}/extras/gtest/include"
+ "--sysconfdir=/etc"
+ "--disable-doc-gen"
+ ];
enableParallelBuilding = true;
diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc
index 60d6e9dc0..5090ea6d2 100644
--- a/src/libcmd/installables.cc
+++ b/src/libcmd/installables.cc
@@ -554,7 +554,7 @@ ref<eval_cache::EvalCache> openEvalCache(
auto vFlake = state.allocValue();
flake::callFlake(state, *lockedFlake, *vFlake);
- state.forceAttrs(*vFlake, noPos);
+ state.forceAttrs(*vFlake, noPos, "while parsing cached flake data");
auto aOutputs = vFlake->attrs->get(state.symbols.create("outputs"));
assert(aOutputs);
@@ -618,7 +618,7 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths()
else if (v.type() == nString) {
PathSet context;
- auto s = state->forceString(v, context, noPos);
+ auto s = state->forceString(v, context, noPos, fmt("while evaluating the flake output attribute '%s'", attrPath));
auto storePath = state->store->maybeParseStorePath(s);
if (storePath && context.count(std::string(s))) {
return {{
diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc
index b7f691808..4158439b6 100644
--- a/src/libcmd/repl.cc
+++ b/src/libcmd/repl.cc
@@ -397,7 +397,7 @@ StringSet NixRepl::completePrefix(const std::string & prefix)
Expr * e = parseString(expr);
Value v;
e->eval(*state, *env, v);
- state->forceAttrs(v, noPos);
+ state->forceAttrs(v, noPos, "while evaluating an attrset for the purpose of completion (this error should not be displayed; file an issue?)");
for (auto & i : *v.attrs) {
std::string_view name = state->symbols[i.name];
@@ -590,7 +590,7 @@ bool NixRepl::processLine(std::string line)
const auto [path, line] = [&] () -> std::pair<Path, uint32_t> {
if (v.type() == nPath || v.type() == nString) {
PathSet context;
- auto path = state->coerceToPath(noPos, v, context);
+ auto path = state->coerceToPath(noPos, v, context, "while evaluating the filename to edit");
return {path, 0};
} else if (v.isLambda()) {
auto pos = state->positions[v.lambda.fun->pos];
@@ -839,7 +839,7 @@ void NixRepl::loadFiles()
void NixRepl::addAttrsToScope(Value & attrs)
{
- state->forceAttrs(attrs, [&]() { return attrs.determinePos(noPos); });
+ state->forceAttrs(attrs, [&]() { return attrs.determinePos(noPos); }, "while evaluating an attribute set to be merged in the global scope");
if (displ + attrs.attrs->size() >= envSize)
throw Error("environment full; cannot add more variables");
@@ -944,7 +944,7 @@ std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int m
Bindings::iterator i = v.attrs->find(state->sDrvPath);
PathSet context;
if (i != v.attrs->end())
- str << state->store->printStorePath(state->coerceToStorePath(i->pos, *i->value, context));
+ str << state->store->printStorePath(state->coerceToStorePath(i->pos, *i->value, context, "while evaluating the drvPath of a derivation"));
else
str << "???";
str << "»";
diff --git a/src/libexpr/attr-path.cc b/src/libexpr/attr-path.cc
index 94ab60f9a..7c0705091 100644
--- a/src/libexpr/attr-path.cc
+++ b/src/libexpr/attr-path.cc
@@ -118,7 +118,7 @@ std::pair<std::string, uint32_t> findPackageFilename(EvalState & state, Value &
// FIXME: is it possible to extract the Pos object instead of doing this
// toString + parsing?
- auto pos = state.forceString(*v2);
+ auto pos = state.forceString(*v2, noPos, "while evaluating the 'meta.position' attribute of a derivation");
auto colon = pos.rfind(':');
if (colon == std::string::npos)
diff --git a/src/libexpr/eval-cache.cc b/src/libexpr/eval-cache.cc
index f8c4275a1..1219b2471 100644
--- a/src/libexpr/eval-cache.cc
+++ b/src/libexpr/eval-cache.cc
@@ -385,7 +385,7 @@ Value & AttrCursor::getValue()
if (!_value) {
if (parent) {
auto & vParent = parent->first->getValue();
- root->state.forceAttrs(vParent, noPos);
+ root->state.forceAttrs(vParent, noPos, "while searching for an attribute");
auto attr = vParent.attrs->get(parent->second);
if (!attr)
throw Error("attribute '%s' is unexpectedly missing", getAttrPathStr());
@@ -571,14 +571,14 @@ std::string AttrCursor::getString()
debug("using cached string attribute '%s'", getAttrPathStr());
return s->first;
} else
- root->state.debugThrowLastTrace(TypeError("'%s' is not a string", getAttrPathStr()));
+ root->state.error("'%s' is not a string", getAttrPathStr()).debugThrow<TypeError>();
}
}
auto & v = forceValue();
if (v.type() != nString && v.type() != nPath)
- root->state.debugThrowLastTrace(TypeError("'%s' is not a string but %s", getAttrPathStr(), showType(v.type())));
+ root->state.error("'%s' is not a string but %s", getAttrPathStr()).debugThrow<TypeError>();
return v.type() == nString ? v.string.s : v.path;
}
@@ -613,7 +613,7 @@ string_t AttrCursor::getStringWithContext()
return *s;
}
} else
- root->state.debugThrowLastTrace(TypeError("'%s' is not a string", getAttrPathStr()));
+ root->state.error("'%s' is not a string", getAttrPathStr()).debugThrow<TypeError>();
}
}
@@ -624,7 +624,7 @@ string_t AttrCursor::getStringWithContext()
else if (v.type() == nPath)
return {v.path, {}};
else
- root->state.debugThrowLastTrace(TypeError("'%s' is not a string but %s", getAttrPathStr(), showType(v.type())));
+ root->state.error("'%s' is not a string but %s", getAttrPathStr()).debugThrow<TypeError>();
}
bool AttrCursor::getBool()
@@ -637,14 +637,14 @@ bool AttrCursor::getBool()
debug("using cached Boolean attribute '%s'", getAttrPathStr());
return *b;
} else
- root->state.debugThrowLastTrace(TypeError("'%s' is not a Boolean", getAttrPathStr()));
+ root->state.error("'%s' is not a Boolean", getAttrPathStr()).debugThrow<TypeError>();
}
}
auto & v = forceValue();
if (v.type() != nBool)
- root->state.debugThrowLastTrace(TypeError("'%s' is not a Boolean", getAttrPathStr()));
+ root->state.error("'%s' is not a Boolean", getAttrPathStr()).debugThrow<TypeError>();
return v.boolean;
}
@@ -696,7 +696,7 @@ std::vector<std::string> AttrCursor::getListOfStrings()
std::vector<std::string> res;
for (auto & elem : v.listItems())
- res.push_back(std::string(root->state.forceStringNoCtx(*elem)));
+ res.push_back(std::string(root->state.forceStringNoCtx(*elem, noPos, "while evaluating an attribute for caching")));
if (root->db)
cachedValue = {root->db->setListOfStrings(getKey(), res), res};
@@ -714,14 +714,14 @@ std::vector<Symbol> AttrCursor::getAttrs()
debug("using cached attrset attribute '%s'", getAttrPathStr());
return *attrs;
} else
- root->state.debugThrowLastTrace(TypeError("'%s' is not an attribute set", getAttrPathStr()));
+ root->state.error("'%s' is not an attribute set", getAttrPathStr()).debugThrow<TypeError>();
}
}
auto & v = forceValue();
if (v.type() != nAttrs)
- root->state.debugThrowLastTrace(TypeError("'%s' is not an attribute set", getAttrPathStr()));
+ root->state.error("'%s' is not an attribute set", getAttrPathStr()).debugThrow<TypeError>();
std::vector<Symbol> attrs;
for (auto & attr : *getValue().attrs)
diff --git a/src/libexpr/eval-inline.hh b/src/libexpr/eval-inline.hh
index f2f4ba725..f0da688db 100644
--- a/src/libexpr/eval-inline.hh
+++ b/src/libexpr/eval-inline.hh
@@ -103,33 +103,36 @@ void EvalState::forceValue(Value & v, Callable getPos)
else if (v.isApp())
callFunction(*v.app.left, *v.app.right, v, noPos);
else if (v.isBlackhole())
- throwEvalError(getPos(), "infinite recursion encountered");
+ error("infinite recursion encountered").atPos(getPos()).template debugThrow<EvalError>();
}
[[gnu::always_inline]]
-inline void EvalState::forceAttrs(Value & v, const PosIdx pos)
+inline void EvalState::forceAttrs(Value & v, const PosIdx pos, std::string_view errorCtx)
{
- forceAttrs(v, [&]() { return pos; });
+ forceAttrs(v, [&]() { return pos; }, errorCtx);
}
template <typename Callable>
[[gnu::always_inline]]
-inline void EvalState::forceAttrs(Value & v, Callable getPos)
+inline void EvalState::forceAttrs(Value & v, Callable getPos, std::string_view errorCtx)
{
- forceValue(v, getPos);
- if (v.type() != nAttrs)
- throwTypeError(getPos(), "value is %1% while a set was expected", v);
+ forceValue(v, noPos);
+ if (v.type() != nAttrs) {
+ PosIdx pos = getPos();
+ error("value is %1% while a set was expected", showType(v)).withTrace(pos, errorCtx).debugThrow<TypeError>();
+ }
}
[[gnu::always_inline]]
-inline void EvalState::forceList(Value & v, const PosIdx pos)
+inline void EvalState::forceList(Value & v, const PosIdx pos, std::string_view errorCtx)
{
- forceValue(v, pos);
- if (!v.isList())
- throwTypeError(pos, "value is %1% while a list was expected", v);
+ forceValue(v, noPos);
+ if (!v.isList()) {
+ error("value is %1% while a list was expected", showType(v)).withTrace(pos, errorCtx).debugThrow<TypeError>();
+ }
}
diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 9bc20a502..1828b8c2e 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -11,7 +11,9 @@
#include <algorithm>
#include <chrono>
+#include <iostream>
#include <cstring>
+#include <optional>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
@@ -318,7 +320,7 @@ static Symbol getName(const AttrName & name, EvalState & state, Env & env)
} else {
Value nameValue;
name.expr->eval(state, env, nameValue);
- state.forceStringNoCtx(nameValue);
+ state.forceStringNoCtx(nameValue, noPos, "while evaluating an attribute name");
return state.symbols.create(nameValue.string.s);
}
}
@@ -414,6 +416,44 @@ static Strings parseNixPath(const std::string & s)
return res;
}
+ErrorBuilder & ErrorBuilder::atPos(PosIdx pos)
+{
+ info.errPos = state.positions[pos];
+ return *this;
+}
+
+ErrorBuilder & ErrorBuilder::withTrace(PosIdx pos, const std::string_view text)
+{
+ info.traces.push_front(Trace{ .pos = state.positions[pos], .hint = hintformat(std::string(text)), .frame = false });
+ return *this;
+}
+
+ErrorBuilder & ErrorBuilder::withFrameTrace(PosIdx pos, const std::string_view text)
+{
+ info.traces.push_front(Trace{ .pos = state.positions[pos], .hint = hintformat(std::string(text)), .frame = true });
+ return *this;
+}
+
+ErrorBuilder & ErrorBuilder::withSuggestions(Suggestions & s)
+{
+ info.suggestions = s;
+ return *this;
+}
+
+ErrorBuilder & ErrorBuilder::withFrame(const Env & env, const Expr & expr)
+{
+ // NOTE: This is abusing side-effects.
+ // TODO: check compatibility with nested debugger calls.
+ state.debugTraces.push_front(DebugTrace {
+ .pos = nullptr,
+ .expr = expr,
+ .env = env,
+ .hint = hintformat("Fake frame for debugging purposes"),
+ .isError = true
+ });
+ return *this;
+}
+
EvalState::EvalState(
const Strings & _searchPath,
@@ -646,25 +686,7 @@ void EvalState::addConstant(const std::string & name, Value * v)
Value * EvalState::addPrimOp(const std::string & name,
size_t arity, PrimOpFun primOp)
{
- auto name2 = name.substr(0, 2) == "__" ? name.substr(2) : name;
- auto sym = symbols.create(name2);
-
- /* Hack to make constants lazy: turn them into a application of
- the primop to a dummy value. */
- if (arity == 0) {
- auto vPrimOp = allocValue();
- vPrimOp->mkPrimOp(new PrimOp { .fun = primOp, .arity = 1, .name = name2 });
- Value v;
- v.mkApp(vPrimOp, vPrimOp);
- return addConstant(name, v);
- }
-
- Value * v = allocValue();
- v->mkPrimOp(new PrimOp { .fun = primOp, .arity = arity, .name = name2 });
- staticBaseEnv->vars.emplace_back(symbols.create(name), baseEnvDispl);
- baseEnv.values[baseEnvDispl++] = v;
- baseEnv.values[0]->attrs->push_back(Attr(sym, v));
- return v;
+ return addPrimOp(PrimOp { .fun = primOp, .arity = arity, .name = name });
}
@@ -842,176 +864,14 @@ void EvalState::runDebugRepl(const Error * error, const Env & env, const Expr &
}
}
-/* 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. */
-void EvalState::throwEvalError(const PosIdx pos, const char * s, Env & env, Expr & expr)
-{
- debugThrow(EvalError({
- .msg = hintfmt(s),
- .errPos = positions[pos]
- }), env, expr);
-}
-
-void EvalState::throwEvalError(const PosIdx pos, const char * s)
-{
- debugThrowLastTrace(EvalError({
- .msg = hintfmt(s),
- .errPos = positions[pos]
- }));
-}
-
-void EvalState::throwEvalError(const char * s, const std::string & s2)
-{
- debugThrowLastTrace(EvalError(s, s2));
-}
-
-void EvalState::throwEvalError(const PosIdx pos, const Suggestions & suggestions, const char * s,
- const std::string & s2, Env & env, Expr & expr)
-{
- debugThrow(EvalError(ErrorInfo{
- .msg = hintfmt(s, s2),
- .errPos = positions[pos],
- .suggestions = suggestions,
- }), env, expr);
-}
-
-void EvalState::throwEvalError(const PosIdx pos, const char * s, const std::string & s2)
-{
- debugThrowLastTrace(EvalError({
- .msg = hintfmt(s, s2),
- .errPos = positions[pos]
- }));
-}
-
-void EvalState::throwEvalError(const PosIdx pos, const char * s, const std::string & s2, Env & env, Expr & expr)
-{
- debugThrow(EvalError({
- .msg = hintfmt(s, s2),
- .errPos = positions[pos]
- }), env, expr);
-}
-
-void EvalState::throwEvalError(const char * s, const std::string & s2,
- const std::string & s3)
-{
- debugThrowLastTrace(EvalError({
- .msg = hintfmt(s, s2, s3),
- .errPos = positions[noPos]
- }));
-}
-
-void EvalState::throwEvalError(const PosIdx pos, const char * s, const std::string & s2,
- const std::string & s3)
-{
- debugThrowLastTrace(EvalError({
- .msg = hintfmt(s, s2, s3),
- .errPos = positions[pos]
- }));
-}
-
-void EvalState::throwEvalError(const PosIdx pos, const char * s, const std::string & s2,
- const std::string & s3, Env & env, Expr & expr)
-{
- debugThrow(EvalError({
- .msg = hintfmt(s, s2, s3),
- .errPos = positions[pos]
- }), env, expr);
-}
-
-void EvalState::throwEvalError(const PosIdx p1, const char * s, const Symbol sym, const PosIdx p2, Env & env, Expr & expr)
-{
- // p1 is where the error occurred; p2 is a position mentioned in the message.
- debugThrow(EvalError({
- .msg = hintfmt(s, symbols[sym], positions[p2]),
- .errPos = positions[p1]
- }), env, expr);
-}
-
-void EvalState::throwTypeError(const PosIdx pos, const char * s, const Value & v)
-{
- debugThrowLastTrace(TypeError({
- .msg = hintfmt(s, showType(v)),
- .errPos = positions[pos]
- }));
-}
-
-void EvalState::throwTypeError(const PosIdx pos, const char * s, const Value & v, Env & env, Expr & expr)
-{
- debugThrow(TypeError({
- .msg = hintfmt(s, showType(v)),
- .errPos = positions[pos]
- }), env, expr);
-}
-
-void EvalState::throwTypeError(const PosIdx pos, const char * s)
-{
- debugThrowLastTrace(TypeError({
- .msg = hintfmt(s),
- .errPos = positions[pos]
- }));
-}
-
-void EvalState::throwTypeError(const PosIdx pos, const char * s, const ExprLambda & fun,
- const Symbol s2, Env & env, Expr &expr)
-{
- debugThrow(TypeError({
- .msg = hintfmt(s, fun.showNamePos(*this), symbols[s2]),
- .errPos = positions[pos]
- }), env, expr);
-}
-
-void EvalState::throwTypeError(const PosIdx pos, const Suggestions & suggestions, const char * s,
- const ExprLambda & fun, const Symbol s2, Env & env, Expr &expr)
-{
- debugThrow(TypeError(ErrorInfo {
- .msg = hintfmt(s, fun.showNamePos(*this), symbols[s2]),
- .errPos = positions[pos],
- .suggestions = suggestions,
- }), env, expr);
-}
-
-void EvalState::throwTypeError(const char * s, const Value & v, Env & env, Expr &expr)
-{
- debugThrow(TypeError({
- .msg = hintfmt(s, showType(v)),
- .errPos = positions[expr.getPos()],
- }), env, expr);
-}
-
-void EvalState::throwAssertionError(const PosIdx pos, const char * s, const std::string & s1, Env & env, Expr &expr)
-{
- debugThrow(AssertionError({
- .msg = hintfmt(s, s1),
- .errPos = positions[pos]
- }), env, expr);
-}
-
-void EvalState::throwUndefinedVarError(const PosIdx pos, const char * s, const std::string & s1, Env & env, Expr &expr)
-{
- debugThrow(UndefinedVarError({
- .msg = hintfmt(s, s1),
- .errPos = positions[pos]
- }), env, expr);
-}
-
-void EvalState::throwMissingArgumentError(const PosIdx pos, const char * s, const std::string & s1, Env & env, Expr &expr)
-{
- debugThrow(MissingArgumentError({
- .msg = hintfmt(s, s1),
- .errPos = positions[pos]
- }), env, expr);
-}
-
void EvalState::addErrorTrace(Error & e, const char * s, const std::string & s2) const
{
e.addTrace(nullptr, s, s2);
}
-void EvalState::addErrorTrace(Error & e, const PosIdx pos, const char * s, const std::string & s2) const
+void EvalState::addErrorTrace(Error & e, const PosIdx pos, const char * s, const std::string & s2, bool frame) const
{
- e.addTrace(positions[pos], s, s2);
+ e.addTrace(positions[pos], hintfmt(s, s2), frame);
}
static std::unique_ptr<DebugTraceStacker> makeDebugTraceStacker(
@@ -1088,7 +948,7 @@ inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval)
if (env->type == Env::HasWithExpr) {
if (noEval) return 0;
Value * v = allocValue();
- evalAttrs(*env->up, (Expr *) env->values[0], *v);
+ evalAttrs(*env->up, (Expr *) env->values[0], *v, noPos, "<borked>");
env->values[0] = v;
env->type = Env::HasWithAttrs;
}
@@ -1098,7 +958,7 @@ inline Value * EvalState::lookupVar(Env * env, const ExprVar & var, bool noEval)
return j->value;
}
if (!env->prevWith)
- throwUndefinedVarError(var.pos, "undefined variable '%1%'", symbols[var.name], *env, const_cast<ExprVar&>(var));
+ error("undefined variable '%1%'", symbols[var.name]).atPos(var.pos).withFrame(*env, var).debugThrow<UndefinedVarError>();
for (size_t l = env->prevWith; l; --l, env = env->up) ;
}
}
@@ -1248,7 +1108,7 @@ void EvalState::cacheFile(
// computation.
if (mustBeTrivial &&
!(dynamic_cast<ExprAttrs *>(e)))
- throw EvalError("file '%s' must be an attribute set", path);
+ error("file '%s' must be an attribute set", path).debugThrow<EvalError>();
eval(e, v);
} catch (Error & e) {
addErrorTrace(e, "while evaluating the file '%1%':", resolvedPath);
@@ -1266,31 +1126,31 @@ void EvalState::eval(Expr * e, Value & v)
}
-inline bool EvalState::evalBool(Env & env, Expr * e)
+inline bool EvalState::evalBool(Env & env, Expr * e, const PosIdx pos, std::string_view errorCtx)
{
- Value v;
- e->eval(*this, env, v);
- if (v.type() != nBool)
- throwTypeError(noPos, "value is %1% while a Boolean was expected", v, env, *e);
- return v.boolean;
-}
-
-
-inline bool EvalState::evalBool(Env & env, Expr * e, const PosIdx pos)
-{
- Value v;
- e->eval(*this, env, v);
- if (v.type() != nBool)
- throwTypeError(pos, "value is %1% while a Boolean was expected", v, env, *e);
- return v.boolean;
+ try {
+ Value v;
+ e->eval(*this, env, v);
+ if (v.type() != nBool)
+ error("value is %1% while a Boolean was expected", showType(v)).withFrame(env, *e).debugThrow<TypeError>();
+ return v.boolean;
+ } catch (Error & e) {
+ e.addTrace(positions[pos], errorCtx);
+ throw;
+ }
}
-inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v)
+inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v, const PosIdx pos, std::string_view errorCtx)
{
- e->eval(*this, env, v);
- if (v.type() != nAttrs)
- throwTypeError(noPos, "value is %1% while a set was expected", v, env, *e);
+ try {
+ e->eval(*this, env, v);
+ if (v.type() != nAttrs)
+ error("value is %1% while a set was expected", showType(v)).withFrame(env, *e).debugThrow<TypeError>();
+ } catch (Error & e) {
+ e.addTrace(positions[pos], errorCtx);
+ throw;
+ }
}
@@ -1363,7 +1223,7 @@ void ExprAttrs::eval(EvalState & state, Env & env, Value & v)
Hence we need __overrides.) */
if (hasOverrides) {
Value * vOverrides = (*v.attrs)[overrides->second.displ].value;
- state.forceAttrs(*vOverrides, [&]() { return vOverrides->determinePos(noPos); });
+ state.forceAttrs(*vOverrides, [&]() { return vOverrides->determinePos(noPos); }, "while evaluating the `__overrides` attribute");
Bindings * newBnds = state.allocBindings(v.attrs->capacity() + vOverrides->attrs->size());
for (auto & i : *v.attrs)
newBnds->push_back(i);
@@ -1391,11 +1251,11 @@ void ExprAttrs::eval(EvalState & state, Env & env, Value & v)
state.forceValue(nameVal, i.pos);
if (nameVal.type() == nNull)
continue;
- state.forceStringNoCtx(nameVal);
+ state.forceStringNoCtx(nameVal, i.pos, "while evaluating the name of a dynamic attribute");
auto nameSym = state.symbols.create(nameVal.string.s);
Bindings::iterator j = v.attrs->find(nameSym);
if (j != v.attrs->end())
- state.throwEvalError(i.pos, "dynamic attribute '%1%' already defined at %2%", nameSym, j->pos, env, *this);
+ state.error("dynamic attribute '%1%' already defined at %2%", state.symbols[nameSym], state.positions[j->pos]).atPos(i.pos).withFrame(env, *this).debugThrow<EvalError>();
i.valueExpr->setName(nameSym);
/* Keep sorted order so find can catch duplicates */
@@ -1492,15 +1352,14 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v)
return;
}
} else {
- state.forceAttrs(*vAttrs, pos);
+ state.forceAttrs(*vAttrs, pos, "while selecting an attribute");
if ((j = vAttrs->attrs->find(name)) == vAttrs->attrs->end()) {
std::set<std::string> allAttrNames;
for (auto & attr : *vAttrs->attrs)
allAttrNames.insert(state.symbols[attr.name]);
- state.throwEvalError(
- pos,
- Suggestions::bestMatches(allAttrNames, state.symbols[name]),
- "attribute '%1%' missing", state.symbols[name], env, *this);
+ auto suggestions = Suggestions::bestMatches(allAttrNames, state.symbols[name]);
+ state.error("attribute '%1%' missing", state.symbols[name])
+ .atPos(pos).withSuggestions(suggestions).withFrame(env, *this).debugThrow<EvalError>();
}
}
vAttrs = j->value;
@@ -1595,7 +1454,12 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
if (!lambda.hasFormals())
env2.values[displ++] = args[0];
else {
- forceAttrs(*args[0], pos);
+ try {
+ forceAttrs(*args[0], lambda.pos, "while evaluating the value passed for the lambda argument");
+ } catch (Error & e) {
+ if (pos) e.addTrace(positions[pos], "from call site");
+ throw;
+ }
if (lambda.arg)
env2.values[displ++] = args[0];
@@ -1607,8 +1471,15 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
for (auto & i : lambda.formals->formals) {
auto j = args[0]->attrs->get(i.name);
if (!j) {
- if (!i.def) throwTypeError(pos, "%1% called without required argument '%2%'",
- lambda, i.name, *fun.lambda.env, lambda);
+ if (!i.def) {
+ error("function '%1%' called without required argument '%2%'",
+ (lambda.name ? std::string(symbols[lambda.name]) : "anonymous lambda"),
+ symbols[i.name])
+ .atPos(lambda.pos)
+ .withTrace(pos, "from call site")
+ .withFrame(*fun.lambda.env, lambda)
+ .debugThrow<TypeError>();
+ }
env2.values[displ++] = i.def->maybeThunk(*this, env2);
} else {
attrsUsed++;
@@ -1626,11 +1497,15 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
std::set<std::string> formalNames;
for (auto & formal : lambda.formals->formals)
formalNames.insert(symbols[formal.name]);
- throwTypeError(
- pos,
- Suggestions::bestMatches(formalNames, symbols[i.name]),
- "%1% called with unexpected argument '%2%'",
- lambda, i.name, *fun.lambda.env, lambda);
+ auto suggestions = Suggestions::bestMatches(formalNames, symbols[i.name]);
+ error("function '%1%' called with unexpected argument '%2%'",
+ (lambda.name ? std::string(symbols[lambda.name]) : "anonymous lambda"),
+ symbols[i.name])
+ .atPos(lambda.pos)
+ .withTrace(pos, "from call site")
+ .withSuggestions(suggestions)
+ .withFrame(*fun.lambda.env, lambda)
+ .debugThrow<TypeError>();
}
abort(); // can't happen
}
@@ -1653,11 +1528,15 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
lambda.body->eval(*this, env2, vCur);
} catch (Error & e) {
if (loggerSettings.showTrace.get()) {
- addErrorTrace(e, lambda.pos, "while calling %s",
- (lambda.name
- ? concatStrings("'", symbols[lambda.name], "'")
- : "anonymous lambda"));
- addErrorTrace(e, pos, "while evaluating call site%s", "");
+ addErrorTrace(
+ e,
+ lambda.pos,
+ "while calling %s",
+ lambda.name
+ ? concatStrings("'", symbols[lambda.name], "'")
+ : "anonymous lambda",
+ true);
+ if (pos) addErrorTrace(e, pos, "from call site%s", "", true);
}
throw;
}
@@ -1676,9 +1555,17 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
return;
} else {
/* We have all the arguments, so call the primop. */
+ auto name = vCur.primOp->name;
+
nrPrimOpCalls++;
- if (countCalls) primOpCalls[vCur.primOp->name]++;
- vCur.primOp->fun(*this, pos, args, vCur);
+ if (countCalls) primOpCalls[name]++;
+
+ try {
+ vCur.primOp->fun(*this, noPos, args, vCur);
+ } catch (Error & e) {
+ addErrorTrace(e, pos, "while calling the '%1%' builtin", name);
+ throw;
+ }
nrArgs -= argsLeft;
args += argsLeft;
@@ -1713,9 +1600,20 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
for (size_t i = 0; i < argsLeft; ++i)
vArgs[argsDone + i] = args[i];
+ auto name = primOp->primOp->name;
nrPrimOpCalls++;
- if (countCalls) primOpCalls[primOp->primOp->name]++;
- primOp->primOp->fun(*this, pos, vArgs, vCur);
+ if (countCalls) primOpCalls[name]++;
+
+ try {
+ // TODO:
+ // 1. Unify this and above code. Heavily redundant.
+ // 2. Create a fake env (arg1, arg2, etc.) and a fake expr (arg1: arg2: etc: builtins.name arg1 arg2 etc)
+ // so the debugger allows to inspect the wrong parameters passed to the builtin.
+ primOp->primOp->fun(*this, noPos, vArgs, vCur);
+ } catch (Error & e) {
+ addErrorTrace(e, pos, "while calling the '%1%' builtin", name);
+ throw;
+ }
nrArgs -= argsLeft;
args += argsLeft;
@@ -1728,14 +1626,18 @@ void EvalState::callFunction(Value & fun, size_t nrArgs, Value * * args, Value &
heap-allocate a copy and use that instead. */
Value * args2[] = {allocValue(), args[0]};
*args2[0] = vCur;
- /* !!! Should we use the attr pos here? */
- callFunction(*functor->value, 2, args2, vCur, pos);
+ try {
+ callFunction(*functor->value, 2, args2, vCur, functor->pos);
+ } catch (Error & e) {
+ e.addTrace(positions[pos], "while calling a functor (an attribute set with a '__functor' attribute)");
+ throw;
+ }
nrArgs--;
args++;
}
else
- throwTypeError(pos, "attempt to call something which is not a function but %1%", vCur);
+ error("attempt to call something which is not a function but %1%", showType(vCur)).atPos(pos).debugThrow<TypeError>();
}
vRes = vCur;
@@ -1799,13 +1701,12 @@ void EvalState::autoCallFunction(Bindings & args, Value & fun, Value & res)
if (j != args.end()) {
attrs.insert(*j);
} else if (!i.def) {
- throwMissingArgumentError(i.pos, R"(cannot evaluate a function that has an argument without a value ('%1%')
-
+ error(R"(cannot evaluate a function that has an argument without a value ('%1%')
Nix attempted to evaluate a function as a top level expression; in
this case it must have its arguments supplied either by default
values, or passed explicitly with '--arg' or '--argstr'. See
-https://nixos.org/manual/nix/stable/language/constructs.html#functions.)", symbols[i.name],
- *fun.lambda.env, *fun.lambda.fun);
+https://nixos.org/manual/nix/stable/language/constructs.html#functions.)", symbols[i.name])
+ .atPos(i.pos).withFrame(*fun.lambda.env, *fun.lambda.fun).debugThrow<MissingArgumentError>();
}
}
}
@@ -1828,16 +1729,17 @@ void ExprWith::eval(EvalState & state, Env & env, Value & v)
void ExprIf::eval(EvalState & state, Env & env, Value & v)
{
- (state.evalBool(env, cond, pos) ? then : else_)->eval(state, env, v);
+ // We cheat in the parser, and pass the position of the condition as the position of the if itself.
+ (state.evalBool(env, cond, pos, "while evaluating a branch condition") ? then : else_)->eval(state, env, v);
}
void ExprAssert::eval(EvalState & state, Env & env, Value & v)
{
- if (!state.evalBool(env, cond, pos)) {
+ if (!state.evalBool(env, cond, pos, "in the condition of the assert statement")) {
std::ostringstream out;
cond->show(state.symbols, out);
- state.throwAssertionError(pos, "assertion '%1%' failed", out.str(), env, *this);
+ state.error("assertion '%1%' failed", out.str()).atPos(pos).withFrame(env, *this).debugThrow<AssertionError>();
}
body->eval(state, env, v);
}
@@ -1845,7 +1747,7 @@ void ExprAssert::eval(EvalState & state, Env & env, Value & v)
void ExprOpNot::eval(EvalState & state, Env & env, Value & v)
{
- v.mkBool(!state.evalBool(env, e));
+ v.mkBool(!state.evalBool(env, e, noPos, "in the argument of the not operator")); // XXX: FIXME: !
}
@@ -1853,7 +1755,7 @@ void ExprOpEq::eval(EvalState & state, Env & env, Value & v)
{
Value v1; e1->eval(state, env, v1);
Value v2; e2->eval(state, env, v2);
- v.mkBool(state.eqValues(v1, v2));
+ v.mkBool(state.eqValues(v1, v2, pos, "while testing two values for equality"));
}
@@ -1861,33 +1763,33 @@ void ExprOpNEq::eval(EvalState & state, Env & env, Value & v)
{
Value v1; e1->eval(state, env, v1);
Value v2; e2->eval(state, env, v2);
- v.mkBool(!state.eqValues(v1, v2));
+ v.mkBool(!state.eqValues(v1, v2, pos, "while testing two values for inequality"));
}
void ExprOpAnd::eval(EvalState & state, Env & env, Value & v)
{
- v.mkBool(state.evalBool(env, e1, pos) && state.evalBool(env, e2, pos));
+ v.mkBool(state.evalBool(env, e1, pos, "in the left operand of the AND (&&) operator") && state.evalBool(env, e2, pos, "in the right operand of the AND (&&) operator"));
}
void ExprOpOr::eval(EvalState & state, Env & env, Value & v)
{
- v.mkBool(state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos));
+ v.mkBool(state.evalBool(env, e1, pos, "in the left operand of the OR (||) operator") || state.evalBool(env, e2, pos, "in the right operand of the OR (||) operator"));
}
void ExprOpImpl::eval(EvalState & state, Env & env, Value & v)
{
- v.mkBool(!state.evalBool(env, e1, pos) || state.evalBool(env, e2, pos));
+ v.mkBool(!state.evalBool(env, e1, pos, "in the left operand of the IMPL (->) operator") || state.evalBool(env, e2, pos, "in the right operand of the IMPL (->) operator"));
}
void ExprOpUpdate::eval(EvalState & state, Env & env, Value & v)
{
Value v1, v2;
- state.evalAttrs(env, e1, v1);
- state.evalAttrs(env, e2, v2);
+ state.evalAttrs(env, e1, v1, pos, "in the left operand of the update (//) operator");
+ state.evalAttrs(env, e2, v2, pos, "in the right operand of the update (//) operator");
state.nrOpUpdates++;
@@ -1926,18 +1828,18 @@ 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);
+ state.concatLists(v, 2, lists, pos, "while evaluating one of the elements to concatenate");
}
-void EvalState::concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos)
+void EvalState::concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos, std::string_view errorCtx)
{
nrListConcats++;
Value * nonEmpty = 0;
size_t len = 0;
for (size_t n = 0; n < nrLists; ++n) {
- forceList(*lists[n], pos);
+ forceList(*lists[n], pos, errorCtx);
auto l = lists[n]->listSize();
len += l;
if (l) nonEmpty = lists[n];
@@ -2014,20 +1916,22 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
nf = n;
nf += vTmp.fpoint;
} else
- state.throwEvalError(i_pos, "cannot add %1% to an integer", showType(vTmp), env, *this);
+ state.error("cannot add %1% to an integer", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow<EvalError>();
} else if (firstType == nFloat) {
if (vTmp.type() == nInt) {
nf += vTmp.integer;
} else if (vTmp.type() == nFloat) {
nf += vTmp.fpoint;
} else
- state.throwEvalError(i_pos, "cannot add %1% to a float", showType(vTmp), env, *this);
+ state.error("cannot add %1% to a float", showType(vTmp)).atPos(i_pos).withFrame(env, *this).debugThrow<EvalError>();
} else {
if (s.empty()) s.reserve(es->size());
/* skip canonization of first path, which would only be not
canonized in the first place if it's coming from a ./${foo} type
path */
- auto part = state.coerceToString(i_pos, vTmp, context, false, firstType == nString, !first);
+ auto part = state.coerceToString(i_pos, vTmp, context,
+ "while evaluating a path segment",
+ false, firstType == nString, !first);
sSize += part->size();
s.emplace_back(std::move(part));
}
@@ -2041,7 +1945,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
v.mkFloat(nf);
else if (firstType == nPath) {
if (!context.empty())
- state.throwEvalError(pos, "a string that refers to a store path cannot be appended to a path", env, *this);
+ state.error("a string that refers to a store path cannot be appended to a path").atPos(pos).withFrame(env, *this).debugThrow<EvalError>();
v.mkPath(canonPath(str()));
} else
v.mkStringMove(c_str(), context);
@@ -2091,33 +1995,47 @@ void EvalState::forceValueDeep(Value & v)
}
-NixInt EvalState::forceInt(Value & v, const PosIdx pos)
+NixInt EvalState::forceInt(Value & v, const PosIdx pos, std::string_view errorCtx)
{
- forceValue(v, pos);
- if (v.type() != nInt)
- throwTypeError(pos, "value is %1% while an integer was expected", v);
-
- return v.integer;
+ try {
+ forceValue(v, pos);
+ if (v.type() != nInt)
+ error("value is %1% while an integer was expected", showType(v)).debugThrow<TypeError>();
+ return v.integer;
+ } catch (Error & e) {
+ e.addTrace(positions[pos], errorCtx);
+ throw;
+ }
}
-NixFloat EvalState::forceFloat(Value & v, const PosIdx pos)
+NixFloat EvalState::forceFloat(Value & v, const PosIdx pos, std::string_view errorCtx)
{
- forceValue(v, pos);
- if (v.type() == nInt)
- return v.integer;
- else if (v.type() != nFloat)
- throwTypeError(pos, "value is %1% while a float was expected", v);
- return v.fpoint;
+ try {
+ forceValue(v, pos);
+ if (v.type() == nInt)
+ return v.integer;
+ else if (v.type() != nFloat)
+ error("value is %1% while a float was expected", showType(v)).debugThrow<TypeError>();
+ return v.fpoint;
+ } catch (Error & e) {
+ e.addTrace(positions[pos], errorCtx);
+ throw;
+ }
}
-bool EvalState::forceBool(Value & v, const PosIdx pos)
+bool EvalState::forceBool(Value & v, const PosIdx pos, std::string_view errorCtx)
{
- forceValue(v, pos);
- if (v.type() != nBool)
- throwTypeError(pos, "value is %1% while a Boolean was expected", v);
- return v.boolean;
+ try {
+ forceValue(v, pos);
+ if (v.type() != nBool)
+ error("value is %1% while a Boolean was expected", showType(v)).debugThrow<TypeError>();
+ return v.boolean;
+ } catch (Error & e) {
+ e.addTrace(positions[pos], errorCtx);
+ throw;
+ }
}
@@ -2127,21 +2045,30 @@ bool EvalState::isFunctor(Value & fun)
}
-void EvalState::forceFunction(Value & v, const PosIdx pos)
+void EvalState::forceFunction(Value & v, const PosIdx pos, std::string_view errorCtx)
{
- forceValue(v, pos);
- if (v.type() != nFunction && !isFunctor(v))
- throwTypeError(pos, "value is %1% while a function was expected", v);
+ try {
+ forceValue(v, pos);
+ if (v.type() != nFunction && !isFunctor(v))
+ error("value is %1% while a function was expected", showType(v)).debugThrow<TypeError>();
+ } catch (Error & e) {
+ e.addTrace(positions[pos], errorCtx);
+ throw;
+ }
}
-std::string_view EvalState::forceString(Value & v, const PosIdx pos)
+std::string_view EvalState::forceString(Value & v, const PosIdx pos, std::string_view errorCtx)
{
- forceValue(v, pos);
- if (v.type() != nString) {
- throwTypeError(pos, "value is %1% while a string was expected", v);
+ try {
+ forceValue(v, pos);
+ if (v.type() != nString)
+ error("value is %1% while a string was expected", showType(v)).debugThrow<TypeError>();
+ return v.string.s;
+ } catch (Error & e) {
+ e.addTrace(positions[pos], errorCtx);
+ throw;
}
- return v.string.s;
}
@@ -2164,24 +2091,19 @@ NixStringContext Value::getContext(const Store & store)
}
-std::string_view EvalState::forceString(Value & v, PathSet & context, const PosIdx pos)
+std::string_view EvalState::forceString(Value & v, PathSet & context, const PosIdx pos, std::string_view errorCtx)
{
- auto s = forceString(v, pos);
+ auto s = forceString(v, pos, errorCtx);
copyContext(v, context);
return s;
}
-std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos)
+std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos, std::string_view errorCtx)
{
- auto s = forceString(v, pos);
+ auto s = forceString(v, pos, errorCtx);
if (v.string.context) {
- if (pos)
- throwEvalError(pos, "the string '%1%' is not allowed to refer to a store path (such as '%2%')",
- v.string.s, v.string.context[0]);
- else
- throwEvalError("the string '%1%' is not allowed to refer to a store path (such as '%2%')",
- v.string.s, v.string.context[0]);
+ error("the string '%1%' is not allowed to refer to a store path (such as '%2%')", v.string.s, v.string.context[0]).withTrace(pos, errorCtx).debugThrow<EvalError>();
}
return s;
}
@@ -2205,14 +2127,16 @@ std::optional<std::string> EvalState::tryAttrsToString(const PosIdx pos, Value &
if (i != v.attrs->end()) {
Value v1;
callFunction(*i->value, v, v1, pos);
- return coerceToString(pos, v1, context, coerceMore, copyToStore).toOwned();
+ return coerceToString(pos, v1, context,
+ "while evaluating the result of the `__toString` attribute",
+ coerceMore, copyToStore).toOwned();
}
return {};
}
-BackedStringView EvalState::coerceToString(const PosIdx pos, Value & v, PathSet & context,
- bool coerceMore, bool copyToStore, bool canonicalizePath)
+BackedStringView EvalState::coerceToString(const PosIdx pos, Value &v, PathSet &context,
+ std::string_view errorCtx, bool coerceMore, bool copyToStore, bool canonicalizePath)
{
forceValue(v, pos);
@@ -2235,13 +2159,23 @@ BackedStringView EvalState::coerceToString(const PosIdx pos, Value & v, PathSet
if (maybeString)
return std::move(*maybeString);
auto i = v.attrs->find(sOutPath);
- if (i == v.attrs->end())
- throwTypeError(pos, "cannot coerce a set to a string");
- return coerceToString(pos, *i->value, context, coerceMore, copyToStore);
+ if (i == v.attrs->end()) {
+ error("cannot coerce %1% to a string", showType(v))
+ .withTrace(pos, errorCtx)
+ .debugThrow<TypeError>();
+ }
+ return coerceToString(pos, *i->value, context, errorCtx,
+ coerceMore, copyToStore, canonicalizePath);
}
- if (v.type() == nExternal)
- return v.external->coerceToString(positions[pos], context, coerceMore, copyToStore);
+ if (v.type() == nExternal) {
+ try {
+ return v.external->coerceToString(positions[pos], context, coerceMore, copyToStore);
+ } catch (Error & e) {
+ e.addTrace(nullptr, errorCtx);
+ throw;
+ }
+ }
if (coerceMore) {
/* Note that `false' is represented as an empty string for
@@ -2255,7 +2189,14 @@ BackedStringView EvalState::coerceToString(const PosIdx pos, Value & v, PathSet
if (v.isList()) {
std::string result;
for (auto [n, v2] : enumerate(v.listItems())) {
- result += *coerceToString(pos, *v2, context, coerceMore, copyToStore);
+ try {
+ result += *coerceToString(noPos, *v2, context,
+ "while evaluating one element of the list",
+ coerceMore, copyToStore, canonicalizePath);
+ } catch (Error & e) {
+ e.addTrace(positions[pos], errorCtx);
+ throw;
+ }
if (n < v.listSize() - 1
/* !!! not quite correct */
&& (!v2->isList() || v2->listSize() != 0))
@@ -2265,14 +2206,16 @@ BackedStringView EvalState::coerceToString(const PosIdx pos, Value & v, PathSet
}
}
- throwTypeError(pos, "cannot coerce %1% to a string", v);
+ error("cannot coerce %1% to a string", showType(v))
+ .withTrace(pos, errorCtx)
+ .debugThrow<TypeError>();
}
StorePath EvalState::copyPathToStore(PathSet & context, const Path & path)
{
if (nix::isDerivation(path))
- throwEvalError("file names are not allowed to end in '%1%'", drvExtension);
+ error("file names are not allowed to end in '%1%'", drvExtension).debugThrow<EvalError>();
auto dstPath = [&]() -> StorePath
{
@@ -2293,28 +2236,25 @@ StorePath EvalState::copyPathToStore(PathSet & context, const Path & path)
}
-Path EvalState::coerceToPath(const PosIdx pos, Value & v, PathSet & context)
+Path EvalState::coerceToPath(const PosIdx pos, Value & v, PathSet & context, std::string_view errorCtx)
{
- auto path = coerceToString(pos, v, context, false, false).toOwned();
+ auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned();
if (path == "" || path[0] != '/')
- throwEvalError(pos, "string '%1%' doesn't represent an absolute path", path);
+ error("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow<EvalError>();
return path;
}
-StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, PathSet & context)
+StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, PathSet & context, std::string_view errorCtx)
{
- auto path = coerceToString(pos, v, context, false, false).toOwned();
+ auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned();
if (auto storePath = store->maybeParseStorePath(path))
return *storePath;
- throw EvalError({
- .msg = hintfmt("path '%1%' is not in the Nix store", path),
- .errPos = positions[pos]
- });
+ error("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow<EvalError>();
}
-bool EvalState::eqValues(Value & v1, Value & v2)
+bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx)
{
forceValue(v1, noPos);
forceValue(v2, noPos);
@@ -2334,7 +2274,6 @@ bool EvalState::eqValues(Value & v1, Value & v2)
if (v1.type() != v2.type()) return false;
switch (v1.type()) {
-
case nInt:
return v1.integer == v2.integer;
@@ -2353,7 +2292,7 @@ bool EvalState::eqValues(Value & v1, Value & v2)
case nList:
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;
+ if (!eqValues(*v1.listElems()[n], *v2.listElems()[n], pos, errorCtx)) return false;
return true;
case nAttrs: {
@@ -2363,7 +2302,7 @@ bool EvalState::eqValues(Value & v1, Value & 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);
+ return eqValues(*i->value, *j->value, pos, errorCtx);
}
if (v1.attrs->size() != v2.attrs->size()) return false;
@@ -2371,7 +2310,7 @@ bool EvalState::eqValues(Value & v1, Value & v2)
/* 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))
+ if (i->name != j->name || !eqValues(*i->value, *j->value, pos, errorCtx))
return false;
return true;
@@ -2388,9 +2327,7 @@ bool EvalState::eqValues(Value & v1, Value & v2)
return v1.fpoint == v2.fpoint;
default:
- throwEvalError("cannot compare %1% with %2%",
- showType(v1),
- showType(v2));
+ error("cannot compare %1% with %2%", showType(v1), showType(v2)).withTrace(pos, errorCtx).debugThrow<EvalError>();
}
}
@@ -2517,8 +2454,7 @@ void EvalState::printStats()
std::string ExternalValueBase::coerceToString(const Pos & pos, PathSet & context, bool copyMore, bool copyToStore) const
{
throw TypeError({
- .msg = hintfmt("cannot coerce %1% to a string", showType()),
- .errPos = pos
+ .msg = hintfmt("cannot coerce %1% to a string", showType())
});
}
diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh
index df6ac431d..e4d5906bd 100644
--- a/src/libexpr/eval.hh
+++ b/src/libexpr/eval.hh
@@ -86,6 +86,43 @@ struct DebugTrace {
void debugError(Error * e, Env & env, Expr & expr);
+class ErrorBuilder
+{
+ private:
+ EvalState & state;
+ ErrorInfo info;
+
+ ErrorBuilder(EvalState & s, ErrorInfo && i): state(s), info(i) { }
+
+ public:
+ template<typename... Args>
+ [[nodiscard, gnu::noinline]]
+ static ErrorBuilder * create(EvalState & s, const Args & ... args)
+ {
+ return new ErrorBuilder(s, ErrorInfo { .msg = hintfmt(args...) });
+ }
+
+ [[nodiscard, gnu::noinline]]
+ ErrorBuilder & atPos(PosIdx pos);
+
+ [[nodiscard, gnu::noinline]]
+ ErrorBuilder & withTrace(PosIdx pos, const std::string_view text);
+
+ [[nodiscard, gnu::noinline]]
+ ErrorBuilder & withFrameTrace(PosIdx pos, const std::string_view text);
+
+ [[nodiscard, gnu::noinline]]
+ ErrorBuilder & withSuggestions(Suggestions & s);
+
+ [[nodiscard, gnu::noinline]]
+ ErrorBuilder & withFrame(const Env & e, const Expr & ex);
+
+ template<class ErrorType>
+ [[gnu::noinline, gnu::noreturn]]
+ void debugThrow();
+};
+
+
class EvalState : public std::enable_shared_from_this<EvalState>
{
public:
@@ -145,29 +182,38 @@ public:
template<class E>
[[gnu::noinline, gnu::noreturn]]
- void debugThrow(E && error, const Env & env, const Expr & expr)
+ void debugThrowLastTrace(E && error)
{
- if (debugRepl)
- runDebugRepl(&error, env, expr);
-
- throw std::move(error);
+ debugThrow(error, nullptr, nullptr);
}
template<class E>
[[gnu::noinline, gnu::noreturn]]
- void debugThrowLastTrace(E && e)
+ void debugThrow(E && error, const Env * env, const Expr * expr)
{
- // Call this in the situation where Expr and Env are inaccessible.
- // The debugger will start in the last context that's in the
- // DebugTrace stack.
- if (debugRepl && !debugTraces.empty()) {
- const DebugTrace & last = debugTraces.front();
- runDebugRepl(&e, last.env, last.expr);
+ if (debugRepl && ((env && expr) || !debugTraces.empty())) {
+ if (!env || !expr) {
+ const DebugTrace & last = debugTraces.front();
+ env = &last.env;
+ expr = &last.expr;
+ }
+ runDebugRepl(&error, *env, *expr);
}
- throw std::move(e);
+ throw std::move(error);
}
+ // This is dangerous, but gets in line with the idea that error creation and
+ // throwing should not allocate on the stack of hot functions.
+ // as long as errors are immediately thrown, it works.
+ ErrorBuilder * errorBuilder;
+
+ template<typename... Args>
+ [[nodiscard, gnu::noinline]]
+ ErrorBuilder & error(const Args & ... args) {
+ errorBuilder = ErrorBuilder::create(*this, args...);
+ return *errorBuilder;
+ }
private:
SrcToStore srcToStore;
@@ -282,8 +328,8 @@ public:
/* 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 PosIdx pos);
- inline void evalAttrs(Env & env, Expr * e, Value & v);
+ inline bool evalBool(Env & env, Expr * e, const PosIdx pos, std::string_view errorCtx);
+ inline void evalAttrs(Env & env, Expr * e, Value & v, const PosIdx pos, std::string_view errorCtx);
/* 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
@@ -299,89 +345,25 @@ public:
void forceValueDeep(Value & v);
/* Force `v', and then verify that it has the expected type. */
- NixInt forceInt(Value & v, const PosIdx pos);
- NixFloat forceFloat(Value & v, const PosIdx pos);
- bool forceBool(Value & v, const PosIdx pos);
+ NixInt forceInt(Value & v, const PosIdx pos, std::string_view errorCtx);
+ NixFloat forceFloat(Value & v, const PosIdx pos, std::string_view errorCtx);
+ bool forceBool(Value & v, const PosIdx pos, std::string_view errorCtx);
- void forceAttrs(Value & v, const PosIdx pos);
+ void forceAttrs(Value & v, const PosIdx pos, std::string_view errorCtx);
template <typename Callable>
- inline void forceAttrs(Value & v, Callable getPos);
-
- inline void forceList(Value & v, const PosIdx pos);
- void forceFunction(Value & v, const PosIdx pos); // either lambda or primop
- std::string_view forceString(Value & v, const PosIdx pos = noPos);
- std::string_view forceString(Value & v, PathSet & context, const PosIdx pos = noPos);
- std::string_view forceStringNoCtx(Value & v, const PosIdx pos = noPos);
-
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const PosIdx pos, const char * s);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const PosIdx pos, const char * s,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const char * s, const std::string & s2);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const PosIdx pos, const char * s, const std::string & s2);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const char * s, const std::string & s2,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const PosIdx pos, const char * s, const std::string & s2,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const char * s, const std::string & s2, const std::string & s3,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const PosIdx pos, const char * s, const std::string & s2, const std::string & s3,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const PosIdx pos, const char * s, const std::string & s2, const std::string & s3);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const char * s, const std::string & s2, const std::string & s3);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const PosIdx pos, const Suggestions & suggestions, const char * s, const std::string & s2,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwEvalError(const PosIdx p1, const char * s, const Symbol sym, const PosIdx p2,
- Env & env, Expr & expr);
+ inline void forceAttrs(Value & v, Callable getPos, std::string_view errorCtx);
- [[gnu::noinline, gnu::noreturn]]
- void throwTypeError(const PosIdx pos, const char * s, const Value & v);
- [[gnu::noinline, gnu::noreturn]]
- void throwTypeError(const PosIdx pos, const char * s, const Value & v,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwTypeError(const PosIdx pos, const char * s);
- [[gnu::noinline, gnu::noreturn]]
- void throwTypeError(const PosIdx pos, const char * s,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwTypeError(const PosIdx pos, const char * s, const ExprLambda & fun, const Symbol s2,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwTypeError(const PosIdx pos, const Suggestions & suggestions, const char * s, const ExprLambda & fun, const Symbol s2,
- Env & env, Expr & expr);
- [[gnu::noinline, gnu::noreturn]]
- void throwTypeError(const char * s, const Value & v,
- Env & env, Expr & expr);
-
- [[gnu::noinline, gnu::noreturn]]
- void throwAssertionError(const PosIdx pos, const char * s, const std::string & s1,
- Env & env, Expr & expr);
-
- [[gnu::noinline, gnu::noreturn]]
- void throwUndefinedVarError(const PosIdx pos, const char * s, const std::string & s1,
- Env & env, Expr & expr);
-
- [[gnu::noinline, gnu::noreturn]]
- void throwMissingArgumentError(const PosIdx pos, const char * s, const std::string & s1,
- Env & env, Expr & expr);
+ inline void forceList(Value & v, const PosIdx pos, std::string_view errorCtx);
+ void forceFunction(Value & v, const PosIdx pos, std::string_view errorCtx); // either lambda or primop
+ std::string_view forceString(Value & v, const PosIdx pos, std::string_view errorCtx);
+ std::string_view forceString(Value & v, PathSet & context, const PosIdx pos, std::string_view errorCtx);
+ std::string_view forceStringNoCtx(Value & v, const PosIdx pos, std::string_view errorCtx);
[[gnu::noinline]]
void addErrorTrace(Error & e, const char * s, const std::string & s2) const;
[[gnu::noinline]]
- void addErrorTrace(Error & e, const PosIdx pos, const char * s, const std::string & s2) const;
+ void addErrorTrace(Error & e, const PosIdx pos, const char * s, const std::string & s2, bool frame = false) const;
public:
/* Return true iff the value `v' denotes a derivation (i.e. a
@@ -396,6 +378,7 @@ public:
booleans and lists to a string. If `copyToStore' is set,
referenced paths are copied to the Nix store as a side effect. */
BackedStringView coerceToString(const PosIdx pos, Value & v, PathSet & context,
+ std::string_view errorCtx,
bool coerceMore = false, bool copyToStore = true,
bool canonicalizePath = true);
@@ -404,10 +387,10 @@ public:
/* 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 PosIdx pos, Value & v, PathSet & context);
+ Path coerceToPath(const PosIdx pos, Value & v, PathSet & context, std::string_view errorCtx);
/* Like coerceToPath, but the result must be a store path. */
- StorePath coerceToStorePath(const PosIdx pos, Value & v, PathSet & context);
+ StorePath coerceToStorePath(const PosIdx pos, Value & v, PathSet & context, std::string_view errorCtx);
public:
@@ -467,7 +450,7 @@ 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 eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_view errorCtx);
bool isFunctor(Value & fun);
@@ -502,7 +485,7 @@ public:
void mkThunk_(Value & v, Expr * expr);
void mkPos(Value & v, PosIdx pos);
- void concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos);
+ void concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos, std::string_view errorCtx);
/* Print statistics. */
void printStats();
@@ -665,6 +648,13 @@ extern EvalSettings evalSettings;
static const std::string corepkgsPrefix{"/__corepkgs__/"};
+template<class ErrorType>
+void ErrorBuilder::debugThrow()
+{
+ // NOTE: We always use the -LastTrace version as we push the new trace in withFrame()
+ state.debugThrowLastTrace(ErrorType(info));
+}
+
}
#include "eval-inline.hh"
diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc
index 105d32467..336eb274d 100644
--- a/src/libexpr/flake/flake.cc
+++ b/src/libexpr/flake/flake.cc
@@ -259,28 +259,28 @@ static Flake getFlake(
if (setting.value->type() == nString)
flake.config.settings.emplace(
state.symbols[setting.name],
- std::string(state.forceStringNoCtx(*setting.value, setting.pos)));
+ std::string(state.forceStringNoCtx(*setting.value, setting.pos, "")));
else if (setting.value->type() == nPath) {
PathSet emptyContext = {};
flake.config.settings.emplace(
state.symbols[setting.name],
- state.coerceToString(setting.pos, *setting.value, emptyContext, false, true, true) .toOwned());
+ state.coerceToString(setting.pos, *setting.value, emptyContext, "", false, true, true) .toOwned());
}
else if (setting.value->type() == nInt)
flake.config.settings.emplace(
state.symbols[setting.name],
- state.forceInt(*setting.value, setting.pos));
+ state.forceInt(*setting.value, setting.pos, ""));
else if (setting.value->type() == nBool)
flake.config.settings.emplace(
state.symbols[setting.name],
- Explicit<bool> { state.forceBool(*setting.value, setting.pos) });
+ Explicit<bool> { state.forceBool(*setting.value, setting.pos, "") });
else if (setting.value->type() == nList) {
std::vector<std::string> ss;
for (auto elem : setting.value->listItems()) {
if (elem->type() != nString)
throw TypeError("list element in flake configuration setting '%s' is %s while a string is expected",
state.symbols[setting.name], showType(*setting.value));
- ss.emplace_back(state.forceStringNoCtx(*elem, setting.pos));
+ ss.emplace_back(state.forceStringNoCtx(*elem, setting.pos, ""));
}
flake.config.settings.emplace(state.symbols[setting.name], ss);
}
@@ -741,7 +741,7 @@ void callFlake(EvalState & state,
static void prim_getFlake(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- std::string flakeRefS(state.forceStringNoCtx(*args[0], pos));
+ std::string flakeRefS(state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.getFlake"));
auto flakeRef = parseFlakeRef(flakeRefS, {}, true);
if (evalSettings.pureEval && !flakeRef.input.isLocked())
throw Error("cannot call 'getFlake' on unlocked flake reference '%s', at %s (use --impure to override)", flakeRefS, state.positions[pos]);
diff --git a/src/libexpr/get-drvs.cc b/src/libexpr/get-drvs.cc
index 5ad5d1fd4..1602fbffb 100644
--- a/src/libexpr/get-drvs.cc
+++ b/src/libexpr/get-drvs.cc
@@ -51,7 +51,7 @@ std::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);
+ name = state->forceStringNoCtx(*i->value, noPos, "while evaluating the 'name' attribute of a derivation");
}
return name;
}
@@ -61,7 +61,7 @@ std::string DrvInfo::querySystem() const
{
if (system == "" && attrs) {
auto i = attrs->find(state->sSystem);
- system = i == attrs->end() ? "unknown" : state->forceStringNoCtx(*i->value, i->pos);
+ system = i == attrs->end() ? "unknown" : state->forceStringNoCtx(*i->value, i->pos, "while evaluating the 'system' attribute of a derivation");
}
return system;
}
@@ -75,7 +75,7 @@ std::optional<StorePath> DrvInfo::queryDrvPath() const
if (i == attrs->end())
drvPath = {std::nullopt};
else
- drvPath = {state->coerceToStorePath(i->pos, *i->value, context)};
+ drvPath = {state->coerceToStorePath(i->pos, *i->value, context, "while evaluating the 'drvPath' attribute of a derivation")};
}
return drvPath.value_or(std::nullopt);
}
@@ -95,7 +95,7 @@ StorePath DrvInfo::queryOutPath() const
Bindings::iterator i = attrs->find(state->sOutPath);
PathSet context;
if (i != attrs->end())
- outPath = state->coerceToStorePath(i->pos, *i->value, context);
+ outPath = state->coerceToStorePath(i->pos, *i->value, context, "while evaluating the output path of a derivation");
}
if (!outPath)
throw UnimplementedError("CA derivations are not yet supported");
@@ -109,23 +109,23 @@ DrvInfo::Outputs DrvInfo::queryOutputs(bool withPaths, bool onlyOutputsToInstall
/* Get the ‘outputs’ list. */
Bindings::iterator i;
if (attrs && (i = attrs->find(state->sOutputs)) != attrs->end()) {
- state->forceList(*i->value, i->pos);
+ state->forceList(*i->value, i->pos, "while evaluating the 'outputs' attribute of a derivation");
/* For each output... */
for (auto elem : i->value->listItems()) {
- std::string output(state->forceStringNoCtx(*elem, i->pos));
+ std::string output(state->forceStringNoCtx(*elem, i->pos, "while evaluating the name of an output of a derivation"));
if (withPaths) {
/* Evaluate the corresponding set. */
Bindings::iterator out = attrs->find(state->symbols.create(output));
if (out == attrs->end()) continue; // FIXME: throw error?
- state->forceAttrs(*out->value, i->pos);
+ state->forceAttrs(*out->value, i->pos, "while evaluating an output of a derivation");
/* 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.emplace(output, state->coerceToStorePath(outPath->pos, *outPath->value, context));
+ outputs.emplace(output, state->coerceToStorePath(outPath->pos, *outPath->value, context, "while evaluating an output path of a derivation"));
} else
outputs.emplace(output, std::nullopt);
}
@@ -137,7 +137,7 @@ DrvInfo::Outputs DrvInfo::queryOutputs(bool withPaths, bool onlyOutputsToInstall
return outputs;
Bindings::iterator i;
- if (attrs && (i = attrs->find(state->sOutputSpecified)) != attrs->end() && state->forceBool(*i->value, i->pos)) {
+ if (attrs && (i = attrs->find(state->sOutputSpecified)) != attrs->end() && state->forceBool(*i->value, i->pos, "while evaluating the 'outputSpecified' attribute of a derivation")) {
Outputs result;
auto out = outputs.find(queryOutputName());
if (out == outputs.end())
@@ -169,7 +169,7 @@ std::string DrvInfo::queryOutputName() const
{
if (outputName == "" && attrs) {
Bindings::iterator i = attrs->find(state->sOutputName);
- outputName = i != attrs->end() ? state->forceStringNoCtx(*i->value) : "";
+ outputName = i != attrs->end() ? state->forceStringNoCtx(*i->value, noPos, "while evaluating the output name of a derivation") : "";
}
return outputName;
}
@@ -181,7 +181,7 @@ Bindings * DrvInfo::getMeta()
if (!attrs) return 0;
Bindings::iterator a = attrs->find(state->sMeta);
if (a == attrs->end()) return 0;
- state->forceAttrs(*a->value, a->pos);
+ state->forceAttrs(*a->value, a->pos, "while evaluating the 'meta' attribute of a derivation");
meta = a->value->attrs;
return meta;
}
@@ -382,7 +382,7 @@ static void getDerivations(EvalState & state, Value & vIn,
`recurseForDerivations = true' attribute. */
if (i->value->type() == nAttrs) {
Bindings::iterator j = i->value->attrs->find(state.sRecurseForDerivations);
- if (j != i->value->attrs->end() && state.forceBool(*j->value, j->pos))
+ if (j != i->value->attrs->end() && state.forceBool(*j->value, j->pos, "while evaluating the attribute `recurseForDerivations`"))
getDerivations(state, *i->value, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures);
}
}
diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh
index ac7ce021e..ffe67f97d 100644
--- a/src/libexpr/nixexpr.hh
+++ b/src/libexpr/nixexpr.hh
@@ -8,7 +8,6 @@
#include "error.hh"
#include "chunked-vector.hh"
-
namespace nix {
diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y
index e07909f8e..ffb364a90 100644
--- a/src/libexpr/parser.y
+++ b/src/libexpr/parser.y
@@ -400,21 +400,21 @@ expr_op
| '-' expr_op %prec NEGATE { $$ = new ExprCall(CUR_POS, 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 ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$1, $3}); }
- | expr_op LEQ expr_op { $$ = new ExprOpNot(new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$3, $1})); }
- | expr_op '>' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__lessThan")), {$3, $1}); }
- | expr_op GEQ expr_op { $$ = new ExprOpNot(new ExprCall(CUR_POS, 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 '<' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__lessThan")), {$1, $3}); }
+ | expr_op LEQ expr_op { $$ = new ExprOpNot(new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__lessThan")), {$3, $1})); }
+ | expr_op '>' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__lessThan")), {$3, $1}); }
+ | expr_op GEQ expr_op { $$ = new ExprOpNot(new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__lessThan")), {$1, $3})); }
+ | expr_op AND expr_op { $$ = new ExprOpAnd(makeCurPos(@2, data), $1, $3); }
+ | expr_op OR expr_op { $$ = new ExprOpOr(makeCurPos(@2, data), $1, $3); }
+ | expr_op IMPL expr_op { $$ = new ExprOpImpl(makeCurPos(@2, data), $1, $3); }
+ | expr_op UPDATE expr_op { $$ = new ExprOpUpdate(makeCurPos(@2, data), $1, $3); }
| expr_op '?' attrpath { $$ = new ExprOpHasAttr($1, *$3); }
| expr_op '+' expr_op
- { $$ = new ExprConcatStrings(CUR_POS, false, new std::vector<std::pair<PosIdx, Expr *>>({{makeCurPos(@1, data), $1}, {makeCurPos(@3, data), $3}})); }
- | expr_op '-' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__sub")), {$1, $3}); }
- | expr_op '*' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__mul")), {$1, $3}); }
- | expr_op '/' expr_op { $$ = new ExprCall(CUR_POS, new ExprVar(data->symbols.create("__div")), {$1, $3}); }
- | expr_op CONCAT expr_op { $$ = new ExprOpConcatLists(CUR_POS, $1, $3); }
+ { $$ = new ExprConcatStrings(makeCurPos(@2, data), false, new std::vector<std::pair<PosIdx, Expr *> >({{makeCurPos(@1, data), $1}, {makeCurPos(@3, data), $3}})); }
+ | expr_op '-' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__sub")), {$1, $3}); }
+ | expr_op '*' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__mul")), {$1, $3}); }
+ | expr_op '/' expr_op { $$ = new ExprCall(makeCurPos(@2, data), new ExprVar(data->symbols.create("__div")), {$1, $3}); }
+ | expr_op CONCAT expr_op { $$ = new ExprOpConcatLists(makeCurPos(@2, data), $1, $3); }
| expr_app
;
@@ -782,13 +782,13 @@ Path EvalState::findFile(SearchPath & searchPath, const std::string_view path, c
if (hasPrefix(path, "nix/"))
return concatStrings(corepkgsPrefix, path.substr(4));
- debugThrowLastTrace(ThrownError({
+ debugThrow(ThrownError({
.msg = hintfmt(evalSettings.pureEval
? "cannot look up '<%s>' in pure evaluation mode (use '--impure' to override)"
: "file '%s' was not found in the Nix search path (add it using $NIX_PATH or -I)",
path),
.errPos = positions[pos]
- }));
+ }), 0, 0);
}
diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc
index 080892cbd..c6f41c4ca 100644
--- a/src/libexpr/primops.cc
+++ b/src/libexpr/primops.cc
@@ -114,15 +114,7 @@ static Path realisePath(EvalState & state, const PosIdx pos, Value & v, const Re
{
PathSet context;
- auto path = [&]()
- {
- try {
- return state.coerceToPath(pos, v, context);
- } catch (Error & e) {
- e.addTrace(state.positions[pos], "while realising the context of a path");
- throw;
- }
- }();
+ auto path = state.coerceToPath(noPos, v, context, "while realising the context of a path");
try {
StringMap rewrites = state.realiseContext(context);
@@ -209,9 +201,9 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v
, "/"), **state.vImportedDrvToDerivation);
}
- state.forceFunction(**state.vImportedDrvToDerivation, pos);
+ state.forceFunction(**state.vImportedDrvToDerivation, pos, "while evaluating imported-drv-to-derivation.nix.gen.hh");
v.mkApp(*state.vImportedDrvToDerivation, w);
- state.forceAttrs(v, pos);
+ state.forceAttrs(v, pos, "while calling imported-drv-to-derivation.nix.gen.hh");
}
else if (path == corepkgsPrefix + "fetchurl.nix") {
@@ -224,7 +216,7 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v
if (!vScope)
state.evalFile(path, v);
else {
- state.forceAttrs(*vScope, pos);
+ state.forceAttrs(*vScope, pos, "while evaluating the first argument passed to builtins.scopedImport");
Env * env = &state.allocEnv(vScope->attrs->size());
env->up = &state.baseEnv;
@@ -329,7 +321,7 @@ void prim_importNative(EvalState & state, const PosIdx pos, Value * * args, Valu
{
auto path = realisePath(state, pos, *args[0]);
- std::string sym(state.forceStringNoCtx(*args[1], pos));
+ std::string sym(state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.importNative"));
void *handle = dlopen(path.c_str(), RTLD_LAZY | RTLD_LOCAL);
if (!handle)
@@ -354,28 +346,26 @@ void prim_importNative(EvalState & state, const PosIdx pos, Value * * args, Valu
/* Execute a program and parse its output */
void prim_exec(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceList(*args[0], pos);
+ state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.exec");
auto elems = args[0]->listElems();
auto count = args[0]->listSize();
if (count == 0)
- state.debugThrowLastTrace(EvalError({
- .msg = hintfmt("at least one argument to 'exec' required"),
- .errPos = state.positions[pos]
- }));
+ state.error("at least one argument to 'exec' required").atPos(pos).debugThrow<EvalError>();
PathSet context;
- auto program = state.coerceToString(pos, *elems[0], context, false, false).toOwned();
+ auto program = state.coerceToString(pos, *elems[0], context,
+ "while evaluating the first element of the argument passed to builtins.exec",
+ false, false).toOwned();
Strings commandArgs;
for (unsigned int i = 1; i < args[0]->listSize(); ++i) {
- commandArgs.push_back(state.coerceToString(pos, *elems[i], context, false, false).toOwned());
+ commandArgs.push_back(
+ state.coerceToString(pos, *elems[i], context,
+ "while evaluating an element of the argument passed to builtins.exec",
+ false, false).toOwned());
}
try {
auto _ = state.realiseContext(context); // FIXME: Handle CA derivations
} catch (InvalidPathError & e) {
- state.debugThrowLastTrace(EvalError({
- .msg = hintfmt("cannot execute '%1%', since path '%2%' is not valid",
- program, e.path),
- .errPos = state.positions[pos]
- }));
+ state.error("cannot execute '%1%', since path '%2%' is not valid", program, e.path).atPos(pos).debugThrow<EvalError>();
}
auto output = runProgram(program, true, commandArgs);
@@ -383,18 +373,17 @@ void prim_exec(EvalState & state, const PosIdx pos, Value * * args, Value & v)
try {
parsed = state.parseExprFromString(std::move(output), "/");
} catch (Error & e) {
- e.addTrace(state.positions[pos], "While parsing the output from '%1%'", program);
+ e.addTrace(state.positions[pos], "while parsing the output from '%1%'", program);
throw;
}
try {
state.eval(parsed, v);
} catch (Error & e) {
- e.addTrace(state.positions[pos], "While evaluating the output from '%1%'", program);
+ e.addTrace(state.positions[pos], "while evaluating the output from '%1%'", program);
throw;
}
}
-
/* Return a string representing the type of the expression. */
static void prim_typeOf(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
@@ -545,42 +534,69 @@ static RegisterPrimOp primop_isPath({
.fun = prim_isPath,
});
+template<typename Callable>
+ static inline void withExceptionContext(Trace trace, Callable&& func)
+{
+ try
+ {
+ func();
+ }
+ catch(Error & e)
+ {
+ e.pushTrace(trace);
+ throw;
+ }
+}
+
struct CompareValues
{
EvalState & state;
+ const PosIdx pos;
+ const std::string_view errorCtx;
- CompareValues(EvalState & state) : state(state) { };
+ CompareValues(EvalState & state, const PosIdx pos, const std::string_view && errorCtx) : state(state), pos(pos), errorCtx(errorCtx) { };
bool operator () (Value * v1, Value * v2) const
{
- if (v1->type() == nFloat && v2->type() == nInt)
- return v1->fpoint < v2->integer;
- if (v1->type() == nInt && v2->type() == nFloat)
- return v1->integer < v2->fpoint;
- if (v1->type() != v2->type())
- state.debugThrowLastTrace(EvalError("cannot compare %1% with %2%", showType(*v1), showType(*v2)));
- switch (v1->type()) {
- case nInt:
- return v1->integer < v2->integer;
- case nFloat:
- return v1->fpoint < v2->fpoint;
- case nString:
- return strcmp(v1->string.s, v2->string.s) < 0;
- case nPath:
- return strcmp(v1->path, v2->path) < 0;
- case nList:
- // Lexicographic comparison
- for (size_t i = 0;; i++) {
- if (i == v2->listSize()) {
- return false;
- } else if (i == v1->listSize()) {
- return true;
- } else if (!state.eqValues(*v1->listElems()[i], *v2->listElems()[i])) {
- return (*this)(v1->listElems()[i], v2->listElems()[i]);
+ return (*this)(v1, v2, errorCtx);
+ }
+
+ bool operator () (Value * v1, Value * v2, std::string_view errorCtx) const
+ {
+ try {
+ if (v1->type() == nFloat && v2->type() == nInt)
+ return v1->fpoint < v2->integer;
+ if (v1->type() == nInt && v2->type() == nFloat)
+ return v1->integer < v2->fpoint;
+ if (v1->type() != v2->type())
+ state.error("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow<EvalError>();
+ switch (v1->type()) {
+ case nInt:
+ return v1->integer < v2->integer;
+ case nFloat:
+ return v1->fpoint < v2->fpoint;
+ case nString:
+ return strcmp(v1->string.s, v2->string.s) < 0;
+ case nPath:
+ return strcmp(v1->path, v2->path) < 0;
+ case nList:
+ // Lexicographic comparison
+ for (size_t i = 0;; i++) {
+ if (i == v2->listSize()) {
+ return false;
+ } else if (i == v1->listSize()) {
+ return true;
+ } else if (!state.eqValues(*v1->listElems()[i], *v2->listElems()[i], pos, errorCtx)) {
+ return (*this)(v1->listElems()[i], v2->listElems()[i], "while comparing two list elements");
+ }
}
- }
- default:
- state.debugThrowLastTrace(EvalError("cannot compare %1% with %2%", showType(*v1), showType(*v2)));
+ default:
+ state.error("cannot compare %s with %s; values of that type are incomparable", showType(*v1), showType(*v2)).debugThrow<EvalError>();
+ }
+ } catch (Error & e) {
+ if (!errorCtx.empty())
+ e.addTrace(nullptr, errorCtx);
+ throw;
}
}
};
@@ -595,105 +611,67 @@ typedef std::list<Value *> ValueList;
static Bindings::iterator getAttr(
EvalState & state,
- std::string_view funcName,
Symbol attrSym,
Bindings * attrSet,
- const PosIdx pos)
+ std::string_view errorCtx)
{
Bindings::iterator value = attrSet->find(attrSym);
if (value == attrSet->end()) {
- hintformat errorMsg = hintfmt(
- "attribute '%s' missing for call to '%s'",
- state.symbols[attrSym],
- funcName
- );
-
- auto aPos = attrSet->pos;
- if (!aPos) {
- state.debugThrowLastTrace(TypeError({
- .msg = errorMsg,
- .errPos = state.positions[pos],
- }));
- } else {
- auto e = TypeError({
- .msg = errorMsg,
- .errPos = state.positions[aPos],
- });
-
- // Adding another trace for the function name to make it clear
- // which call received wrong arguments.
- e.addTrace(state.positions[pos], hintfmt("while invoking '%s'", funcName));
- state.debugThrowLastTrace(e);
- }
+ state.error("attribute '%s' missing", state.symbols[attrSym]).withTrace(noPos, errorCtx).debugThrow<TypeError>();
}
-
return value;
}
static void prim_genericClosure(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceAttrs(*args[0], pos);
+ state.forceAttrs(*args[0], noPos, "while evaluating the first argument passed to builtins.genericClosure");
/* Get the start set. */
- Bindings::iterator startSet = getAttr(
- state,
- "genericClosure",
- state.sStartSet,
- args[0]->attrs,
- pos
- );
+ Bindings::iterator startSet = getAttr(state, state.sStartSet, args[0]->attrs, "in the attrset passed as argument to builtins.genericClosure");
- state.forceList(*startSet->value, pos);
+ state.forceList(*startSet->value, noPos, "while evaluating the 'startSet' attribute passed as argument to builtins.genericClosure");
ValueList workSet;
for (auto elem : startSet->value->listItems())
workSet.push_back(elem);
- /* Get the operator. */
- Bindings::iterator op = getAttr(
- state,
- "genericClosure",
- state.sOperator,
- args[0]->attrs,
- pos
- );
+ if (startSet->value->listSize() == 0) {
+ v = *startSet->value;
+ return;
+ }
- state.forceValue(*op->value, pos);
+ /* Get the operator. */
+ Bindings::iterator op = getAttr(state, state.sOperator, args[0]->attrs, "in the attrset passed as argument to builtins.genericClosure");
+ state.forceFunction(*op->value, noPos, "while evaluating the 'operator' attribute passed as argument to builtins.genericClosure");
- /* Construct the closure by applying the operator to element of
+ /* Construct the closure by applying the operator to elements 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.
- auto cmp = CompareValues(state);
+ auto cmp = CompareValues(state, noPos, "while comparing the `key` attributes of two genericClosure elements");
std::set<Value *, decltype(cmp)> doneKeys(cmp);
while (!workSet.empty()) {
Value * e = *(workSet.begin());
workSet.pop_front();
- state.forceAttrs(*e, pos);
+ state.forceAttrs(*e, noPos, "while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure");
- Bindings::iterator key =
- e->attrs->find(state.sKey);
- if (key == e->attrs->end())
- state.debugThrowLastTrace(EvalError({
- .msg = hintfmt("attribute 'key' required"),
- .errPos = state.positions[pos]
- }));
- state.forceValue(*key->value, pos);
+ Bindings::iterator key = getAttr(state, state.sKey, e->attrs, "in one of the attrsets generated by (or initially passed to) builtins.genericClosure");
+ state.forceValue(*key->value, noPos);
if (!doneKeys.insert(key->value).second) continue;
res.push_back(e);
/* Call the `operator' function with `e' as argument. */
- Value call;
- call.mkApp(op->value, e);
- state.forceList(call, pos);
+ Value newElements;
+ state.callFunction(*op->value, 1, &e, newElements, noPos);
+ state.forceList(newElements, noPos, "while evaluating the return value of the `operator` passed to builtins.genericClosure");
/* Add the values returned by the operator to the work set. */
- for (auto elem : call.listItems()) {
- state.forceValue(*elem, pos);
+ for (auto elem : newElements.listItems()) {
+ state.forceValue(*elem, noPos); // "while evaluating one one of the elements returned by the `operator` passed to builtins.genericClosure");
workSet.push_back(elem);
}
}
@@ -761,7 +739,7 @@ static RegisterPrimOp primop_break({
throw Error(ErrorInfo{
.level = lvlInfo,
.msg = hintfmt("quit the debugger"),
- .errPos = state.positions[noPos],
+ .errPos = nullptr,
});
}
}
@@ -780,7 +758,8 @@ static RegisterPrimOp primop_abort({
.fun = [](EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- auto s = state.coerceToString(pos, *args[0], context).toOwned();
+ auto s = state.coerceToString(pos, *args[0], context,
+ "while evaluating the error message passed to builtins.abort").toOwned();
state.debugThrowLastTrace(Abort("evaluation aborted with the following error message: '%1%'", s));
}
});
@@ -798,7 +777,8 @@ static RegisterPrimOp primop_throw({
.fun = [](EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- auto s = state.coerceToString(pos, *args[0], context).toOwned();
+ auto s = state.coerceToString(pos, *args[0], context,
+ "while evaluating the error message passed to builtin.throw").toOwned();
state.debugThrowLastTrace(ThrownError(s));
}
});
@@ -810,7 +790,10 @@ static void prim_addErrorContext(EvalState & state, const PosIdx pos, Value * *
v = *args[1];
} catch (Error & e) {
PathSet context;
- e.addTrace(nullptr, state.coerceToString(pos, *args[0], context).toOwned());
+ auto message = state.coerceToString(pos, *args[0], context,
+ "while evaluating the error message passed to builtins.addErrorContext",
+ false, false).toOwned();
+ e.addTrace(nullptr, message, true);
throw;
}
}
@@ -823,7 +806,8 @@ static RegisterPrimOp primop_addErrorContext(RegisterPrimOp::Info {
static void prim_ceil(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto value = state.forceFloat(*args[0], args[0]->determinePos(pos));
+ auto value = state.forceFloat(*args[0], args[0]->determinePos(pos),
+ "while evaluating the first argument passed to builtins.ceil");
v.mkInt(ceil(value));
}
@@ -842,7 +826,7 @@ static RegisterPrimOp primop_ceil({
static void prim_floor(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto value = state.forceFloat(*args[0], args[0]->determinePos(pos));
+ auto value = state.forceFloat(*args[0], args[0]->determinePos(pos), "while evaluating the first argument passed to builtins.floor");
v.mkInt(floor(value));
}
@@ -916,7 +900,7 @@ static RegisterPrimOp primop_tryEval({
/* Return an environment variable. Use with care. */
static void prim_getEnv(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- std::string name(state.forceStringNoCtx(*args[0], pos));
+ std::string name(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.getEnv"));
v.mkString(evalSettings.restrictEval || evalSettings.pureEval ? "" : getEnv(name).value_or(""));
}
@@ -1013,6 +997,7 @@ static void prim_second(EvalState & state, const PosIdx pos, Value * * args, Val
* Derivations
*************************************************************/
+static void derivationStrictInternal(EvalState & state, const std::string & name, Bindings * attrs, Value & v);
/* Construct (as a unobservable side effect) a Nix derivation
expression that performs the derivation described by the argument
@@ -1023,38 +1008,68 @@ static void prim_second(EvalState & state, const PosIdx pos, Value * * args, Val
derivation. */
static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- using nlohmann::json;
- state.forceAttrs(*args[0], pos);
+ state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.derivationStrict");
+
+ Bindings * attrs = args[0]->attrs;
/* Figure out the name first (for stack backtraces). */
- Bindings::iterator attr = getAttr(
- state,
- "derivationStrict",
- state.sName,
- args[0]->attrs,
- pos
- );
+ Bindings::iterator nameAttr = getAttr(state, state.sName, attrs, "in the attrset passed as argument to builtins.derivationStrict");
std::string drvName;
- const auto posDrvName = attr->pos;
try {
- drvName = state.forceStringNoCtx(*attr->value, pos);
+ drvName = state.forceStringNoCtx(*nameAttr->value, pos, "while evaluating the `name` attribute passed to builtins.derivationStrict");
} catch (Error & e) {
- e.addTrace(state.positions[posDrvName], "while evaluating the derivation attribute 'name'");
+ e.addTrace(state.positions[nameAttr->pos], "while evaluating the derivation attribute 'name'");
throw;
}
+ try {
+ derivationStrictInternal(state, drvName, attrs, v);
+ } catch (Error & e) {
+ Pos pos = state.positions[nameAttr->pos];
+ /*
+ * Here we make two abuses of the error system
+ *
+ * 1. We print the location as a string to avoid a code snippet being
+ * printed. While the location of the name attribute is a good hint, the
+ * exact code there is irrelevant.
+ *
+ * 2. We mark this trace as a frame trace, meaning that we stop printing
+ * less important traces from now on. In particular, this prevents the
+ * display of the automatic "while calling builtins.derivationStrict"
+ * trace, which is of little use for the public we target here.
+ *
+ * Please keep in mind that error reporting is done on a best-effort
+ * basis in nix. There is no accurate location for a derivation, as it
+ * often results from the composition of several functions
+ * (derivationStrict, derivation, mkDerivation, mkPythonModule, etc.)
+ */
+ e.addTrace(nullptr, hintfmt(
+ "while evaluating derivation '%s'\n"
+ " whose name attribute is located at %s",
+ drvName, pos), true);
+ throw;
+ }
+}
+
+static void derivationStrictInternal(EvalState & state, const std::string &
+drvName, Bindings * attrs, Value & v)
+{
/* Check whether attributes should be passed as a JSON file. */
+ using nlohmann::json;
std::optional<json> jsonObject;
- attr = args[0]->attrs->find(state.sStructuredAttrs);
- if (attr != args[0]->attrs->end() && state.forceBool(*attr->value, pos))
+ auto attr = attrs->find(state.sStructuredAttrs);
+ if (attr != attrs->end() &&
+ state.forceBool(*attr->value, noPos,
+ "while evaluating the `__structuredAttrs` "
+ "attribute passed to builtins.derivationStrict"))
jsonObject = json::object();
/* 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);
+ attr = attrs->find(state.sIgnoreNulls);
+ if (attr != attrs->end())
+ ignoreNulls = state.forceBool(*attr->value, noPos, "while evaluating the `__ignoreNulls` attribute " "passed to builtins.derivationStrict");
/* Build the derivation expression by processing the attributes. */
Derivation drv;
@@ -1071,7 +1086,7 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
StringSet outputs;
outputs.insert("out");
- for (auto & i : args[0]->attrs->lexicographicOrder(state.symbols)) {
+ for (auto & i : attrs->lexicographicOrder(state.symbols)) {
if (i->name == state.sIgnoreNulls) continue;
const std::string & key = state.symbols[i->name];
vomit("processing attribute '%1%'", key);
@@ -1082,7 +1097,7 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
else
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("invalid value '%s' for 'outputHashMode' attribute", s),
- .errPos = state.positions[posDrvName]
+ .errPos = state.positions[noPos]
}));
};
@@ -1092,7 +1107,7 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
if (outputs.find(j) != outputs.end())
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("duplicate derivation output '%1%'", j),
- .errPos = state.positions[posDrvName]
+ .errPos = state.positions[noPos]
}));
/* !!! Check whether j is a valid attribute
name. */
@@ -1102,32 +1117,35 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
if (j == "drv")
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("invalid derivation output name 'drv'" ),
- .errPos = state.positions[posDrvName]
+ .errPos = state.positions[noPos]
}));
outputs.insert(j);
}
if (outputs.empty())
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("derivation cannot have an empty set of outputs"),
- .errPos = state.positions[posDrvName]
+ .errPos = state.positions[noPos]
}));
};
try {
+ // This try-catch block adds context for most errors.
+ // Use this empty error context to signify that we defer to it.
+ const std::string_view context_below("");
if (ignoreNulls) {
- state.forceValue(*i->value, pos);
+ state.forceValue(*i->value, noPos);
if (i->value->type() == nNull) continue;
}
if (i->name == state.sContentAddressed) {
- contentAddressed = state.forceBool(*i->value, pos);
+ contentAddressed = state.forceBool(*i->value, noPos, context_below);
if (contentAddressed)
settings.requireExperimentalFeature(Xp::CaDerivations);
}
else if (i->name == state.sImpure) {
- isImpure = state.forceBool(*i->value, pos);
+ isImpure = state.forceBool(*i->value, noPos, context_below);
if (isImpure)
settings.requireExperimentalFeature(Xp::ImpureDerivations);
}
@@ -1135,9 +1153,11 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
/* The `args' attribute is special: it supplies the
command-line arguments to the builder. */
else if (i->name == state.sArgs) {
- state.forceList(*i->value, pos);
+ state.forceList(*i->value, noPos, context_below);
for (auto elem : i->value->listItems()) {
- auto s = state.coerceToString(posDrvName, *elem, context, true).toOwned();
+ auto s = state.coerceToString(noPos, *elem, context,
+ "while evaluating an element of the argument list",
+ true).toOwned();
drv.args.push_back(s);
}
}
@@ -1150,29 +1170,29 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
if (i->name == state.sStructuredAttrs) continue;
- (*jsonObject)[key] = printValueAsJSON(state, true, *i->value, pos, context);
+ (*jsonObject)[key] = printValueAsJSON(state, true, *i->value, noPos, context);
if (i->name == state.sBuilder)
- drv.builder = state.forceString(*i->value, context, posDrvName);
+ drv.builder = state.forceString(*i->value, context, noPos, context_below);
else if (i->name == state.sSystem)
- drv.platform = state.forceStringNoCtx(*i->value, posDrvName);
+ drv.platform = state.forceStringNoCtx(*i->value, noPos, context_below);
else if (i->name == state.sOutputHash)
- outputHash = state.forceStringNoCtx(*i->value, posDrvName);
+ outputHash = state.forceStringNoCtx(*i->value, noPos, context_below);
else if (i->name == state.sOutputHashAlgo)
- outputHashAlgo = state.forceStringNoCtx(*i->value, posDrvName);
+ outputHashAlgo = state.forceStringNoCtx(*i->value, noPos, context_below);
else if (i->name == state.sOutputHashMode)
- handleHashMode(state.forceStringNoCtx(*i->value, posDrvName));
+ handleHashMode(state.forceStringNoCtx(*i->value, noPos, context_below));
else if (i->name == state.sOutputs) {
/* Require ‘outputs’ to be a list of strings. */
- state.forceList(*i->value, posDrvName);
+ state.forceList(*i->value, noPos, context_below);
Strings ss;
for (auto elem : i->value->listItems())
- ss.emplace_back(state.forceStringNoCtx(*elem, posDrvName));
+ ss.emplace_back(state.forceStringNoCtx(*elem, noPos, context_below));
handleOutputs(ss);
}
} else {
- auto s = state.coerceToString(i->pos, *i->value, context, true).toOwned();
+ auto s = state.coerceToString(noPos, *i->value, context, context_below, true).toOwned();
drv.env.emplace(key, s);
if (i->name == state.sBuilder) drv.builder = std::move(s);
else if (i->name == state.sSystem) drv.platform = std::move(s);
@@ -1186,9 +1206,9 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
}
} catch (Error & e) {
- e.addTrace(state.positions[posDrvName],
- "while evaluating the attribute '%1%' of the derivation '%2%'",
- key, drvName);
+ e.addTrace(state.positions[i->pos],
+ hintfmt("while evaluating attribute '%1%' of derivation '%2%'", key, drvName),
+ true);
throw;
}
}
@@ -1232,20 +1252,20 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
if (drv.builder == "")
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("required attribute 'builder' missing"),
- .errPos = state.positions[posDrvName]
+ .errPos = state.positions[noPos]
}));
if (drv.platform == "")
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("required attribute 'system' missing"),
- .errPos = state.positions[posDrvName]
+ .errPos = state.positions[noPos]
}));
/* Check whether the derivation name is valid. */
if (isDerivation(drvName))
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("derivation names are not allowed to end in '%s'", drvExtension),
- .errPos = state.positions[posDrvName]
+ .errPos = state.positions[noPos]
}));
if (outputHash) {
@@ -1256,7 +1276,7 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
if (outputs.size() != 1 || *(outputs.begin()) != "out")
state.debugThrowLastTrace(Error({
.msg = hintfmt("multiple outputs are not supported in fixed-output derivations"),
- .errPos = state.positions[posDrvName]
+ .errPos = state.positions[noPos]
}));
auto h = newHashAllowEmpty(*outputHash, parseHashTypeOpt(outputHashAlgo));
@@ -1277,7 +1297,7 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
if (contentAddressed && isImpure)
throw EvalError({
.msg = hintfmt("derivation cannot be both content-addressed and impure"),
- .errPos = state.positions[posDrvName]
+ .errPos = state.positions[noPos]
});
auto ht = parseHashTypeOpt(outputHashAlgo).value_or(htSHA256);
@@ -1321,7 +1341,7 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
if (!h)
throw AssertionError({
.msg = hintfmt("derivation produced no hash for output '%s'", i),
- .errPos = state.positions[posDrvName],
+ .errPos = state.positions[noPos],
});
auto outPath = state.store->makeOutputPath(i, *h, drvName);
drv.env[i] = state.store->printStorePath(outPath);
@@ -1354,11 +1374,12 @@ static void prim_derivationStrict(EvalState & state, const PosIdx pos, Value * *
drvHashes.lock()->insert_or_assign(drvPath, h);
}
- auto attrs = state.buildBindings(1 + drv.outputs.size());
- attrs.alloc(state.sDrvPath).mkString(drvPathS, {"=" + drvPathS});
+ auto result = state.buildBindings(1 + drv.outputs.size());
+ result.alloc(state.sDrvPath).mkString(drvPathS, {"=" + drvPathS});
for (auto & i : drv.outputs)
- mkOutputString(state, attrs, drvPath, drv, i);
- v.mkAttrs(attrs);
+ mkOutputString(state, result, drvPath, drv, i);
+
+ v.mkAttrs(result);
}
static RegisterPrimOp primop_derivationStrict(RegisterPrimOp::Info {
@@ -1376,7 +1397,7 @@ static RegisterPrimOp primop_derivationStrict(RegisterPrimOp::Info {
‘out’. */
static void prim_placeholder(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- v.mkString(hashPlaceholder(state.forceStringNoCtx(*args[0], pos)));
+ v.mkString(hashPlaceholder(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.placeholder")));
}
static RegisterPrimOp primop_placeholder({
@@ -1400,7 +1421,7 @@ static RegisterPrimOp primop_placeholder({
static void prim_toPath(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- Path path = state.coerceToPath(pos, *args[0], context);
+ Path path = state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.toPath");
v.mkString(canonPath(path), context);
}
@@ -1431,7 +1452,7 @@ static void prim_storePath(EvalState & state, const PosIdx pos, Value * * args,
}));
PathSet context;
- Path path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context));
+ Path path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.storePath"));
/* 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. */
@@ -1501,7 +1522,9 @@ static RegisterPrimOp primop_pathExists({
static void prim_baseNameOf(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- v.mkString(baseNameOf(*state.coerceToString(pos, *args[0], context, false, false)), context);
+ v.mkString(baseNameOf(*state.coerceToString(pos, *args[0], context,
+ "while evaluating the first argument passed to builtins.baseNameOf",
+ false, false)), context);
}
static RegisterPrimOp primop_baseNameOf({
@@ -1521,7 +1544,9 @@ static RegisterPrimOp primop_baseNameOf({
static void prim_dirOf(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- auto path = state.coerceToString(pos, *args[0], context, false, false);
+ auto path = state.coerceToString(pos, *args[0], context,
+ "while evaluating the first argument passed to builtins.dirOf",
+ false, false);
auto dir = dirOf(*path);
if (args[0]->type() == nPath) v.mkPath(dir); else v.mkString(dir, context);
}
@@ -1572,28 +1597,24 @@ static RegisterPrimOp primop_readFile({
which are desugared to 'findFile __nixPath "x"'. */
static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceList(*args[0], pos);
+ state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.findFile");
SearchPath searchPath;
for (auto v2 : args[0]->listItems()) {
- state.forceAttrs(*v2, pos);
+ state.forceAttrs(*v2, pos, "while evaluating an element of the list passed to builtins.findFile");
std::string prefix;
Bindings::iterator i = v2->attrs->find(state.sPrefix);
if (i != v2->attrs->end())
- prefix = state.forceStringNoCtx(*i->value, pos);
+ prefix = state.forceStringNoCtx(*i->value, pos, "while evaluating the `prefix` attribute of an element of the list passed to builtins.findFile");
- i = getAttr(
- state,
- "findFile",
- state.sPath,
- v2->attrs,
- pos
- );
+ i = getAttr(state, state.sPath, v2->attrs, "in an element of the __nixPath");
PathSet context;
- auto path = state.coerceToString(pos, *i->value, context, false, false).toOwned();
+ auto path = state.coerceToString(pos, *i->value, context,
+ "while evaluating the `path` attribute of an element of the list passed to builtins.findFile",
+ false, false).toOwned();
try {
auto rewrites = state.realiseContext(context);
@@ -1608,7 +1629,7 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V
searchPath.emplace_back(prefix, path);
}
- auto path = state.forceStringNoCtx(*args[1], pos);
+ auto path = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.findFile");
v.mkPath(state.checkSourcePath(state.findFile(searchPath, path, pos)));
}
@@ -1622,7 +1643,7 @@ static RegisterPrimOp primop_findFile(RegisterPrimOp::Info {
/* Return the cryptographic hash of a file in base-16. */
static void prim_hashFile(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto type = state.forceStringNoCtx(*args[0], pos);
+ auto type = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.hashFile");
std::optional<HashType> ht = parseHashType(type);
if (!ht)
state.debugThrowLastTrace(Error({
@@ -1646,23 +1667,73 @@ static RegisterPrimOp primop_hashFile({
.fun = prim_hashFile,
});
+
+/* Stringize a directory entry enum. Used by `readFileType' and `readDir'. */
+static const char * dirEntTypeToString(unsigned char dtType)
+{
+ /* Enum DT_(DIR|LNK|REG|UNKNOWN) */
+ switch(dtType) {
+ case DT_REG: return "regular"; break;
+ case DT_DIR: return "directory"; break;
+ case DT_LNK: return "symlink"; break;
+ default: return "unknown"; break;
+ }
+ return "unknown"; /* Unreachable */
+}
+
+
+static void prim_readFileType(EvalState & state, const PosIdx pos, Value * * args, Value & v)
+{
+ auto path = realisePath(state, pos, *args[0]);
+ /* Retrieve the directory entry type and stringize it. */
+ v.mkString(dirEntTypeToString(getFileType(path)));
+}
+
+static RegisterPrimOp primop_readFileType({
+ .name = "__readFileType",
+ .args = {"p"},
+ .doc = R"(
+ Determine the directory entry type of a filesystem node, being
+ one of "directory", "regular", "symlink", or "unknown".
+ )",
+ .fun = prim_readFileType,
+});
+
/* Read a directory (without . or ..) */
static void prim_readDir(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
auto path = realisePath(state, pos, *args[0]);
+ // Retrieve directory entries for all nodes in a directory.
+ // This is similar to `getFileType` but is optimized to reduce system calls
+ // on many systems.
DirEntries entries = readDirectory(path);
auto attrs = state.buildBindings(entries.size());
+ // If we hit unknown directory entry types we may need to fallback to
+ // using `getFileType` on some systems.
+ // In order to reduce system calls we make each lookup lazy by using
+ // `builtins.readFileType` application.
+ Value * readFileType = nullptr;
+
for (auto & ent : entries) {
- if (ent.type == DT_UNKNOWN)
- ent.type = getFileType(path + "/" + ent.name);
- attrs.alloc(ent.name).mkString(
- ent.type == DT_REG ? "regular" :
- ent.type == DT_DIR ? "directory" :
- ent.type == DT_LNK ? "symlink" :
- "unknown");
+ auto & attr = attrs.alloc(ent.name);
+ if (ent.type == DT_UNKNOWN) {
+ // Some filesystems or operating systems may not be able to return
+ // detailed node info quickly in this case we produce a thunk to
+ // query the file type lazily.
+ auto epath = state.allocValue();
+ Path path2 = path + "/" + ent.name;
+ epath->mkString(path2);
+ if (!readFileType)
+ readFileType = &state.getBuiltin("readFileType");
+ attr.mkApp(readFileType, epath);
+ } else {
+ // This branch of the conditional is much more likely.
+ // Here we just stringize the directory entry type.
+ attr.mkString(dirEntTypeToString(ent.type));
+ }
}
v.mkAttrs(attrs);
@@ -1829,7 +1900,7 @@ static RegisterPrimOp primop_toJSON({
/* Parse a JSON string to a value. */
static void prim_fromJSON(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto s = state.forceStringNoCtx(*args[0], pos);
+ auto s = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.fromJSON");
try {
parseJSON(state, s, v);
} catch (JSONParseError &e) {
@@ -1858,8 +1929,8 @@ static RegisterPrimOp primop_fromJSON({
static void prim_toFile(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- std::string name(state.forceStringNoCtx(*args[0], pos));
- std::string contents(state.forceString(*args[1], context, pos));
+ std::string name(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.toFile"));
+ std::string contents(state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.toFile"));
StorePathSet refs;
@@ -2016,7 +2087,7 @@ static void addPath(
Value res;
state.callFunction(*filterFun, 2, args, res, pos);
- return state.forceBool(res, pos);
+ return state.forceBool(res, pos, "while evaluating the return value of the path filter function");
}) : defaultPathFilter;
std::optional<StorePath> expectedStorePath;
@@ -2042,17 +2113,8 @@ static void addPath(
static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- Path path = state.coerceToPath(pos, *args[1], context);
-
- state.forceValue(*args[0], pos);
- if (args[0]->type() != nFunction)
- state.debugThrowLastTrace(TypeError({
- .msg = hintfmt(
- "first argument in call to 'filterSource' is not a function but %1%",
- showType(*args[0])),
- .errPos = state.positions[pos]
- }));
-
+ Path path = state.coerceToPath(pos, *args[1], context, "while evaluating the second argument (the path to filter) passed to builtins.filterSource");
+ state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource");
addPath(state, pos, std::string(baseNameOf(path)), path, args[0], FileIngestionMethod::Recursive, std::nullopt, v, context);
}
@@ -2113,7 +2175,7 @@ static RegisterPrimOp primop_filterSource({
static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceAttrs(*args[0], pos);
+ state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.path");
Path path;
std::string name;
Value * filterFun = nullptr;
@@ -2124,16 +2186,15 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value
for (auto & attr : *args[0]->attrs) {
auto n = state.symbols[attr.name];
if (n == "path")
- path = state.coerceToPath(attr.pos, *attr.value, context);
+ path = state.coerceToPath(attr.pos, *attr.value, context, "while evaluating the `path` attribute passed to builtins.path");
else if (attr.name == state.sName)
- name = state.forceStringNoCtx(*attr.value, attr.pos);
- else if (n == "filter") {
- state.forceValue(*attr.value, pos);
- filterFun = attr.value;
- } else if (n == "recursive")
- method = FileIngestionMethod { state.forceBool(*attr.value, attr.pos) };
+ name = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `name` attribute passed to builtins.path");
+ else if (n == "filter")
+ state.forceFunction(*(filterFun = attr.value), attr.pos, "while evaluating the `filter` parameter passed to builtins.path");
+ else if (n == "recursive")
+ method = FileIngestionMethod { state.forceBool(*attr.value, attr.pos, "while evaluating the `recursive` attribute passed to builtins.path") };
else if (n == "sha256")
- expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos), htSHA256);
+ expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `sha256` attribute passed to builtins.path"), htSHA256);
else
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("unsupported argument '%1%' to 'addPath'", state.symbols[attr.name]),
@@ -2142,7 +2203,7 @@ static void prim_path(EvalState & state, const PosIdx pos, Value * * args, Value
}
if (path.empty())
state.debugThrowLastTrace(EvalError({
- .msg = hintfmt("'path' required"),
+ .msg = hintfmt("missing required 'path' attribute in the first argument to builtins.path"),
.errPos = state.positions[pos]
}));
if (name.empty())
@@ -2196,7 +2257,7 @@ static RegisterPrimOp primop_path({
strings. */
static void prim_attrNames(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceAttrs(*args[0], pos);
+ state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.attrNames");
state.mkList(v, args[0]->attrs->size());
@@ -2223,7 +2284,7 @@ static RegisterPrimOp primop_attrNames({
order as attrNames. */
static void prim_attrValues(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceAttrs(*args[0], pos);
+ state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.attrValues");
state.mkList(v, args[0]->attrs->size());
@@ -2255,14 +2316,13 @@ static RegisterPrimOp primop_attrValues({
/* Dynamic version of the `.' operator. */
void prim_getAttr(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto attr = state.forceStringNoCtx(*args[0], pos);
- state.forceAttrs(*args[1], pos);
+ auto attr = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.getAttr");
+ state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.getAttr");
Bindings::iterator i = getAttr(
state,
- "getAttr",
state.symbols.create(attr),
args[1]->attrs,
- pos
+ "in the attribute set under consideration"
);
// !!! add to stack trace?
if (state.countCalls && i->pos) state.attrSelects[i->pos]++;
@@ -2285,8 +2345,8 @@ static RegisterPrimOp primop_getAttr({
/* Return position information of the specified attribute. */
static void prim_unsafeGetAttrPos(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto attr = state.forceStringNoCtx(*args[0], pos);
- state.forceAttrs(*args[1], pos);
+ auto attr = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.unsafeGetAttrPos");
+ state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.unsafeGetAttrPos");
Bindings::iterator i = args[1]->attrs->find(state.symbols.create(attr));
if (i == args[1]->attrs->end())
v.mkNull();
@@ -2303,8 +2363,8 @@ static RegisterPrimOp primop_unsafeGetAttrPos(RegisterPrimOp::Info {
/* Dynamic version of the `?' operator. */
static void prim_hasAttr(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto attr = state.forceStringNoCtx(*args[0], pos);
- state.forceAttrs(*args[1], pos);
+ auto attr = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.hasAttr");
+ state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.hasAttr");
v.mkBool(args[1]->attrs->find(state.symbols.create(attr)) != args[1]->attrs->end());
}
@@ -2337,8 +2397,8 @@ static RegisterPrimOp primop_isAttrs({
static void prim_removeAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceAttrs(*args[0], pos);
- state.forceList(*args[1], pos);
+ state.forceAttrs(*args[0], pos, "while evaluating the first argument passed to builtins.removeAttrs");
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.removeAttrs");
/* Get the attribute names to be removed.
We keep them as Attrs instead of Symbols so std::set_difference
@@ -2346,7 +2406,7 @@ static void prim_removeAttrs(EvalState & state, const PosIdx pos, Value * * args
boost::container::small_vector<Attr, 64> names;
names.reserve(args[1]->listSize());
for (auto elem : args[1]->listItems()) {
- state.forceStringNoCtx(*elem, pos);
+ state.forceStringNoCtx(*elem, pos, "while evaluating the values of the second argument passed to builtins.removeAttrs");
names.emplace_back(state.symbols.create(elem->string.s), nullptr);
}
std::sort(names.begin(), names.end());
@@ -2385,34 +2445,22 @@ static RegisterPrimOp primop_removeAttrs({
name, the first takes precedence. */
static void prim_listToAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceList(*args[0], pos);
+ state.forceList(*args[0], pos, "while evaluating the argument passed to builtins.listToAttrs");
auto attrs = state.buildBindings(args[0]->listSize());
std::set<Symbol> seen;
for (auto v2 : args[0]->listItems()) {
- state.forceAttrs(*v2, pos);
+ state.forceAttrs(*v2, pos, "while evaluating an element of the list passed to builtins.listToAttrs");
- Bindings::iterator j = getAttr(
- state,
- "listToAttrs",
- state.sName,
- v2->attrs,
- pos
- );
+ Bindings::iterator j = getAttr(state, state.sName, v2->attrs, "in a {name=...; value=...;} pair");
- auto name = state.forceStringNoCtx(*j->value, j->pos);
+ auto name = state.forceStringNoCtx(*j->value, j->pos, "while evaluating the `name` attribute of an element of the list passed to builtins.listToAttrs");
auto sym = state.symbols.create(name);
if (seen.insert(sym).second) {
- Bindings::iterator j2 = getAttr(
- state,
- "listToAttrs",
- state.sValue,
- v2->attrs,
- pos
- );
+ Bindings::iterator j2 = getAttr(state, state.sValue, v2->attrs, "in a {name=...; value=...;} pair");
attrs.insert(sym, j2->value, j2->pos);
}
}
@@ -2453,8 +2501,8 @@ static RegisterPrimOp primop_listToAttrs({
static void prim_intersectAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceAttrs(*args[0], pos);
- state.forceAttrs(*args[1], pos);
+ state.forceAttrs(*args[0], pos, "while evaluating the first argument passed to builtins.intersectAttrs");
+ state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.intersectAttrs");
Bindings &left = *args[0]->attrs;
Bindings &right = *args[1]->attrs;
@@ -2531,14 +2579,14 @@ static RegisterPrimOp primop_intersectAttrs({
static void prim_catAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto attrName = state.symbols.create(state.forceStringNoCtx(*args[0], pos));
- state.forceList(*args[1], pos);
+ auto attrName = state.symbols.create(state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.catAttrs"));
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.catAttrs");
Value * res[args[1]->listSize()];
unsigned int found = 0;
for (auto v2 : args[1]->listItems()) {
- state.forceAttrs(*v2, pos);
+ state.forceAttrs(*v2, pos, "while evaluating an element in the list passed as second argument to builtins.catAttrs");
Bindings::iterator i = v2->attrs->find(attrName);
if (i != v2->attrs->end())
res[found++] = i->value;
@@ -2611,7 +2659,7 @@ static RegisterPrimOp primop_functionArgs({
/* */
static void prim_mapAttrs(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceAttrs(*args[1], pos);
+ state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.mapAttrs");
auto attrs = state.buildBindings(args[1]->attrs->size());
@@ -2652,21 +2700,16 @@ static void prim_zipAttrsWith(EvalState & state, const PosIdx pos, Value * * arg
std::map<Symbol, std::pair<size_t, Value * *>> attrsSeen;
- state.forceFunction(*args[0], pos);
- state.forceList(*args[1], pos);
+ state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.zipAttrsWith");
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.zipAttrsWith");
const auto listSize = args[1]->listSize();
const auto listElems = args[1]->listElems();
for (unsigned int n = 0; n < listSize; ++n) {
Value * vElem = listElems[n];
- try {
- state.forceAttrs(*vElem, noPos);
- for (auto & attr : *vElem->attrs)
- attrsSeen[attr.name].first++;
- } catch (TypeError & e) {
- e.addTrace(state.positions[pos], hintfmt("while invoking '%s'", "zipAttrsWith"));
- state.debugThrowLastTrace(e);
- }
+ state.forceAttrs(*vElem, noPos, "while evaluating a value of the list passed as second argument to builtins.zipAttrsWith");
+ for (auto & attr : *vElem->attrs)
+ attrsSeen[attr.name].first++;
}
auto attrs = state.buildBindings(attrsSeen.size());
@@ -2750,7 +2793,7 @@ static RegisterPrimOp primop_isList({
static void elemAt(EvalState & state, const PosIdx pos, Value & list, int n, Value & v)
{
- state.forceList(list, pos);
+ state.forceList(list, pos, "while evaluating the first argument passed to builtins.elemAt");
if (n < 0 || (unsigned int) n >= list.listSize())
state.debugThrowLastTrace(Error({
.msg = hintfmt("list index %1% is out of bounds", n),
@@ -2763,7 +2806,7 @@ static void elemAt(EvalState & state, const PosIdx pos, Value & list, int n, Val
/* Return the n-1'th element of a list. */
static void prim_elemAt(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- elemAt(state, pos, *args[0], state.forceInt(*args[1], pos), v);
+ elemAt(state, pos, *args[0], state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.elemAt"), v);
}
static RegisterPrimOp primop_elemAt({
@@ -2798,7 +2841,7 @@ static RegisterPrimOp primop_head({
don't want to use it! */
static void prim_tail(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceList(*args[0], pos);
+ state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.tail");
if (args[0]->listSize() == 0)
state.debugThrowLastTrace(Error({
.msg = hintfmt("'tail' called on an empty list"),
@@ -2829,10 +2872,16 @@ static RegisterPrimOp primop_tail({
/* Apply a function to every element of a list. */
static void prim_map(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceList(*args[1], pos);
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.map");
- state.mkList(v, args[1]->listSize());
+ if (args[1]->listSize() == 0) {
+ v = *args[1];
+ return;
+ }
+
+ state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.map");
+ state.mkList(v, args[1]->listSize());
for (unsigned int n = 0; n < v.listSize(); ++n)
(v.listElems()[n] = state.allocValue())->mkApp(
args[0], args[1]->listElems()[n]);
@@ -2859,8 +2908,14 @@ static RegisterPrimOp primop_map({
returns true. */
static void prim_filter(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceFunction(*args[0], pos);
- state.forceList(*args[1], pos);
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.filter");
+
+ if (args[1]->listSize() == 0) {
+ v = *args[1];
+ return;
+ }
+
+ state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filter");
// FIXME: putting this on the stack is risky.
Value * vs[args[1]->listSize()];
@@ -2870,7 +2925,7 @@ static void prim_filter(EvalState & state, const PosIdx pos, Value * * args, Val
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))
+ if (state.forceBool(res, pos, "while evaluating the return value of the filtering function passed to builtins.filter"))
vs[k++] = args[1]->listElems()[n];
else
same = false;
@@ -2898,9 +2953,9 @@ static RegisterPrimOp primop_filter({
static void prim_elem(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
bool res = false;
- state.forceList(*args[1], pos);
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.elem");
for (auto elem : args[1]->listItems())
- if (state.eqValues(*args[0], *elem)) {
+ if (state.eqValues(*args[0], *elem, pos, "while searching for the presence of the given element in the list")) {
res = true;
break;
}
@@ -2920,8 +2975,8 @@ static RegisterPrimOp primop_elem({
/* Concatenate a list of lists. */
static void prim_concatLists(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceList(*args[0], pos);
- state.concatLists(v, args[0]->listSize(), args[0]->listElems(), pos);
+ state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.concatLists");
+ state.concatLists(v, args[0]->listSize(), args[0]->listElems(), pos, "while evaluating a value of the list passed to builtins.concatLists");
}
static RegisterPrimOp primop_concatLists({
@@ -2936,7 +2991,7 @@ static RegisterPrimOp primop_concatLists({
/* Return the length of a list. This is an O(1) time operation. */
static void prim_length(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceList(*args[0], pos);
+ state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.length");
v.mkInt(args[0]->listSize());
}
@@ -2953,8 +3008,8 @@ static RegisterPrimOp primop_length({
right. The operator is applied strictly. */
static void prim_foldlStrict(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceFunction(*args[0], pos);
- state.forceList(*args[2], pos);
+ state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.foldlStrict");
+ state.forceList(*args[2], pos, "while evaluating the third argument passed to builtins.foldlStrict");
if (args[2]->listSize()) {
Value * vCur = args[1];
@@ -2986,13 +3041,13 @@ static RegisterPrimOp primop_foldlStrict({
static void anyOrAll(bool any, EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceFunction(*args[0], pos);
- state.forceList(*args[1], pos);
+ state.forceFunction(*args[0], pos, std::string("while evaluating the first argument passed to builtins.") + (any ? "any" : "all"));
+ state.forceList(*args[1], pos, std::string("while evaluating the second argument passed to builtins.") + (any ? "any" : "all"));
Value vTmp;
for (auto elem : args[1]->listItems()) {
state.callFunction(*args[0], *elem, vTmp, pos);
- bool res = state.forceBool(vTmp, pos);
+ bool res = state.forceBool(vTmp, pos, std::string("while evaluating the return value of the function passed to builtins.") + (any ? "any" : "all"));
if (res == any) {
v.mkBool(any);
return;
@@ -3035,16 +3090,16 @@ static RegisterPrimOp primop_all({
static void prim_genList(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto len = state.forceInt(*args[1], pos);
+ auto len = state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.genList");
if (len < 0)
- state.debugThrowLastTrace(EvalError({
- .msg = hintfmt("cannot create list of size %1%", len),
- .errPos = state.positions[pos]
- }));
+ state.error("cannot create list of size %1%", len).debugThrow<EvalError>();
- state.mkList(v, len);
+ // More strict than striclty (!) necessary, but acceptable
+ // as evaluating map without accessing any values makes little sense.
+ state.forceFunction(*args[0], noPos, "while evaluating the first argument passed to builtins.genList");
+ state.mkList(v, len);
for (unsigned int n = 0; n < (unsigned int) len; ++n) {
auto arg = state.allocValue();
arg->mkInt(n);
@@ -3073,10 +3128,16 @@ static void prim_lessThan(EvalState & state, const PosIdx pos, Value * * args, V
static void prim_sort(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceFunction(*args[0], pos);
- state.forceList(*args[1], pos);
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.sort");
auto len = args[1]->listSize();
+ if (len == 0) {
+ v = *args[1];
+ return;
+ }
+
+ state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.sort");
+
state.mkList(v, len);
for (unsigned int n = 0; n < len; ++n) {
state.forceValue(*args[1]->listElems()[n], pos);
@@ -3086,13 +3147,15 @@ static void prim_sort(EvalState & state, const PosIdx pos, Value * * args, Value
auto comparator = [&](Value * a, Value * b) {
/* Optimization: if the comparator is lessThan, bypass
callFunction. */
+ /* TODO: (layus) this is absurd. An optimisation like this
+ should be outside the lambda creation */
if (args[0]->isPrimOp() && args[0]->primOp->fun == prim_lessThan)
- return CompareValues(state)(a, b);
+ return CompareValues(state, noPos, "while evaluating the ordering function passed to builtins.sort")(a, b);
Value * vs[] = {a, b};
Value vBool;
- state.callFunction(*args[0], 2, vs, vBool, pos);
- return state.forceBool(vBool, pos);
+ state.callFunction(*args[0], 2, vs, vBool, noPos);
+ return state.forceBool(vBool, pos, "while evaluating the return value of the sorting function passed to builtins.sort");
};
/* FIXME: std::sort can segfault if the comparator is not a strict
@@ -3124,8 +3187,8 @@ static RegisterPrimOp primop_sort({
static void prim_partition(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceFunction(*args[0], pos);
- state.forceList(*args[1], pos);
+ state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.partition");
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.partition");
auto len = args[1]->listSize();
@@ -3136,7 +3199,7 @@ static void prim_partition(EvalState & state, const PosIdx pos, Value * * args,
state.forceValue(*vElem, pos);
Value res;
state.callFunction(*args[0], *vElem, res, pos);
- if (state.forceBool(res, pos))
+ if (state.forceBool(res, pos, "while evaluating the return value of the partition function passed to builtins.partition"))
right.push_back(vElem);
else
wrong.push_back(vElem);
@@ -3184,15 +3247,15 @@ static RegisterPrimOp primop_partition({
static void prim_groupBy(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceFunction(*args[0], pos);
- state.forceList(*args[1], pos);
+ state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.groupBy");
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.groupBy");
ValueVectorMap attrs;
for (auto vElem : args[1]->listItems()) {
Value res;
state.callFunction(*args[0], *vElem, res, pos);
- auto name = state.forceStringNoCtx(res, pos);
+ auto name = state.forceStringNoCtx(res, pos, "while evaluating the return value of the grouping function passed to builtins.groupBy");
auto sym = state.symbols.create(name);
auto vector = attrs.try_emplace(sym, ValueVector()).first;
vector->second.push_back(vElem);
@@ -3236,8 +3299,8 @@ static RegisterPrimOp primop_groupBy({
static void prim_concatMap(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceFunction(*args[0], pos);
- state.forceList(*args[1], pos);
+ state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.concatMap");
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.concatMap");
auto nrLists = args[1]->listSize();
Value lists[nrLists];
@@ -3246,12 +3309,7 @@ static void prim_concatMap(EvalState & state, const PosIdx pos, Value * * args,
for (unsigned int n = 0; n < nrLists; ++n) {
Value * vElem = args[1]->listElems()[n];
state.callFunction(*args[0], *vElem, lists[n], pos);
- try {
- state.forceList(lists[n], lists[n].determinePos(args[0]->determinePos(pos)));
- } catch (TypeError &e) {
- e.addTrace(state.positions[pos], hintfmt("while invoking '%s'", "concatMap"));
- state.debugThrowLastTrace(e);
- }
+ state.forceList(lists[n], lists[n].determinePos(args[0]->determinePos(pos)), "while evaluating the return value of the function passed to buitlins.concatMap");
len += lists[n].listSize();
}
@@ -3286,9 +3344,11 @@ static void prim_add(EvalState & state, const PosIdx pos, Value * * args, Value
state.forceValue(*args[0], pos);
state.forceValue(*args[1], pos);
if (args[0]->type() == nFloat || args[1]->type() == nFloat)
- v.mkFloat(state.forceFloat(*args[0], pos) + state.forceFloat(*args[1], pos));
+ v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the addition")
+ + state.forceFloat(*args[1], pos, "while evaluating the second argument of the addition"));
else
- v.mkInt(state.forceInt(*args[0], pos) + state.forceInt(*args[1], pos));
+ v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the addition")
+ + state.forceInt(*args[1], pos, "while evaluating the second argument of the addition"));
}
static RegisterPrimOp primop_add({
@@ -3305,9 +3365,11 @@ static void prim_sub(EvalState & state, const PosIdx pos, Value * * args, Value
state.forceValue(*args[0], pos);
state.forceValue(*args[1], pos);
if (args[0]->type() == nFloat || args[1]->type() == nFloat)
- v.mkFloat(state.forceFloat(*args[0], pos) - state.forceFloat(*args[1], pos));
+ v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first argument of the subtraction")
+ - state.forceFloat(*args[1], pos, "while evaluating the second argument of the subtraction"));
else
- v.mkInt(state.forceInt(*args[0], pos) - state.forceInt(*args[1], pos));
+ v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the subtraction")
+ - state.forceInt(*args[1], pos, "while evaluating the second argument of the subtraction"));
}
static RegisterPrimOp primop_sub({
@@ -3324,9 +3386,11 @@ static void prim_mul(EvalState & state, const PosIdx pos, Value * * args, Value
state.forceValue(*args[0], pos);
state.forceValue(*args[1], pos);
if (args[0]->type() == nFloat || args[1]->type() == nFloat)
- v.mkFloat(state.forceFloat(*args[0], pos) * state.forceFloat(*args[1], pos));
+ v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first of the multiplication")
+ * state.forceFloat(*args[1], pos, "while evaluating the second argument of the multiplication"));
else
- v.mkInt(state.forceInt(*args[0], pos) * state.forceInt(*args[1], pos));
+ v.mkInt( state.forceInt(*args[0], pos, "while evaluating the first argument of the multiplication")
+ * state.forceInt(*args[1], pos, "while evaluating the second argument of the multiplication"));
}
static RegisterPrimOp primop_mul({
@@ -3343,7 +3407,7 @@ static void prim_div(EvalState & state, const PosIdx pos, Value * * args, Value
state.forceValue(*args[0], pos);
state.forceValue(*args[1], pos);
- NixFloat f2 = state.forceFloat(*args[1], pos);
+ NixFloat f2 = state.forceFloat(*args[1], pos, "while evaluating the second operand of the division");
if (f2 == 0)
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("division by zero"),
@@ -3351,10 +3415,10 @@ static void prim_div(EvalState & state, const PosIdx pos, Value * * args, Value
}));
if (args[0]->type() == nFloat || args[1]->type() == nFloat) {
- v.mkFloat(state.forceFloat(*args[0], pos) / state.forceFloat(*args[1], pos));
+ v.mkFloat(state.forceFloat(*args[0], pos, "while evaluating the first operand of the division") / f2);
} else {
- NixInt i1 = state.forceInt(*args[0], pos);
- NixInt i2 = state.forceInt(*args[1], pos);
+ NixInt i1 = state.forceInt(*args[0], pos, "while evaluating the first operand of the division");
+ NixInt i2 = state.forceInt(*args[1], pos, "while evaluating the second operand of the division");
/* Avoid division overflow as it might raise SIGFPE. */
if (i1 == std::numeric_limits<NixInt>::min() && i2 == -1)
state.debugThrowLastTrace(EvalError({
@@ -3377,7 +3441,8 @@ static RegisterPrimOp primop_div({
static void prim_bitAnd(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- v.mkInt(state.forceInt(*args[0], pos) & state.forceInt(*args[1], pos));
+ v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitAnd")
+ & state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitAnd"));
}
static RegisterPrimOp primop_bitAnd({
@@ -3391,7 +3456,8 @@ static RegisterPrimOp primop_bitAnd({
static void prim_bitOr(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- v.mkInt(state.forceInt(*args[0], pos) | state.forceInt(*args[1], pos));
+ v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitOr")
+ | state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitOr"));
}
static RegisterPrimOp primop_bitOr({
@@ -3405,7 +3471,8 @@ static RegisterPrimOp primop_bitOr({
static void prim_bitXor(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- v.mkInt(state.forceInt(*args[0], pos) ^ state.forceInt(*args[1], pos));
+ v.mkInt(state.forceInt(*args[0], pos, "while evaluating the first argument passed to builtins.bitXor")
+ ^ state.forceInt(*args[1], pos, "while evaluating the second argument passed to builtins.bitXor"));
}
static RegisterPrimOp primop_bitXor({
@@ -3421,7 +3488,8 @@ static void prim_lessThan(EvalState & state, const PosIdx pos, Value * * args, V
{
state.forceValue(*args[0], pos);
state.forceValue(*args[1], pos);
- CompareValues comp{state};
+ // pos is exact here, no need for a message.
+ CompareValues comp(state, noPos, "");
v.mkBool(comp(args[0], args[1]));
}
@@ -3448,7 +3516,9 @@ static RegisterPrimOp primop_lessThan({
static void prim_toString(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- auto s = state.coerceToString(pos, *args[0], context, true, false);
+ auto s = state.coerceToString(pos, *args[0], context,
+ "while evaluating the first argument passed to builtins.toString",
+ true, false);
v.mkString(*s, context);
}
@@ -3482,10 +3552,10 @@ static RegisterPrimOp primop_toString({
non-negative. */
static void prim_substring(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- int start = state.forceInt(*args[0], pos);
- int len = state.forceInt(*args[1], pos);
+ int start = state.forceInt(*args[0], pos, "while evaluating the first argument (the start offset) passed to builtins.substring");
+ int len = state.forceInt(*args[1], pos, "while evaluating the second argument (the substring length) passed to builtins.substring");
PathSet context;
- auto s = state.coerceToString(pos, *args[2], context);
+ auto s = state.coerceToString(pos, *args[2], context, "while evaluating the third argument (the string) passed to builtins.substring");
if (start < 0)
state.debugThrowLastTrace(EvalError({
@@ -3519,7 +3589,7 @@ static RegisterPrimOp primop_substring({
static void prim_stringLength(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- auto s = state.coerceToString(pos, *args[0], context);
+ auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.stringLength");
v.mkInt(s->size());
}
@@ -3536,7 +3606,7 @@ static RegisterPrimOp primop_stringLength({
/* Return the cryptographic hash of a string in base-16. */
static void prim_hashString(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto type = state.forceStringNoCtx(*args[0], pos);
+ auto type = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.hashString");
std::optional<HashType> ht = parseHashType(type);
if (!ht)
state.debugThrowLastTrace(Error({
@@ -3545,7 +3615,7 @@ static void prim_hashString(EvalState & state, const PosIdx pos, Value * * args,
}));
PathSet context; // discarded
- auto s = state.forceString(*args[1], context, pos);
+ auto s = state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.hashString");
v.mkString(hashString(*ht, s).to_string(Base16, false));
}
@@ -3584,14 +3654,14 @@ std::shared_ptr<RegexCache> makeRegexCache()
void prim_match(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto re = state.forceStringNoCtx(*args[0], pos);
+ auto re = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.match");
try {
auto regex = state.regexCache->get(re);
PathSet context;
- const auto str = state.forceString(*args[1], context, pos);
+ const auto str = state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.match");
std::cmatch match;
if (!std::regex_match(str.begin(), str.end(), match, regex)) {
@@ -3664,14 +3734,14 @@ static RegisterPrimOp primop_match({
non-matching parts interleaved by the lists of the matching groups. */
void prim_split(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto re = state.forceStringNoCtx(*args[0], pos);
+ auto re = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.split");
try {
auto regex = state.regexCache->get(re);
PathSet context;
- const auto str = state.forceString(*args[1], context, pos);
+ const auto str = state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.split");
auto begin = std::cregex_iterator(str.begin(), str.end(), regex);
auto end = std::cregex_iterator();
@@ -3769,8 +3839,8 @@ static void prim_concatStringsSep(EvalState & state, const PosIdx pos, Value * *
{
PathSet context;
- auto sep = state.forceString(*args[0], context, pos);
- state.forceList(*args[1], pos);
+ auto sep = state.forceString(*args[0], context, pos, "while evaluating the first argument (the separator string) passed to builtins.concatStringsSep");
+ state.forceList(*args[1], pos, "while evaluating the second argument (the list of strings to concat) passed to builtins.concatStringsSep");
std::string res;
res.reserve((args[1]->listSize() + 32) * sep.size());
@@ -3778,7 +3848,7 @@ static void prim_concatStringsSep(EvalState & state, const PosIdx pos, Value * *
for (auto elem : args[1]->listItems()) {
if (first) first = false; else res += sep;
- res += *state.coerceToString(pos, *elem, context);
+ res += *state.coerceToString(pos, *elem, context, "while evaluating one element of the list of strings to concat passed to builtins.concatStringsSep");
}
v.mkString(res, context);
@@ -3797,29 +3867,26 @@ static RegisterPrimOp primop_concatStringsSep({
static void prim_replaceStrings(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceList(*args[0], pos);
- state.forceList(*args[1], pos);
+ state.forceList(*args[0], pos, "while evaluating the first argument passed to builtins.replaceStrings");
+ state.forceList(*args[1], pos, "while evaluating the second argument passed to builtins.replaceStrings");
if (args[0]->listSize() != args[1]->listSize())
- state.debugThrowLastTrace(EvalError({
- .msg = hintfmt("'from' and 'to' arguments to 'replaceStrings' have different lengths"),
- .errPos = state.positions[pos]
- }));
+ state.error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths").atPos(pos).debugThrow<EvalError>();
std::vector<std::string> from;
from.reserve(args[0]->listSize());
for (auto elem : args[0]->listItems())
- from.emplace_back(state.forceString(*elem, pos));
+ from.emplace_back(state.forceString(*elem, pos, "while evaluating one of the strings to replace passed to builtins.replaceStrings"));
std::vector<std::pair<std::string, PathSet>> to;
to.reserve(args[1]->listSize());
for (auto elem : args[1]->listItems()) {
PathSet ctx;
- auto s = state.forceString(*elem, ctx, pos);
+ auto s = state.forceString(*elem, ctx, pos, "while evaluating one of the replacement strings passed to builtins.replaceStrings");
to.emplace_back(s, std::move(ctx));
}
PathSet context;
- auto s = state.forceString(*args[2], context, pos);
+ auto s = state.forceString(*args[2], context, pos, "while evaluating the third argument passed to builtins.replaceStrings");
std::string res;
// Loops one past last character to handle the case where 'from' contains an empty string.
@@ -3877,7 +3944,7 @@ static RegisterPrimOp primop_replaceStrings({
static void prim_parseDrvName(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto name = state.forceStringNoCtx(*args[0], pos);
+ auto name = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.parseDrvName");
DrvName parsed(name);
auto attrs = state.buildBindings(2);
attrs.alloc(state.sName).mkString(parsed.name);
@@ -3901,8 +3968,8 @@ static RegisterPrimOp primop_parseDrvName({
static void prim_compareVersions(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto version1 = state.forceStringNoCtx(*args[0], pos);
- auto version2 = state.forceStringNoCtx(*args[1], pos);
+ auto version1 = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.compareVersions");
+ auto version2 = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument passed to builtins.compareVersions");
v.mkInt(compareVersions(version1, version2));
}
@@ -3921,7 +3988,7 @@ static RegisterPrimOp primop_compareVersions({
static void prim_splitVersion(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- auto version = state.forceStringNoCtx(*args[0], pos);
+ auto version = state.forceStringNoCtx(*args[0], pos, "while evaluating the first argument passed to builtins.splitVersion");
auto iter = version.cbegin();
Strings components;
while (iter != version.cend()) {
diff --git a/src/libexpr/primops/context.cc b/src/libexpr/primops/context.cc
index 4b7357495..db43e5771 100644
--- a/src/libexpr/primops/context.cc
+++ b/src/libexpr/primops/context.cc
@@ -8,7 +8,7 @@ namespace nix {
static void prim_unsafeDiscardStringContext(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- auto s = state.coerceToString(pos, *args[0], context);
+ auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.unsafeDiscardStringContext");
v.mkString(*s);
}
@@ -18,7 +18,7 @@ static RegisterPrimOp primop_unsafeDiscardStringContext("__unsafeDiscardStringCo
static void prim_hasContext(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- state.forceString(*args[0], context, pos);
+ state.forceString(*args[0], context, pos, "while evaluating the argument passed to builtins.hasContext");
v.mkBool(!context.empty());
}
@@ -34,7 +34,7 @@ static RegisterPrimOp primop_hasContext("__hasContext", 1, prim_hasContext);
static void prim_unsafeDiscardOutputDependency(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- auto s = state.coerceToString(pos, *args[0], context);
+ auto s = state.coerceToString(pos, *args[0], context, "while evaluating the argument passed to builtins.unsafeDiscardOutputDependency");
PathSet context2;
for (auto && p : context) {
@@ -80,18 +80,16 @@ static void prim_getContext(EvalState & state, const PosIdx pos, Value * * args,
Strings outputs;
};
PathSet context;
- state.forceString(*args[0], context, pos);
+ state.forceString(*args[0], context, pos, "while evaluating the argument passed to builtins.getContext");
auto contextInfos = std::map<StorePath, ContextInfo>();
for (const auto & p : context) {
- Path drv;
- std::string output;
NixStringContextElem ctx = NixStringContextElem::parse(*state.store, p);
std::visit(overloaded {
[&](NixStringContextElem::DrvDeep & d) {
contextInfos[d.drvPath].allOutputs = true;
},
[&](NixStringContextElem::Built & b) {
- contextInfos[b.drvPath].outputs.emplace_back(std::move(output));
+ contextInfos[b.drvPath].outputs.emplace_back(std::move(b.output));
},
[&](NixStringContextElem::Opaque & o) {
contextInfos[o.path].path = true;
@@ -132,9 +130,9 @@ static RegisterPrimOp primop_getContext("__getContext", 1, prim_getContext);
static void prim_appendContext(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
PathSet context;
- auto orig = state.forceString(*args[0], context, pos);
+ auto orig = state.forceString(*args[0], context, noPos, "while evaluating the first argument passed to builtins.appendContext");
- state.forceAttrs(*args[1], pos);
+ state.forceAttrs(*args[1], pos, "while evaluating the second argument passed to builtins.appendContext");
auto sPath = state.symbols.create("path");
auto sAllOutputs = state.symbols.create("allOutputs");
@@ -142,24 +140,24 @@ static void prim_appendContext(EvalState & state, const PosIdx pos, Value * * ar
const auto & name = state.symbols[i.name];
if (!state.store->isStorePath(name))
throw EvalError({
- .msg = hintfmt("Context key '%s' is not a store path", name),
+ .msg = hintfmt("context key '%s' is not a store path", name),
.errPos = state.positions[i.pos]
});
if (!settings.readOnlyMode)
state.store->ensurePath(state.store->parseStorePath(name));
- state.forceAttrs(*i.value, i.pos);
+ state.forceAttrs(*i.value, i.pos, "while evaluating the value of a string context");
auto iter = i.value->attrs->find(sPath);
if (iter != i.value->attrs->end()) {
- if (state.forceBool(*iter->value, iter->pos))
+ if (state.forceBool(*iter->value, iter->pos, "while evaluating the `path` attribute of a string context"))
context.emplace(name);
}
iter = i.value->attrs->find(sAllOutputs);
if (iter != i.value->attrs->end()) {
- if (state.forceBool(*iter->value, iter->pos)) {
+ if (state.forceBool(*iter->value, iter->pos, "while evaluating the `allOutputs` attribute of a string context")) {
if (!isDerivation(name)) {
throw EvalError({
- .msg = hintfmt("Tried to add all-outputs context of %s, which is not a derivation, to a string", name),
+ .msg = hintfmt("tried to add all-outputs context of %s, which is not a derivation, to a string", name),
.errPos = state.positions[i.pos]
});
}
@@ -169,15 +167,15 @@ static void prim_appendContext(EvalState & state, const PosIdx pos, Value * * ar
iter = i.value->attrs->find(state.sOutputs);
if (iter != i.value->attrs->end()) {
- state.forceList(*iter->value, iter->pos);
+ state.forceList(*iter->value, iter->pos, "while evaluating the `outputs` attribute of a string context");
if (iter->value->listSize() && !isDerivation(name)) {
throw EvalError({
- .msg = hintfmt("Tried to add derivation output context of %s, which is not a derivation, to a string", name),
+ .msg = hintfmt("tried to add derivation output context of %s, which is not a derivation, to a string", name),
.errPos = state.positions[i.pos]
});
}
for (auto elem : iter->value->listItems()) {
- auto outputName = state.forceStringNoCtx(*elem, iter->pos);
+ auto outputName = state.forceStringNoCtx(*elem, iter->pos, "while evaluating an output name within a string context");
context.insert(concatStrings("!", outputName, "!", name));
}
}
diff --git a/src/libexpr/primops/fetchClosure.cc b/src/libexpr/primops/fetchClosure.cc
index 662c9652e..0dfa97fa3 100644
--- a/src/libexpr/primops/fetchClosure.cc
+++ b/src/libexpr/primops/fetchClosure.cc
@@ -7,7 +7,7 @@ namespace nix {
static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
- state.forceAttrs(*args[0], pos);
+ state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.fetchClosure");
std::optional<std::string> fromStoreUrl;
std::optional<StorePath> fromPath;
@@ -19,7 +19,8 @@ static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * arg
if (attrName == "fromPath") {
PathSet context;
- fromPath = state.coerceToStorePath(attr.pos, *attr.value, context);
+ fromPath = state.coerceToStorePath(attr.pos, *attr.value, context,
+ "while evaluating the 'fromPath' attribute passed to builtins.fetchClosure");
}
else if (attrName == "toPath") {
@@ -27,12 +28,14 @@ static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * arg
toCA = true;
if (attr.value->type() != nString || attr.value->string.s != std::string("")) {
PathSet context;
- toPath = state.coerceToStorePath(attr.pos, *attr.value, context);
+ toPath = state.coerceToStorePath(attr.pos, *attr.value, context,
+ "while evaluating the 'toPath' attribute passed to builtins.fetchClosure");
}
}
else if (attrName == "fromStore")
- fromStoreUrl = state.forceStringNoCtx(*attr.value, attr.pos);
+ fromStoreUrl = state.forceStringNoCtx(*attr.value, attr.pos,
+ "while evaluating the 'fromStore' attribute passed to builtins.fetchClosure");
else
throw Error({
diff --git a/src/libexpr/primops/fetchMercurial.cc b/src/libexpr/primops/fetchMercurial.cc
index 249c0934e..c41bd60b6 100644
--- a/src/libexpr/primops/fetchMercurial.cc
+++ b/src/libexpr/primops/fetchMercurial.cc
@@ -19,23 +19,23 @@ static void prim_fetchMercurial(EvalState & state, const PosIdx pos, Value * * a
if (args[0]->type() == nAttrs) {
- state.forceAttrs(*args[0], pos);
-
for (auto & attr : *args[0]->attrs) {
std::string_view n(state.symbols[attr.name]);
if (n == "url")
- url = state.coerceToString(attr.pos, *attr.value, context, false, false).toOwned();
+ url = state.coerceToString(attr.pos, *attr.value, context,
+ "while evaluating the `url` attribute passed to builtins.fetchMercurial",
+ false, false).toOwned();
else if (n == "rev") {
// Ugly: unlike fetchGit, here the "rev" attribute can
// be both a revision or a branch/tag name.
- auto value = state.forceStringNoCtx(*attr.value, attr.pos);
+ auto value = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `rev` attribute passed to builtins.fetchMercurial");
if (std::regex_match(value.begin(), value.end(), revRegex))
rev = Hash::parseAny(value, htSHA1);
else
ref = value;
}
else if (n == "name")
- name = state.forceStringNoCtx(*attr.value, attr.pos);
+ name = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the `name` attribute passed to builtins.fetchMercurial");
else
throw EvalError({
.msg = hintfmt("unsupported argument '%s' to 'fetchMercurial'", state.symbols[attr.name]),
@@ -50,7 +50,9 @@ static void prim_fetchMercurial(EvalState & state, const PosIdx pos, Value * * a
});
} else
- url = state.coerceToString(pos, *args[0], context, false, false).toOwned();
+ url = state.coerceToString(pos, *args[0], context,
+ "while evaluating the first argument passed to builtins.fetchMercurial",
+ false, false).toOwned();
// FIXME: git externals probably can be used to bypass the URI
// whitelist. Ah well.
diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc
index fb392a6e8..83d93b75c 100644
--- a/src/libexpr/primops/fetchTree.cc
+++ b/src/libexpr/primops/fetchTree.cc
@@ -102,7 +102,7 @@ static void fetchTree(
state.forceValue(*args[0], pos);
if (args[0]->type() == nAttrs) {
- state.forceAttrs(*args[0], pos);
+ state.forceAttrs(*args[0], pos, "while evaluating the argument passed to builtins.fetchTree");
fetchers::Attrs attrs;
@@ -112,7 +112,7 @@ static void fetchTree(
.msg = hintfmt("unexpected attribute 'type'"),
.errPos = state.positions[pos]
}));
- type = state.forceStringNoCtx(*aType->value, aType->pos);
+ type = state.forceStringNoCtx(*aType->value, aType->pos, "while evaluating the `type` attribute passed to builtins.fetchTree");
} else if (!type)
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("attribute 'type' is missing in call to 'fetchTree'"),
@@ -125,7 +125,7 @@ static void fetchTree(
if (attr.name == state.sType) continue;
state.forceValue(*attr.value, attr.pos);
if (attr.value->type() == nPath || attr.value->type() == nString) {
- auto s = state.coerceToString(attr.pos, *attr.value, context, false, false).toOwned();
+ auto s = state.coerceToString(attr.pos, *attr.value, context, "", false, false).toOwned();
attrs.emplace(state.symbols[attr.name],
state.symbols[attr.name] == "url"
? type == "git"
@@ -151,7 +151,9 @@ static void fetchTree(
input = fetchers::Input::fromAttrs(std::move(attrs));
} else {
- auto url = state.coerceToString(pos, *args[0], context, false, false).toOwned();
+ auto url = state.coerceToString(pos, *args[0], context,
+ "while evaluating the first argument passed to the fetcher",
+ false, false).toOwned();
if (type == "git") {
fetchers::Attrs attrs;
@@ -195,16 +197,14 @@ static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v
if (args[0]->type() == nAttrs) {
- state.forceAttrs(*args[0], pos);
-
for (auto & attr : *args[0]->attrs) {
std::string_view n(state.symbols[attr.name]);
if (n == "url")
- url = state.forceStringNoCtx(*attr.value, attr.pos);
+ url = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the url we should fetch");
else if (n == "sha256")
- expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos), htSHA256);
+ expectedHash = newHashAllowEmpty(state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the sha256 of the content we should fetch"), htSHA256);
else if (n == "name")
- name = state.forceStringNoCtx(*attr.value, attr.pos);
+ name = state.forceStringNoCtx(*attr.value, attr.pos, "while evaluating the name of the content we should fetch");
else
state.debugThrowLastTrace(EvalError({
.msg = hintfmt("unsupported argument '%s' to '%s'", n, who),
@@ -218,7 +218,7 @@ static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v
.errPos = state.positions[pos]
}));
} else
- url = state.forceStringNoCtx(*args[0], pos);
+ url = state.forceStringNoCtx(*args[0], pos, "while evaluating the url we should fetch");
if (who == "fetchTarball")
url = evalSettings.resolvePseudoUrl(*url);
diff --git a/src/libexpr/primops/fromTOML.cc b/src/libexpr/primops/fromTOML.cc
index 9753e2ac9..8a5231781 100644
--- a/src/libexpr/primops/fromTOML.cc
+++ b/src/libexpr/primops/fromTOML.cc
@@ -7,7 +7,7 @@ namespace nix {
static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, Value & val)
{
- auto toml = state.forceStringNoCtx(*args[0], pos);
+ auto toml = state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.fromTOML");
std::istringstream tomlStream(std::string{toml});
diff --git a/src/libexpr/tests/error_traces.cc b/src/libexpr/tests/error_traces.cc
new file mode 100644
index 000000000..5e2213f69
--- /dev/null
+++ b/src/libexpr/tests/error_traces.cc
@@ -0,0 +1,1298 @@
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "libexprtests.hh"
+
+namespace nix {
+
+ using namespace testing;
+
+ // Testing eval of PrimOp's
+ class ErrorTraceTest : public LibExprTest { };
+
+ TEST_F(ErrorTraceTest, TraceBuilder) {
+ ASSERT_THROW(
+ state.error("Not much").debugThrow<EvalError>(),
+ EvalError
+ );
+
+ ASSERT_THROW(
+ state.error("Not much").withTrace(noPos, "No more").debugThrow<EvalError>(),
+ EvalError
+ );
+
+ ASSERT_THROW(
+ try {
+ try {
+ state.error("Not much").withTrace(noPos, "No more").debugThrow<EvalError>();
+ } catch (Error & e) {
+ e.addTrace(state.positions[noPos], "Something", "");
+ throw;
+ }
+ } catch (BaseError & e) {
+ ASSERT_EQ(PrintToString(e.info().msg),
+ PrintToString(hintfmt("Not much")));
+ auto trace = e.info().traces.rbegin();
+ ASSERT_EQ(e.info().traces.size(), 2);
+ ASSERT_EQ(PrintToString(trace->hint),
+ PrintToString(hintfmt("No more")));
+ trace++;
+ ASSERT_EQ(PrintToString(trace->hint),
+ PrintToString(hintfmt("Something")));
+ throw;
+ }
+ , EvalError
+ );
+ }
+
+ TEST_F(ErrorTraceTest, NestedThrows) {
+ try {
+ state.error("Not much").withTrace(noPos, "No more").debugThrow<EvalError>();
+ } catch (BaseError & e) {
+ try {
+ state.error("Not much more").debugThrow<EvalError>();
+ } catch (Error & e2) {
+ e.addTrace(state.positions[noPos], "Something", "");
+ //e2.addTrace(state.positions[noPos], "Something", "");
+ ASSERT_TRUE(e.info().traces.size() == 2);
+ ASSERT_TRUE(e2.info().traces.size() == 0);
+ ASSERT_FALSE(&e.info() == &e2.info());
+ }
+ }
+ }
+
+#define ASSERT_TRACE1(args, type, message) \
+ ASSERT_THROW( \
+ std::string expr(args); \
+ std::string name = expr.substr(0, expr.find(" ")); \
+ try { \
+ Value v = eval("builtins." args); \
+ state.forceValueDeep(v); \
+ } catch (BaseError & e) { \
+ ASSERT_EQ(PrintToString(e.info().msg), \
+ PrintToString(message)); \
+ ASSERT_EQ(e.info().traces.size(), 1) << "while testing " args << std::endl << e.what(); \
+ auto trace = e.info().traces.rbegin(); \
+ ASSERT_EQ(PrintToString(trace->hint), \
+ PrintToString(hintfmt("while calling the '%s' builtin", name))); \
+ throw; \
+ } \
+ , type \
+ )
+
+#define ASSERT_TRACE2(args, type, message, context) \
+ ASSERT_THROW( \
+ std::string expr(args); \
+ std::string name = expr.substr(0, expr.find(" ")); \
+ try { \
+ Value v = eval("builtins." args); \
+ state.forceValueDeep(v); \
+ } catch (BaseError & e) { \
+ ASSERT_EQ(PrintToString(e.info().msg), \
+ PrintToString(message)); \
+ ASSERT_EQ(e.info().traces.size(), 2) << "while testing " args << std::endl << e.what(); \
+ auto trace = e.info().traces.rbegin(); \
+ ASSERT_EQ(PrintToString(trace->hint), \
+ PrintToString(context)); \
+ ++trace; \
+ ASSERT_EQ(PrintToString(trace->hint), \
+ PrintToString(hintfmt("while calling the '%s' builtin", name))); \
+ throw; \
+ } \
+ , type \
+ )
+
+ TEST_F(ErrorTraceTest, genericClosure) {
+ ASSERT_TRACE2("genericClosure 1",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.genericClosure"));
+
+ ASSERT_TRACE2("genericClosure {}",
+ TypeError,
+ hintfmt("attribute '%s' missing", "startSet"),
+ hintfmt("in the attrset passed as argument to builtins.genericClosure"));
+
+ ASSERT_TRACE2("genericClosure { startSet = 1; }",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating the 'startSet' attribute passed as argument to builtins.genericClosure"));
+
+ ASSERT_TRACE2("genericClosure { startSet = [{ key = 1;}]; operator = true; }",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "a Boolean"),
+ hintfmt("while evaluating the 'operator' attribute passed as argument to builtins.genericClosure"));
+
+ ASSERT_TRACE2("genericClosure { startSet = [{ key = 1;}]; operator = item: true; }",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a Boolean"),
+ hintfmt("while evaluating the return value of the `operator` passed to builtins.genericClosure"));
+
+ ASSERT_TRACE2("genericClosure { startSet = [{ key = 1;}]; operator = item: [ true ]; }",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a Boolean"),
+ hintfmt("while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure"));
+
+ ASSERT_TRACE2("genericClosure { startSet = [{ key = 1;}]; operator = item: [ {} ]; }",
+ TypeError,
+ hintfmt("attribute '%s' missing", "key"),
+ hintfmt("in one of the attrsets generated by (or initially passed to) builtins.genericClosure"));
+
+ ASSERT_TRACE2("genericClosure { startSet = [{ key = 1;}]; operator = item: [{ key = ''a''; }]; }",
+ EvalError,
+ hintfmt("cannot compare %s with %s", "a string", "an integer"),
+ hintfmt("while comparing the `key` attributes of two genericClosure elements"));
+
+ ASSERT_TRACE2("genericClosure { startSet = [ true ]; operator = item: [{ key = ''a''; }]; }",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a Boolean"),
+ hintfmt("while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, replaceStrings) {
+ ASSERT_TRACE2("replaceStrings 0 0 {}",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.replaceStrings"));
+
+ ASSERT_TRACE2("replaceStrings [] 0 {}",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating the second argument passed to builtins.replaceStrings"));
+
+ ASSERT_TRACE1("replaceStrings [ 0 ] [] {}",
+ EvalError,
+ hintfmt("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths"));
+
+ ASSERT_TRACE2("replaceStrings [ 1 ] [ \"new\" ] {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating one of the strings to replace passed to builtins.replaceStrings"));
+
+ ASSERT_TRACE2("replaceStrings [ \"old\" ] [ true ] {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a Boolean"),
+ hintfmt("while evaluating one of the replacement strings passed to builtins.replaceStrings"));
+
+ ASSERT_TRACE2("replaceStrings [ \"old\" ] [ \"new\" ] {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a set"),
+ hintfmt("while evaluating the third argument passed to builtins.replaceStrings"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, scopedImport) {
+ }
+
+
+ TEST_F(ErrorTraceTest, import) {
+ }
+
+
+ TEST_F(ErrorTraceTest, typeOf) {
+ }
+
+
+ TEST_F(ErrorTraceTest, isNull) {
+ }
+
+
+ TEST_F(ErrorTraceTest, isFunction) {
+ }
+
+
+ TEST_F(ErrorTraceTest, isInt) {
+ }
+
+
+ TEST_F(ErrorTraceTest, isFloat) {
+ }
+
+
+ TEST_F(ErrorTraceTest, isString) {
+ }
+
+
+ TEST_F(ErrorTraceTest, isBool) {
+ }
+
+
+ TEST_F(ErrorTraceTest, isPath) {
+ }
+
+
+ TEST_F(ErrorTraceTest, break) {
+ }
+
+
+ TEST_F(ErrorTraceTest, abort) {
+ }
+
+
+ TEST_F(ErrorTraceTest, throw) {
+ }
+
+
+ TEST_F(ErrorTraceTest, addErrorContext) {
+ }
+
+
+ TEST_F(ErrorTraceTest, ceil) {
+ ASSERT_TRACE2("ceil \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a float was expected", "a string"),
+ hintfmt("while evaluating the first argument passed to builtins.ceil"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, floor) {
+ ASSERT_TRACE2("floor \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a float was expected", "a string"),
+ hintfmt("while evaluating the first argument passed to builtins.floor"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, tryEval) {
+ }
+
+
+ TEST_F(ErrorTraceTest, getEnv) {
+ ASSERT_TRACE2("getEnv [ ]",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.getEnv"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, seq) {
+ }
+
+
+ TEST_F(ErrorTraceTest, deepSeq) {
+ }
+
+
+ TEST_F(ErrorTraceTest, trace) {
+ }
+
+
+ TEST_F(ErrorTraceTest, placeholder) {
+ ASSERT_TRACE2("placeholder []",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.placeholder"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, toPath) {
+ ASSERT_TRACE2("toPath []",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.toPath"));
+
+ ASSERT_TRACE2("toPath \"foo\"",
+ EvalError,
+ hintfmt("string '%s' doesn't represent an absolute path", "foo"),
+ hintfmt("while evaluating the first argument passed to builtins.toPath"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, storePath) {
+ ASSERT_TRACE2("storePath true",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a Boolean"),
+ hintfmt("while evaluating the first argument passed to builtins.storePath"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, pathExists) {
+ ASSERT_TRACE2("pathExists []",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a list"),
+ hintfmt("while realising the context of a path"));
+
+ ASSERT_TRACE2("pathExists \"zorglub\"",
+ EvalError,
+ hintfmt("string '%s' doesn't represent an absolute path", "zorglub"),
+ hintfmt("while realising the context of a path"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, baseNameOf) {
+ ASSERT_TRACE2("baseNameOf []",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.baseNameOf"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, dirOf) {
+ }
+
+
+ TEST_F(ErrorTraceTest, readFile) {
+ }
+
+
+ TEST_F(ErrorTraceTest, findFile) {
+ }
+
+
+ TEST_F(ErrorTraceTest, hashFile) {
+ }
+
+
+ TEST_F(ErrorTraceTest, readDir) {
+ }
+
+
+ TEST_F(ErrorTraceTest, toXML) {
+ }
+
+
+ TEST_F(ErrorTraceTest, toJSON) {
+ }
+
+
+ TEST_F(ErrorTraceTest, fromJSON) {
+ }
+
+
+ TEST_F(ErrorTraceTest, toFile) {
+ }
+
+
+ TEST_F(ErrorTraceTest, filterSource) {
+ ASSERT_TRACE2("filterSource [] []",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a list"),
+ hintfmt("while evaluating the second argument (the path to filter) passed to builtins.filterSource"));
+
+ ASSERT_TRACE2("filterSource [] \"foo\"",
+ EvalError,
+ hintfmt("string '%s' doesn't represent an absolute path", "foo"),
+ hintfmt("while evaluating the second argument (the path to filter) passed to builtins.filterSource"));
+
+ ASSERT_TRACE2("filterSource [] ./.",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.filterSource"));
+
+ // Usupported by store "dummy"
+
+ // ASSERT_TRACE2("filterSource (_: 1) ./.",
+ // TypeError,
+ // hintfmt("attempt to call something which is not a function but %s", "an integer"),
+ // hintfmt("while adding path '/home/layus/projects/nix'"));
+
+ // ASSERT_TRACE2("filterSource (_: _: 1) ./.",
+ // TypeError,
+ // hintfmt("value is %s while a Boolean was expected", "an integer"),
+ // hintfmt("while evaluating the return value of the path filter function"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, path) {
+ }
+
+
+ TEST_F(ErrorTraceTest, attrNames) {
+ ASSERT_TRACE2("attrNames []",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a list"),
+ hintfmt("while evaluating the argument passed to builtins.attrNames"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, attrValues) {
+ ASSERT_TRACE2("attrValues []",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a list"),
+ hintfmt("while evaluating the argument passed to builtins.attrValues"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, getAttr) {
+ ASSERT_TRACE2("getAttr [] []",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.getAttr"));
+
+ ASSERT_TRACE2("getAttr \"foo\" []",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a list"),
+ hintfmt("while evaluating the second argument passed to builtins.getAttr"));
+
+ ASSERT_TRACE2("getAttr \"foo\" {}",
+ TypeError,
+ hintfmt("attribute '%s' missing", "foo"),
+ hintfmt("in the attribute set under consideration"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, unsafeGetAttrPos) {
+ }
+
+
+ TEST_F(ErrorTraceTest, hasAttr) {
+ ASSERT_TRACE2("hasAttr [] []",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.hasAttr"));
+
+ ASSERT_TRACE2("hasAttr \"foo\" []",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a list"),
+ hintfmt("while evaluating the second argument passed to builtins.hasAttr"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, isAttrs) {
+ }
+
+
+ TEST_F(ErrorTraceTest, removeAttrs) {
+ ASSERT_TRACE2("removeAttrs \"\" \"\"",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a string"),
+ hintfmt("while evaluating the first argument passed to builtins.removeAttrs"));
+
+ ASSERT_TRACE2("removeAttrs \"\" [ 1 ]",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a string"),
+ hintfmt("while evaluating the first argument passed to builtins.removeAttrs"));
+
+ ASSERT_TRACE2("removeAttrs \"\" [ \"1\" ]",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a string"),
+ hintfmt("while evaluating the first argument passed to builtins.removeAttrs"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, listToAttrs) {
+ ASSERT_TRACE2("listToAttrs 1",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating the argument passed to builtins.listToAttrs"));
+
+ ASSERT_TRACE2("listToAttrs [ 1 ]",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "an integer"),
+ hintfmt("while evaluating an element of the list passed to builtins.listToAttrs"));
+
+ ASSERT_TRACE2("listToAttrs [ {} ]",
+ TypeError,
+ hintfmt("attribute '%s' missing", "name"),
+ hintfmt("in a {name=...; value=...;} pair"));
+
+ ASSERT_TRACE2("listToAttrs [ { name = 1; } ]",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the `name` attribute of an element of the list passed to builtins.listToAttrs"));
+
+ ASSERT_TRACE2("listToAttrs [ { name = \"foo\"; } ]",
+ TypeError,
+ hintfmt("attribute '%s' missing", "value"),
+ hintfmt("in a {name=...; value=...;} pair"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, intersectAttrs) {
+ ASSERT_TRACE2("intersectAttrs [] []",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.intersectAttrs"));
+
+ ASSERT_TRACE2("intersectAttrs {} []",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a list"),
+ hintfmt("while evaluating the second argument passed to builtins.intersectAttrs"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, catAttrs) {
+ ASSERT_TRACE2("catAttrs [] {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.catAttrs"));
+
+ ASSERT_TRACE2("catAttrs \"foo\" {}",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a set"),
+ hintfmt("while evaluating the second argument passed to builtins.catAttrs"));
+
+ ASSERT_TRACE2("catAttrs \"foo\" [ 1 ]",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "an integer"),
+ hintfmt("while evaluating an element in the list passed as second argument to builtins.catAttrs"));
+
+ ASSERT_TRACE2("catAttrs \"foo\" [ { foo = 1; } 1 { bar = 5;} ]",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "an integer"),
+ hintfmt("while evaluating an element in the list passed as second argument to builtins.catAttrs"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, functionArgs) {
+ ASSERT_TRACE1("functionArgs {}",
+ TypeError,
+ hintfmt("'functionArgs' requires a function"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, mapAttrs) {
+ ASSERT_TRACE2("mapAttrs [] []",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a list"),
+ hintfmt("while evaluating the second argument passed to builtins.mapAttrs"));
+
+ // XXX: defered
+ // ASSERT_TRACE2("mapAttrs \"\" { foo.bar = 1; }",
+ // TypeError,
+ // hintfmt("attempt to call something which is not a function but %s", "a string"),
+ // hintfmt("while evaluating the attribute 'foo'"));
+
+ // ASSERT_TRACE2("mapAttrs (x: x + \"1\") { foo.bar = 1; }",
+ // TypeError,
+ // hintfmt("attempt to call something which is not a function but %s", "a string"),
+ // hintfmt("while evaluating the attribute 'foo'"));
+
+ // ASSERT_TRACE2("mapAttrs (x: y: x + 1) { foo.bar = 1; }",
+ // TypeError,
+ // hintfmt("cannot coerce %s to a string", "an integer"),
+ // hintfmt("while evaluating a path segment"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, zipAttrsWith) {
+ ASSERT_TRACE2("zipAttrsWith [] [ 1 ]",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "a list"),
+ hintfmt("while evaluating the first argument passed to builtins.zipAttrsWith"));
+
+ ASSERT_TRACE2("zipAttrsWith (_: 1) [ 1 ]",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "an integer"),
+ hintfmt("while evaluating a value of the list passed as second argument to builtins.zipAttrsWith"));
+
+ // XXX: How to properly tell that the fucntion takes two arguments ?
+ // The same question also applies to sort, and maybe others.
+ // Due to lazyness, we only create a thunk, and it fails later on.
+ // ASSERT_TRACE2("zipAttrsWith (_: 1) [ { foo = 1; } ]",
+ // TypeError,
+ // hintfmt("attempt to call something which is not a function but %s", "an integer"),
+ // hintfmt("while evaluating the attribute 'foo'"));
+
+ // XXX: Also deferred deeply
+ // ASSERT_TRACE2("zipAttrsWith (a: b: a + b) [ { foo = 1; } { foo = 2; } ]",
+ // TypeError,
+ // hintfmt("cannot coerce %s to a string", "a list"),
+ // hintfmt("while evaluating a path segment"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, isList) {
+ }
+
+
+ TEST_F(ErrorTraceTest, elemAt) {
+ ASSERT_TRACE2("elemAt \"foo\" (-1)",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the first argument passed to builtins.elemAt"));
+
+ ASSERT_TRACE1("elemAt [] (-1)",
+ Error,
+ hintfmt("list index %d is out of bounds", -1));
+
+ ASSERT_TRACE1("elemAt [\"foo\"] 3",
+ Error,
+ hintfmt("list index %d is out of bounds", 3));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, head) {
+ ASSERT_TRACE2("head 1",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.elemAt"));
+
+ ASSERT_TRACE1("head []",
+ Error,
+ hintfmt("list index %d is out of bounds", 0));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, tail) {
+ ASSERT_TRACE2("tail 1",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.tail"));
+
+ ASSERT_TRACE1("tail []",
+ Error,
+ hintfmt("'tail' called on an empty list"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, map) {
+ ASSERT_TRACE2("map 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.map"));
+
+ ASSERT_TRACE2("map 1 [ 1 ]",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.map"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, filter) {
+ ASSERT_TRACE2("filter 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.filter"));
+
+ ASSERT_TRACE2("filter 1 [ \"foo\" ]",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.filter"));
+
+ ASSERT_TRACE2("filter (_: 5) [ \"foo\" ]",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "an integer"),
+ hintfmt("while evaluating the return value of the filtering function passed to builtins.filter"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, elem) {
+ ASSERT_TRACE2("elem 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.elem"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, concatLists) {
+ ASSERT_TRACE2("concatLists 1",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.concatLists"));
+
+ ASSERT_TRACE2("concatLists [ 1 ]",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating a value of the list passed to builtins.concatLists"));
+
+ ASSERT_TRACE2("concatLists [ [1] \"foo\" ]",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating a value of the list passed to builtins.concatLists"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, length) {
+ ASSERT_TRACE2("length 1",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.length"));
+
+ ASSERT_TRACE2("length \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the first argument passed to builtins.length"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, foldlPrime) {
+ ASSERT_TRACE2("foldl' 1 \"foo\" true",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.foldlStrict"));
+
+ ASSERT_TRACE2("foldl' (_: 1) \"foo\" true",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a Boolean"),
+ hintfmt("while evaluating the third argument passed to builtins.foldlStrict"));
+
+ ASSERT_TRACE1("foldl' (_: 1) \"foo\" [ true ]",
+ TypeError,
+ hintfmt("attempt to call something which is not a function but %s", "an integer"));
+
+ ASSERT_TRACE2("foldl' (a: b: a && b) \"foo\" [ true ]",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "a string"),
+ hintfmt("in the left operand of the AND (&&) operator"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, any) {
+ ASSERT_TRACE2("any 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.any"));
+
+ ASSERT_TRACE2("any (_: 1) \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.any"));
+
+ ASSERT_TRACE2("any (_: 1) [ \"foo\" ]",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "an integer"),
+ hintfmt("while evaluating the return value of the function passed to builtins.any"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, all) {
+ ASSERT_TRACE2("all 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.all"));
+
+ ASSERT_TRACE2("all (_: 1) \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.all"));
+
+ ASSERT_TRACE2("all (_: 1) [ \"foo\" ]",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "an integer"),
+ hintfmt("while evaluating the return value of the function passed to builtins.all"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, genList) {
+ ASSERT_TRACE2("genList 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.genList"));
+
+ ASSERT_TRACE2("genList 1 2",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.genList", "an integer"));
+
+ // XXX: defered
+ // ASSERT_TRACE2("genList (x: x + \"foo\") 2 #TODO",
+ // TypeError,
+ // hintfmt("cannot add %s to an integer", "a string"),
+ // hintfmt("while evaluating anonymous lambda"));
+
+ ASSERT_TRACE1("genList false (-3)",
+ EvalError,
+ hintfmt("cannot create list of size %d", -3));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, sort) {
+ ASSERT_TRACE2("sort 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.sort"));
+
+ ASSERT_TRACE2("sort 1 [ \"foo\" ]",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.sort"));
+
+ ASSERT_TRACE1("sort (_: 1) [ \"foo\" \"bar\" ]",
+ TypeError,
+ hintfmt("attempt to call something which is not a function but %s", "an integer"));
+
+ ASSERT_TRACE2("sort (_: _: 1) [ \"foo\" \"bar\" ]",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "an integer"),
+ hintfmt("while evaluating the return value of the sorting function passed to builtins.sort"));
+
+ // XXX: Trace too deep, need better asserts
+ // ASSERT_TRACE1("sort (a: b: a <= b) [ \"foo\" {} ] # TODO",
+ // TypeError,
+ // hintfmt("cannot compare %s with %s", "a string", "a set"));
+
+ // ASSERT_TRACE1("sort (a: b: a <= b) [ {} {} ] # TODO",
+ // TypeError,
+ // hintfmt("cannot compare %s with %s; values of that type are incomparable", "a set", "a set"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, partition) {
+ ASSERT_TRACE2("partition 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.partition"));
+
+ ASSERT_TRACE2("partition (_: 1) \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.partition"));
+
+ ASSERT_TRACE2("partition (_: 1) [ \"foo\" ]",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "an integer"),
+ hintfmt("while evaluating the return value of the partition function passed to builtins.partition"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, groupBy) {
+ ASSERT_TRACE2("groupBy 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.groupBy"));
+
+ ASSERT_TRACE2("groupBy (_: 1) \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.groupBy"));
+
+ ASSERT_TRACE2("groupBy (x: x) [ \"foo\" \"bar\" 1 ]",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the return value of the grouping function passed to builtins.groupBy"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, concatMap) {
+ ASSERT_TRACE2("concatMap 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a function was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.concatMap"));
+
+ ASSERT_TRACE2("concatMap (x: 1) \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the second argument passed to builtins.concatMap"));
+
+ ASSERT_TRACE2("concatMap (x: 1) [ \"foo\" ] # TODO",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "an integer"),
+ hintfmt("while evaluating the return value of the function passed to buitlins.concatMap"));
+
+ ASSERT_TRACE2("concatMap (x: \"foo\") [ 1 2 ] # TODO",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the return value of the function passed to buitlins.concatMap"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, add) {
+ ASSERT_TRACE2("add \"foo\" 1",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a string"),
+ hintfmt("while evaluating the first argument of the addition"));
+
+ ASSERT_TRACE2("add 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a string"),
+ hintfmt("while evaluating the second argument of the addition"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, sub) {
+ ASSERT_TRACE2("sub \"foo\" 1",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a string"),
+ hintfmt("while evaluating the first argument of the subtraction"));
+
+ ASSERT_TRACE2("sub 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a string"),
+ hintfmt("while evaluating the second argument of the subtraction"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, mul) {
+ ASSERT_TRACE2("mul \"foo\" 1",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a string"),
+ hintfmt("while evaluating the first argument of the multiplication"));
+
+ ASSERT_TRACE2("mul 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a string"),
+ hintfmt("while evaluating the second argument of the multiplication"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, div) {
+ ASSERT_TRACE2("div \"foo\" 1 # TODO: an integer was expected -> a number",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a string"),
+ hintfmt("while evaluating the first operand of the division"));
+
+ ASSERT_TRACE2("div 1 \"foo\"",
+ TypeError,
+ hintfmt("value is %s while a float was expected", "a string"),
+ hintfmt("while evaluating the second operand of the division"));
+
+ ASSERT_TRACE1("div \"foo\" 0",
+ EvalError,
+ hintfmt("division by zero"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, bitAnd) {
+ ASSERT_TRACE2("bitAnd 1.1 2",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a float"),
+ hintfmt("while evaluating the first argument passed to builtins.bitAnd"));
+
+ ASSERT_TRACE2("bitAnd 1 2.2",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a float"),
+ hintfmt("while evaluating the second argument passed to builtins.bitAnd"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, bitOr) {
+ ASSERT_TRACE2("bitOr 1.1 2",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a float"),
+ hintfmt("while evaluating the first argument passed to builtins.bitOr"));
+
+ ASSERT_TRACE2("bitOr 1 2.2",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a float"),
+ hintfmt("while evaluating the second argument passed to builtins.bitOr"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, bitXor) {
+ ASSERT_TRACE2("bitXor 1.1 2",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a float"),
+ hintfmt("while evaluating the first argument passed to builtins.bitXor"));
+
+ ASSERT_TRACE2("bitXor 1 2.2",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a float"),
+ hintfmt("while evaluating the second argument passed to builtins.bitXor"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, lessThan) {
+ ASSERT_TRACE1("lessThan 1 \"foo\"",
+ EvalError,
+ hintfmt("cannot compare %s with %s", "an integer", "a string"));
+
+ ASSERT_TRACE1("lessThan {} {}",
+ EvalError,
+ hintfmt("cannot compare %s with %s; values of that type are incomparable", "a set", "a set"));
+
+ ASSERT_TRACE2("lessThan [ 1 2 ] [ \"foo\" ]",
+ EvalError,
+ hintfmt("cannot compare %s with %s", "an integer", "a string"),
+ hintfmt("while comparing two list elements"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, toString) {
+ ASSERT_TRACE2("toString { a = 1; }",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a set"),
+ hintfmt("while evaluating the first argument passed to builtins.toString"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, substring) {
+ ASSERT_TRACE2("substring {} \"foo\" true",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a set"),
+ hintfmt("while evaluating the first argument (the start offset) passed to builtins.substring"));
+
+ ASSERT_TRACE2("substring 3 \"foo\" true",
+ TypeError,
+ hintfmt("value is %s while an integer was expected", "a string"),
+ hintfmt("while evaluating the second argument (the substring length) passed to builtins.substring"));
+
+ ASSERT_TRACE2("substring 0 3 {}",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a set"),
+ hintfmt("while evaluating the third argument (the string) passed to builtins.substring"));
+
+ ASSERT_TRACE1("substring (-3) 3 \"sometext\"",
+ EvalError,
+ hintfmt("negative start position in 'substring'"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, stringLength) {
+ ASSERT_TRACE2("stringLength {} # TODO: context is missing ???",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a set"),
+ hintfmt("while evaluating the argument passed to builtins.stringLength"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, hashString) {
+ ASSERT_TRACE2("hashString 1 {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.hashString"));
+
+ ASSERT_TRACE1("hashString \"foo\" \"content\"",
+ UsageError,
+ hintfmt("unknown hash algorithm '%s'", "foo"));
+
+ ASSERT_TRACE2("hashString \"sha256\" {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a set"),
+ hintfmt("while evaluating the second argument passed to builtins.hashString"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, match) {
+ ASSERT_TRACE2("match 1 {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.match"));
+
+ ASSERT_TRACE2("match \"foo\" {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a set"),
+ hintfmt("while evaluating the second argument passed to builtins.match"));
+
+ ASSERT_TRACE1("match \"(.*\" \"\"",
+ EvalError,
+ hintfmt("invalid regular expression '%s'", "(.*"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, split) {
+ ASSERT_TRACE2("split 1 {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.split"));
+
+ ASSERT_TRACE2("split \"foo\" {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a set"),
+ hintfmt("while evaluating the second argument passed to builtins.split"));
+
+ ASSERT_TRACE1("split \"f(o*o\" \"1foo2\"",
+ EvalError,
+ hintfmt("invalid regular expression '%s'", "f(o*o"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, concatStringsSep) {
+ ASSERT_TRACE2("concatStringsSep 1 {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the first argument (the separator string) passed to builtins.concatStringsSep"));
+
+ ASSERT_TRACE2("concatStringsSep \"foo\" {}",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a set"),
+ hintfmt("while evaluating the second argument (the list of strings to concat) passed to builtins.concatStringsSep"));
+
+ ASSERT_TRACE2("concatStringsSep \"foo\" [ 1 2 {} ] # TODO: coerce to string is buggy",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "an integer"),
+ hintfmt("while evaluating one element of the list of strings to concat passed to builtins.concatStringsSep"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, parseDrvName) {
+ ASSERT_TRACE2("parseDrvName 1",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.parseDrvName"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, compareVersions) {
+ ASSERT_TRACE2("compareVersions 1 {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.compareVersions"));
+
+ ASSERT_TRACE2("compareVersions \"abd\" {}",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "a set"),
+ hintfmt("while evaluating the second argument passed to builtins.compareVersions"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, splitVersion) {
+ ASSERT_TRACE2("splitVersion 1",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the first argument passed to builtins.splitVersion"));
+
+ }
+
+
+ TEST_F(ErrorTraceTest, traceVerbose) {
+ }
+
+
+ /* // Needs different ASSERTs
+ TEST_F(ErrorTraceTest, derivationStrict) {
+ ASSERT_TRACE2("derivationStrict \"\"",
+ TypeError,
+ hintfmt("value is %s while a set was expected", "a string"),
+ hintfmt("while evaluating the argument passed to builtins.derivationStrict"));
+
+ ASSERT_TRACE2("derivationStrict {}",
+ TypeError,
+ hintfmt("attribute '%s' missing", "name"),
+ hintfmt("in the attrset passed as argument to builtins.derivationStrict"));
+
+ ASSERT_TRACE2("derivationStrict { name = 1; }",
+ TypeError,
+ hintfmt("value is %s while a string was expected", "an integer"),
+ hintfmt("while evaluating the `name` attribute passed to builtins.derivationStrict"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; }",
+ TypeError,
+ hintfmt("required attribute 'builder' missing"),
+ hintfmt("while evaluating derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; __structuredAttrs = 15; }",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "an integer"),
+ hintfmt("while evaluating the `__structuredAttrs` attribute passed to builtins.derivationStrict"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; __ignoreNulls = 15; }",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "an integer"),
+ hintfmt("while evaluating the `__ignoreNulls` attribute passed to builtins.derivationStrict"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; outputHashMode = 15; }",
+ TypeError,
+ hintfmt("invalid value '15' for 'outputHashMode' attribute"),
+ hintfmt("while evaluating the attribute 'outputHashMode' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; outputHashMode = \"custom\"; }",
+ TypeError,
+ hintfmt("invalid value 'custom' for 'outputHashMode' attribute"),
+ hintfmt("while evaluating the attribute 'outputHashMode' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = {}; }",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a set"),
+ hintfmt("while evaluating the attribute 'system' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = {}; }",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a set"),
+ hintfmt("while evaluating the attribute 'outputs' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = \"drv\"; }",
+ TypeError,
+ hintfmt("invalid derivation output name 'drv'"),
+ hintfmt("while evaluating the attribute 'outputs' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = []; }",
+ TypeError,
+ hintfmt("derivation cannot have an empty set of outputs"),
+ hintfmt("while evaluating the attribute 'outputs' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = [ \"drv\" ]; }",
+ TypeError,
+ hintfmt("invalid derivation output name 'drv'"),
+ hintfmt("while evaluating the attribute 'outputs' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = [ \"out\" \"out\" ]; }",
+ TypeError,
+ hintfmt("duplicate derivation output 'out'"),
+ hintfmt("while evaluating the attribute 'outputs' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = \"out\"; __contentAddressed = \"true\"; }",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "a string"),
+ hintfmt("while evaluating the attribute '__contentAddressed' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = \"out\"; __impure = \"true\"; }",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "a string"),
+ hintfmt("while evaluating the attribute '__impure' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = \"out\"; __impure = \"true\"; }",
+ TypeError,
+ hintfmt("value is %s while a Boolean was expected", "a string"),
+ hintfmt("while evaluating the attribute '__impure' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = \"out\"; args = \"foo\"; }",
+ TypeError,
+ hintfmt("value is %s while a list was expected", "a string"),
+ hintfmt("while evaluating the attribute 'args' of derivation 'foo'"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = \"out\"; args = [ {} ]; }",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a set"),
+ hintfmt("while evaluating an element of the argument list"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = \"out\"; args = [ \"a\" {} ]; }",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a set"),
+ hintfmt("while evaluating an element of the argument list"));
+
+ ASSERT_TRACE2("derivationStrict { name = \"foo\"; builder = 1; system = 1; outputs = \"out\"; FOO = {}; }",
+ TypeError,
+ hintfmt("cannot coerce %s to a string", "a set"),
+ hintfmt("while evaluating the attribute 'FOO' of derivation 'foo'"));
+
+ }
+ */
+
+} /* namespace nix */
diff --git a/src/libexpr/tests/primops.cc b/src/libexpr/tests/primops.cc
index bcdc7086b..9cdcf64a1 100644
--- a/src/libexpr/tests/primops.cc
+++ b/src/libexpr/tests/primops.cc
@@ -823,4 +823,10 @@ namespace nix {
for (const auto [n, elem] : enumerate(v.listItems()))
ASSERT_THAT(*elem, IsStringEq(expected[n]));
}
+
+ TEST_F(PrimOpTest, genericClosure_not_strict) {
+ // Operator should not be used when startSet is empty
+ auto v = eval("builtins.genericClosure { startSet = []; }");
+ ASSERT_THAT(v, IsListOfSize(0));
+ }
} /* namespace nix */
diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc
index 6d4365652..d4871a8e2 100644
--- a/src/libmain/shared.cc
+++ b/src/libmain/shared.cc
@@ -404,8 +404,6 @@ int handleExceptions(const std::string & programName, std::function<void()> fun)
return 1;
} catch (BaseError & e) {
logError(e.info());
- if (e.hasTrace() && !loggerSettings.showTrace.get())
- printError("(use '--show-trace' to show detailed location information)");
return e.status;
} catch (std::bad_alloc & e) {
printError(error + "out of memory");
diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc
index 12596ba49..e2a7dab35 100644
--- a/src/libstore/daemon.cc
+++ b/src/libstore/daemon.cc
@@ -222,7 +222,8 @@ struct ClientSettings
else if (!hasSuffix(s, "/") && trusted.count(s + "/"))
subs.push_back(s + "/");
else
- warn("ignoring untrusted substituter '%s'", s);
+ warn("ignoring untrusted substituter '%s', you are not a trusted user.\n"
+ "Run `man nix.conf` for more information on the `substituters` configuration option.", s);
res = subs;
return true;
};
diff --git a/src/libstore/derivations.hh b/src/libstore/derivations.hh
index 7ee3ded6a..f42c13cdc 100644
--- a/src/libstore/derivations.hh
+++ b/src/libstore/derivations.hh
@@ -13,6 +13,7 @@
namespace nix {
+class Store;
/* Abstract syntax of derivations. */
diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh
index 7111def92..c3ccb5e11 100644
--- a/src/libstore/globals.hh
+++ b/src/libstore/globals.hh
@@ -570,11 +570,15 @@ public:
{"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="},
"trusted-public-keys",
R"(
- A whitespace-separated list of public keys. When paths are copied
- from another Nix store (such as a binary cache), they must be
- signed with one of these keys. For example:
- `cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
- hydra.nixos.org-1:CNHJZBh9K4tP3EKF6FkkgeVYsS3ohTl+oS0Qa8bezVs=`.
+ A whitespace-separated list of public keys.
+
+ At least one of the following condition must be met
+ for Nix to accept copying a store object from another
+ Nix store (such as a substituter):
+
+ - the store object has been signed using a key in the trusted keys list
+ - the [`require-sigs`](#conf-require-sigs) option has been set to `false`
+ - the store object is [output-addressed](@docroot@/glossary.md#gloss-output-addressed-store-object)
)",
{"binary-cache-public-keys"}};
@@ -670,13 +674,14 @@ public:
independently. Lower value means higher priority.
The default is `https://cache.nixos.org`, with a Priority of 40.
- Nix will copy a store path from a remote store only if one
- of the following is true:
+ At least one of the following conditions must be met for Nix to use
+ a substituter:
- - the store object is signed by one of the [`trusted-public-keys`](#conf-trusted-public-keys)
- the substituter is in the [`trusted-substituters`](#conf-trusted-substituters) list
- - the [`require-sigs`](#conf-require-sigs) option has been set to `false`
- - the store object is [output-addressed](@docroot@/glossary.md#gloss-output-addressed-store-object)
+ - the user calling Nix is in the [`trusted-users`](#conf-trusted-users) list
+
+ In addition, each store path should be trusted as described
+ in [`trusted-public-keys`](#conf-trusted-public-keys)
)",
{"binary-caches"}};
diff --git a/src/libstore/outputs-spec.cc b/src/libstore/outputs-spec.cc
index 096443cb2..e26c38138 100644
--- a/src/libstore/outputs-spec.cc
+++ b/src/libstore/outputs-spec.cc
@@ -1,8 +1,10 @@
+#include <regex>
+#include <nlohmann/json.hpp>
+
#include "util.hh"
+#include "regex-combinators.hh"
#include "outputs-spec.hh"
-#include "nlohmann/json.hpp"
-
-#include <regex>
+#include "path-regex.hh"
namespace nix {
@@ -18,11 +20,14 @@ bool OutputsSpec::contains(const std::string & outputName) const
}, raw());
}
+static std::string outputSpecRegexStr =
+ regex::either(
+ regex::group(R"(\*)"),
+ regex::group(regex::list(nameRegexStr)));
std::optional<OutputsSpec> OutputsSpec::parseOpt(std::string_view s)
{
- // See checkName() for valid output name characters.
- static std::regex regex(R"((\*)|([a-zA-Z\+\-\._\?=]+(,[a-zA-Z\+\-\._\?=]+)*))");
+ static std::regex regex(std::string { outputSpecRegexStr });
std::smatch match;
std::string s2 { s }; // until some improves std::regex
diff --git a/src/libstore/path-regex.hh b/src/libstore/path-regex.hh
new file mode 100644
index 000000000..6893c3876
--- /dev/null
+++ b/src/libstore/path-regex.hh
@@ -0,0 +1,7 @@
+#pragma once
+
+namespace nix {
+
+static constexpr std::string_view nameRegexStr = R"([0-9a-zA-Z\+\-\._\?=]+)";
+
+}
diff --git a/src/libstore/path.cc b/src/libstore/path.cc
index 392db225e..46be54281 100644
--- a/src/libstore/path.cc
+++ b/src/libstore/path.cc
@@ -8,8 +8,10 @@ static void checkName(std::string_view path, std::string_view name)
{
if (name.empty())
throw BadStorePath("store path '%s' has an empty name", path);
- if (name.size() > 211)
- throw BadStorePath("store path '%s' has a name longer than 211 characters", path);
+ if (name.size() > StorePath::MaxPathLen)
+ throw BadStorePath("store path '%s' has a name longer than '%d characters",
+ StorePath::MaxPathLen, path);
+ // See nameRegexStr for the definition
for (auto c : name)
if (!((c >= '0' && c <= '9')
|| (c >= 'a' && c <= 'z')
diff --git a/src/libstore/path.hh b/src/libstore/path.hh
index 0694b4c18..6a8f027f9 100644
--- a/src/libstore/path.hh
+++ b/src/libstore/path.hh
@@ -5,7 +5,6 @@
namespace nix {
-class Store;
struct Hash;
class StorePath
@@ -17,6 +16,8 @@ public:
/* Size of the hash part of store paths, in base-32 characters. */
constexpr static size_t HashLen = 32; // i.e. 160 bits
+ constexpr static size_t MaxPathLen = 211;
+
StorePath() = delete;
StorePath(std::string_view baseName);
diff --git a/src/libstore/realisation.hh b/src/libstore/realisation.hh
index 911c61909..62561fce3 100644
--- a/src/libstore/realisation.hh
+++ b/src/libstore/realisation.hh
@@ -7,6 +7,8 @@
namespace nix {
+class Store;
+
struct DrvOutput {
// The hash modulo of the derivation
Hash drvHash;
diff --git a/src/libstore/tests/libstoretests.hh b/src/libstore/tests/libstoretests.hh
new file mode 100644
index 000000000..05397659b
--- /dev/null
+++ b/src/libstore/tests/libstoretests.hh
@@ -0,0 +1,23 @@
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+#include "store-api.hh"
+
+namespace nix {
+
+class LibStoreTest : public ::testing::Test {
+ public:
+ static void SetUpTestSuite() {
+ initLibStore();
+ }
+
+ protected:
+ LibStoreTest()
+ : store(openStore("dummy://"))
+ { }
+
+ ref<Store> store;
+};
+
+
+} /* namespace nix */
diff --git a/src/libstore/tests/local.mk b/src/libstore/tests/local.mk
index f74295d97..a2cf8a0cf 100644
--- a/src/libstore/tests/local.mk
+++ b/src/libstore/tests/local.mk
@@ -12,4 +12,4 @@ libstore-tests_CXXFLAGS += -I src/libstore -I src/libutil
libstore-tests_LIBS = libstore libutil
-libstore-tests_LDFLAGS := $(GTEST_LIBS)
+libstore-tests_LDFLAGS := -lrapidcheck $(GTEST_LIBS)
diff --git a/src/libstore/tests/outputs-spec.cc b/src/libstore/tests/outputs-spec.cc
index 836ba7e82..06e4cabbd 100644
--- a/src/libstore/tests/outputs-spec.cc
+++ b/src/libstore/tests/outputs-spec.cc
@@ -47,6 +47,13 @@ TEST(OutputsSpec, names_underscore) {
ASSERT_EQ(expected.to_string(), str);
}
+TEST(OutputsSpec, names_numberic) {
+ std::string_view str = "01";
+ OutputsSpec expected = OutputsSpec::Names { "01" };
+ ASSERT_EQ(OutputsSpec::parse(str), expected);
+ ASSERT_EQ(expected.to_string(), str);
+}
+
TEST(OutputsSpec, names_out_bin) {
OutputsSpec expected = OutputsSpec::Names { "out", "bin" };
ASSERT_EQ(OutputsSpec::parse("out,bin"), expected);
diff --git a/src/libstore/tests/path.cc b/src/libstore/tests/path.cc
new file mode 100644
index 000000000..8ea252c92
--- /dev/null
+++ b/src/libstore/tests/path.cc
@@ -0,0 +1,144 @@
+#include <regex>
+
+#include <nlohmann/json.hpp>
+#include <gtest/gtest.h>
+#include <rapidcheck/gtest.h>
+
+#include "path-regex.hh"
+#include "store-api.hh"
+
+#include "libstoretests.hh"
+
+namespace nix {
+
+#define STORE_DIR "/nix/store/"
+#define HASH_PART "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q"
+
+class StorePathTest : public LibStoreTest
+{
+};
+
+static std::regex nameRegex { std::string { nameRegexStr } };
+
+#define TEST_DONT_PARSE(NAME, STR) \
+ TEST_F(StorePathTest, bad_ ## NAME) { \
+ std::string_view str = \
+ STORE_DIR HASH_PART "-" STR; \
+ ASSERT_THROW( \
+ store->parseStorePath(str), \
+ BadStorePath); \
+ std::string name { STR }; \
+ EXPECT_FALSE(std::regex_match(name, nameRegex)); \
+ }
+
+TEST_DONT_PARSE(empty, "")
+TEST_DONT_PARSE(garbage, "&*()")
+TEST_DONT_PARSE(double_star, "**")
+TEST_DONT_PARSE(star_first, "*,foo")
+TEST_DONT_PARSE(star_second, "foo,*")
+TEST_DONT_PARSE(bang, "foo!o")
+
+#undef TEST_DONT_PARSE
+
+#define TEST_DO_PARSE(NAME, STR) \
+ TEST_F(StorePathTest, good_ ## NAME) { \
+ std::string_view str = \
+ STORE_DIR HASH_PART "-" STR; \
+ auto p = store->parseStorePath(str); \
+ std::string name { p.name() }; \
+ EXPECT_TRUE(std::regex_match(name, nameRegex)); \
+ }
+
+// 0-9 a-z A-Z + - . _ ? =
+
+TEST_DO_PARSE(numbers, "02345")
+TEST_DO_PARSE(lower_case, "foo")
+TEST_DO_PARSE(upper_case, "FOO")
+TEST_DO_PARSE(plus, "foo+bar")
+TEST_DO_PARSE(dash, "foo-dev")
+TEST_DO_PARSE(underscore, "foo_bar")
+TEST_DO_PARSE(period, "foo.txt")
+TEST_DO_PARSE(question_mark, "foo?why")
+TEST_DO_PARSE(equals_sign, "foo=foo")
+
+#undef TEST_DO_PARSE
+
+// For rapidcheck
+void showValue(const StorePath & p, std::ostream & os) {
+ os << p.to_string();
+}
+
+}
+
+namespace rc {
+using namespace nix;
+
+template<>
+struct Arbitrary<StorePath> {
+ static Gen<StorePath> arbitrary();
+};
+
+Gen<StorePath> Arbitrary<StorePath>::arbitrary()
+{
+ auto len = *gen::inRange<size_t>(1, StorePath::MaxPathLen);
+
+ std::string pre { HASH_PART "-" };
+ pre.reserve(pre.size() + len);
+
+ for (size_t c = 0; c < len; ++c) {
+ switch (auto i = *gen::inRange<uint8_t>(0, 10 + 2 * 26 + 6)) {
+ case 0 ... 9:
+ pre += '0' + i;
+ case 10 ... 35:
+ pre += 'A' + (i - 10);
+ break;
+ case 36 ... 61:
+ pre += 'a' + (i - 36);
+ break;
+ case 62:
+ pre += '+';
+ break;
+ case 63:
+ pre += '-';
+ break;
+ case 64:
+ pre += '.';
+ break;
+ case 65:
+ pre += '_';
+ break;
+ case 66:
+ pre += '?';
+ break;
+ case 67:
+ pre += '=';
+ break;
+ default:
+ assert(false);
+ }
+ }
+
+ return gen::just(StorePath { pre });
+}
+
+} // namespace rc
+
+namespace nix {
+
+RC_GTEST_FIXTURE_PROP(
+ StorePathTest,
+ prop_regex_accept,
+ (const StorePath & p))
+{
+ RC_ASSERT(std::regex_match(std::string { p.name() }, nameRegex));
+}
+
+RC_GTEST_FIXTURE_PROP(
+ StorePathTest,
+ prop_round_rip,
+ (const StorePath & p))
+{
+ RC_ASSERT(p == store->parseStorePath(store->printStorePath(p)));
+}
+
+}
diff --git a/src/libutil/error.cc b/src/libutil/error.cc
index 1a1aecea5..e4f0d4677 100644
--- a/src/libutil/error.cc
+++ b/src/libutil/error.cc
@@ -9,9 +9,9 @@ namespace nix {
const std::string nativeSystem = SYSTEM;
-void BaseError::addTrace(std::shared_ptr<AbstractPos> && e, hintformat hint)
+void BaseError::addTrace(std::shared_ptr<AbstractPos> && e, hintformat hint, bool frame)
{
- err.traces.push_front(Trace { .pos = std::move(e), .hint = hint });
+ err.traces.push_front(Trace { .pos = std::move(e), .hint = hint, .frame = frame });
}
// c++ std::exception descendants must have a 'const char* what()' function.
@@ -200,13 +200,125 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s
auto noSource = ANSI_ITALIC " (source not available)" ANSI_NORMAL "\n";
- // traces
- if (showTrace && !einfo.traces.empty()) {
+ /*
+ * Traces
+ * ------
+ *
+ * The semantics of traces is a bit weird. We have only one option to
+ * print them and to make them verbose (--show-trace). In the code they
+ * are always collected, but they are not printed by default. The code
+ * also collects more traces when the option is on. This means that there
+ * is no way to print the simplified traces at all.
+ *
+ * I (layus) designed the code to attach positions to a restricted set of
+ * messages. This means that we have a lot of traces with no position at
+ * all, including most of the base error messages. For example "type
+ * error: found a string while a set was expected" has no position, but
+ * will come with several traces detailing it's precise relation to the
+ * closest know position. This makes erroring without printing traces
+ * quite useless.
+ *
+ * This is why I introduced the idea to always print a few traces on
+ * error. The number 3 is quite arbitrary, and was selected so as not to
+ * clutter the console on error. For the same reason, a trace with an
+ * error position takes more space, and counts as two traces towards the
+ * limit.
+ *
+ * The rest is truncated, unless --show-trace is passed. This preserves
+ * the same bad semantics of --show-trace to both show the trace and
+ * augment it with new data. Not too sure what is the best course of
+ * action.
+ *
+ * The issue is that it is fundamentally hard to provide a trace for a
+ * lazy language. The trace will only cover the current spine of the
+ * evaluation, missing things that have been evaluated before. For
+ * example, most type errors are hard to inspect because there is not
+ * trace for the faulty value. These errors should really print the faulty
+ * value itself.
+ *
+ * In function calls, the --show-trace flag triggers extra traces for each
+ * function invocation. These work as scopes, allowing to follow the
+ * current spine of the evaluation graph. Without that flag, the error
+ * trace should restrict itself to a restricted prefix of that trace,
+ * until the first scope. If we ever get to such a precise error
+ * reporting, there would be no need to add an arbitrary limit here. We
+ * could always print the full trace, and it would just be small without
+ * the flag.
+ *
+ * One idea I had is for XxxError.addTrace() to perform nothing if one
+ * scope has already been traced. Alternatively, we could stop here when
+ * we encounter such a scope instead of after an arbitrary number of
+ * traces. This however requires to augment traces with the notion of
+ * "scope".
+ *
+ * This is particularly visible in code like evalAttrs(...) where we have
+ * to make a decision between the two following options.
+ *
+ * ``` long traces
+ * inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v, const Pos & pos, std::string_view errorCtx)
+ * {
+ * try {
+ * e->eval(*this, env, v);
+ * if (v.type() != nAttrs)
+ * throwTypeError("value is %1% while a set was expected", v);
+ * } catch (Error & e) {
+ * e.addTrace(pos, errorCtx);
+ * throw;
+ * }
+ * }
+ * ```
+ *
+ * ``` short traces
+ * inline void EvalState::evalAttrs(Env & env, Expr * e, Value & v, const Pos & pos, std::string_view errorCtx)
+ * {
+ * e->eval(*this, env, v);
+ * try {
+ * if (v.type() != nAttrs)
+ * throwTypeError("value is %1% while a set was expected", v);
+ * } catch (Error & e) {
+ * e.addTrace(pos, errorCtx);
+ * throw;
+ * }
+ * }
+ * ```
+ *
+ * The second example can be rewritten more concisely, but kept in this
+ * form to highlight the symmetry. The first option adds more information,
+ * because whatever caused an error down the line, in the generic eval
+ * function, will get annotated with the code location that uses and
+ * required it. The second option is less verbose, but does not provide
+ * any context at all as to where and why a failing value was required.
+ *
+ * Scopes would fix that, by adding context only when --show-trace is
+ * passed, and keeping the trace terse otherwise.
+ *
+ */
+
+ // Enough indent to align with with the `... `
+ // prepended to each element of the trace
+ auto ellipsisIndent = " ";
+
+ bool frameOnly = false;
+ if (!einfo.traces.empty()) {
+ size_t count = 0;
for (const auto & trace : einfo.traces) {
+ if (!showTrace && count > 3) {
+ oss << "\n" << ANSI_WARNING "(stack trace truncated; use '--show-trace' to show the full trace)" ANSI_NORMAL << "\n";
+ break;
+ }
+
+ if (trace.hint.str().empty()) continue;
+ if (frameOnly && !trace.frame) continue;
+
+ count++;
+ frameOnly = trace.frame;
+
oss << "\n" << "… " << trace.hint.str() << "\n";
if (trace.pos) {
- oss << "\n" << ANSI_BLUE << "at " ANSI_WARNING << *trace.pos << ANSI_NORMAL << ":";
+ count++;
+
+ oss << "\n" << ellipsisIndent << ANSI_BLUE << "at " ANSI_WARNING << *trace.pos << ANSI_NORMAL << ":";
if (auto loc = trace.pos->getCodeLines()) {
oss << "\n";
diff --git a/src/libutil/error.hh b/src/libutil/error.hh
index c3bb8c0df..0ebeaba61 100644
--- a/src/libutil/error.hh
+++ b/src/libutil/error.hh
@@ -74,6 +74,8 @@ struct AbstractPos
virtual void print(std::ostream & out) const = 0;
std::optional<LinesOfCode> getCodeLines() const;
+
+ virtual ~AbstractPos() = default;
};
std::ostream & operator << (std::ostream & str, const AbstractPos & pos);
@@ -86,6 +88,7 @@ void printCodeLines(std::ostream & out,
struct Trace {
std::shared_ptr<AbstractPos> pos;
hintformat hint;
+ bool frame;
};
struct ErrorInfo {
@@ -114,6 +117,8 @@ protected:
public:
unsigned int status = 1; // exit status
+ BaseError(const BaseError &) = default;
+
template<typename... Args>
BaseError(unsigned int status, const Args & ... args)
: err { .level = lvlError, .msg = hintfmt(args...) }
@@ -152,15 +157,22 @@ public:
const std::string & msg() const { return calcWhat(); }
const ErrorInfo & info() const { calcWhat(); return err; }
+ void pushTrace(Trace trace)
+ {
+ err.traces.push_front(trace);
+ }
+
template<typename... Args>
- void addTrace(std::shared_ptr<AbstractPos> && e, const std::string & fs, const Args & ... args)
+ void addTrace(std::shared_ptr<AbstractPos> && e, std::string_view fs, const Args & ... args)
{
- addTrace(std::move(e), hintfmt(fs, args...));
+ addTrace(std::move(e), hintfmt(std::string(fs), args...));
}
- void addTrace(std::shared_ptr<AbstractPos> && e, hintformat hint);
+ void addTrace(std::shared_ptr<AbstractPos> && e, hintformat hint, bool frame = false);
bool hasTrace() const { return !err.traces.empty(); }
+
+ const ErrorInfo & info() { return err; };
};
#define MakeError(newClass, superClass) \
diff --git a/src/libutil/regex-combinators.hh b/src/libutil/regex-combinators.hh
new file mode 100644
index 000000000..0b997b25a
--- /dev/null
+++ b/src/libutil/regex-combinators.hh
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <string_view>
+
+namespace nix::regex {
+
+// TODO use constexpr string building like
+// https://github.com/akrzemi1/static_string/blob/master/include/ak_toolkit/static_string.hpp
+
+static inline std::string either(std::string_view a, std::string_view b)
+{
+ return std::string { a } + "|" + b;
+}
+
+static inline std::string group(std::string_view a)
+{
+ return std::string { "(" } + a + ")";
+}
+
+static inline std::string many(std::string_view a)
+{
+ return std::string { "(?:" } + a + ")*";
+}
+
+static inline std::string list(std::string_view a)
+{
+ return std::string { a } + many(group("," + a));
+}
+
+}
diff --git a/src/nix-env/user-env.cc b/src/nix-env/user-env.cc
index 4b1202be3..cad7f9c88 100644
--- a/src/nix-env/user-env.cc
+++ b/src/nix-env/user-env.cc
@@ -134,9 +134,9 @@ bool createUserEnv(EvalState & state, DrvInfos & elems,
state.forceValue(topLevel, [&]() { return topLevel.determinePos(noPos); });
PathSet context;
Attr & aDrvPath(*topLevel.attrs->find(state.sDrvPath));
- auto topLevelDrv = state.coerceToStorePath(aDrvPath.pos, *aDrvPath.value, context);
+ auto topLevelDrv = state.coerceToStorePath(aDrvPath.pos, *aDrvPath.value, context, "");
Attr & aOutPath(*topLevel.attrs->find(state.sOutPath));
- auto topLevelOut = state.coerceToStorePath(aOutPath.pos, *aOutPath.value, context);
+ auto topLevelOut = state.coerceToStorePath(aOutPath.pos, *aOutPath.value, context, "");
/* Realise the resulting store expression. */
debug("building user environment");
diff --git a/src/nix/bundle.cc b/src/nix/bundle.cc
index 26db08d80..6ae9460f6 100644
--- a/src/nix/bundle.cc
+++ b/src/nix/bundle.cc
@@ -97,13 +97,13 @@ struct CmdBundle : InstallableCommand
throw Error("the bundler '%s' does not produce a derivation", bundler.what());
PathSet context2;
- auto drvPath = evalState->coerceToStorePath(attr1->pos, *attr1->value, context2);
+ auto drvPath = evalState->coerceToStorePath(attr1->pos, *attr1->value, context2, "");
auto attr2 = vRes->attrs->get(evalState->sOutPath);
if (!attr2)
throw Error("the bundler '%s' does not produce a derivation", bundler.what());
- auto outPath = evalState->coerceToStorePath(attr2->pos, *attr2->value, context2);
+ auto outPath = evalState->coerceToStorePath(attr2->pos, *attr2->value, context2, "");
store->buildPaths({
DerivedPath::Built {
@@ -118,7 +118,7 @@ struct CmdBundle : InstallableCommand
auto * attr = vRes->attrs->get(evalState->sName);
if (!attr)
throw Error("attribute 'name' missing");
- outLink = evalState->forceStringNoCtx(*attr->value, attr->pos);
+ outLink = evalState->forceStringNoCtx(*attr->value, attr->pos, "");
}
// TODO: will crash if not a localFSStore?
diff --git a/src/nix/eval.cc b/src/nix/eval.cc
index ba82b5772..ccee074e9 100644
--- a/src/nix/eval.cc
+++ b/src/nix/eval.cc
@@ -112,7 +112,7 @@ struct CmdEval : MixJSON, InstallableCommand
else if (raw) {
stopProgressBar();
- std::cout << *state->coerceToString(noPos, *v, context);
+ std::cout << *state->coerceToString(noPos, *v, context, "while generating the eval command output");
}
else if (json) {
diff --git a/src/nix/flake.cc b/src/nix/flake.cc
index 17ebf12eb..020c1b182 100644
--- a/src/nix/flake.cc
+++ b/src/nix/flake.cc
@@ -126,12 +126,12 @@ static void enumerateOutputs(EvalState & state, Value & vFlake,
std::function<void(const std::string & name, Value & vProvide, const PosIdx pos)> callback)
{
auto pos = vFlake.determinePos(noPos);
- state.forceAttrs(vFlake, pos);
+ state.forceAttrs(vFlake, pos, "while evaluating a flake to get its outputs");
auto aOutputs = vFlake.attrs->get(state.symbols.create("outputs"));
assert(aOutputs);
- state.forceAttrs(*aOutputs->value, pos);
+ state.forceAttrs(*aOutputs->value, pos, "while evaluating the outputs of a flake");
auto sHydraJobs = state.symbols.create("hydraJobs");
@@ -391,13 +391,13 @@ struct CmdFlakeCheck : FlakeCommand
checkHydraJobs = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
try {
- state->forceAttrs(v, pos);
+ state->forceAttrs(v, pos, "");
if (state->isDerivation(v))
throw Error("jobset should not be a derivation at top-level");
for (auto & attr : *v.attrs) {
- state->forceAttrs(*attr.value, attr.pos);
+ state->forceAttrs(*attr.value, attr.pos, "");
auto attrPath2 = concatStrings(attrPath, ".", state->symbols[attr.name]);
if (state->isDerivation(*attr.value)) {
Activity act(*logger, lvlChatty, actUnknown,
@@ -419,7 +419,7 @@ struct CmdFlakeCheck : FlakeCommand
fmt("checking NixOS configuration '%s'", attrPath));
Bindings & bindings(*state->allocBindings(0));
auto vToplevel = findAlongAttrPath(*state, "config.system.build.toplevel", bindings, v).first;
- state->forceAttrs(*vToplevel, pos);
+ state->forceValue(*vToplevel, pos);
if (!state->isDerivation(*vToplevel))
throw Error("attribute 'config.system.build.toplevel' is not a derivation");
} catch (Error & e) {
@@ -433,12 +433,12 @@ struct CmdFlakeCheck : FlakeCommand
Activity act(*logger, lvlChatty, actUnknown,
fmt("checking template '%s'", attrPath));
- state->forceAttrs(v, pos);
+ state->forceAttrs(v, pos, "");
if (auto attr = v.attrs->get(state->symbols.create("path"))) {
if (attr->name == state->symbols.create("path")) {
PathSet context;
- auto path = state->coerceToPath(attr->pos, *attr->value, context);
+ auto path = state->coerceToPath(attr->pos, *attr->value, context, "");
if (!store->isInStore(path))
throw Error("template '%s' has a bad 'path' attribute");
// TODO: recursively check the flake in 'path'.
@@ -447,7 +447,7 @@ struct CmdFlakeCheck : FlakeCommand
throw Error("template '%s' lacks attribute 'path'", attrPath);
if (auto attr = v.attrs->get(state->symbols.create("description")))
- state->forceStringNoCtx(*attr->value, attr->pos);
+ state->forceStringNoCtx(*attr->value, attr->pos, "");
else
throw Error("template '%s' lacks attribute 'description'", attrPath);
@@ -504,11 +504,11 @@ struct CmdFlakeCheck : FlakeCommand
warn("flake output attribute '%s' is deprecated; use '%s' instead", name, replacement);
if (name == "checks") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
- state->forceAttrs(*attr.value, attr.pos);
+ state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs) {
auto drvPath = checkDerivation(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
@@ -524,7 +524,7 @@ struct CmdFlakeCheck : FlakeCommand
}
else if (name == "formatter") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
@@ -535,11 +535,11 @@ struct CmdFlakeCheck : FlakeCommand
}
else if (name == "packages" || name == "devShells") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
- state->forceAttrs(*attr.value, attr.pos);
+ state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs)
checkDerivation(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
@@ -548,11 +548,11 @@ struct CmdFlakeCheck : FlakeCommand
}
else if (name == "apps") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
- state->forceAttrs(*attr.value, attr.pos);
+ state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs)
checkApp(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
@@ -561,7 +561,7 @@ struct CmdFlakeCheck : FlakeCommand
}
else if (name == "defaultPackage" || name == "devShell") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
@@ -572,7 +572,7 @@ struct CmdFlakeCheck : FlakeCommand
}
else if (name == "defaultApp") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
@@ -583,7 +583,7 @@ struct CmdFlakeCheck : FlakeCommand
}
else if (name == "legacyPackages") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs) {
checkSystemName(state->symbols[attr.name], attr.pos);
// FIXME: do getDerivations?
@@ -594,7 +594,7 @@ struct CmdFlakeCheck : FlakeCommand
checkOverlay(name, vOutput, pos);
else if (name == "overlays") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs)
checkOverlay(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
@@ -604,14 +604,14 @@ struct CmdFlakeCheck : FlakeCommand
checkModule(name, vOutput, pos);
else if (name == "nixosModules") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs)
checkModule(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
else if (name == "nixosConfigurations") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs)
checkNixOSConfiguration(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
@@ -624,14 +624,14 @@ struct CmdFlakeCheck : FlakeCommand
checkTemplate(name, vOutput, pos);
else if (name == "templates") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs)
checkTemplate(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
else if (name == "defaultBundler") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
@@ -642,11 +642,11 @@ struct CmdFlakeCheck : FlakeCommand
}
else if (name == "bundlers") {
- state->forceAttrs(vOutput, pos);
+ state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
- state->forceAttrs(*attr.value, attr.pos);
+ state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs) {
checkBundler(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
diff --git a/src/nix/main.cc b/src/nix/main.cc
index 2c6309c81..d3d2f5b16 100644
--- a/src/nix/main.cc
+++ b/src/nix/main.cc
@@ -199,7 +199,7 @@ static void showHelp(std::vector<std::string> subcommand, MultiCommand & topleve
if (!attr)
throw UsageError("Nix has no subcommand '%s'", concatStringsSep("", subcommand));
- auto markdown = state.forceString(*attr->value);
+ auto markdown = state.forceString(*attr->value, noPos, "while evaluating the lowdown help text");
RunPager pager;
std::cout << renderMarkdownToTerminal(markdown) << "\n";
diff --git a/src/nix/prefetch.cc b/src/nix/prefetch.cc
index ce3288dc1..fc3823406 100644
--- a/src/nix/prefetch.cc
+++ b/src/nix/prefetch.cc
@@ -28,17 +28,17 @@ std::string resolveMirrorUrl(EvalState & state, const std::string & url)
Value vMirrors;
// FIXME: use nixpkgs flake
state.eval(state.parseExprFromString("import <nixpkgs/pkgs/build-support/fetchurl/mirrors.nix>", "."), vMirrors);
- state.forceAttrs(vMirrors, noPos);
+ state.forceAttrs(vMirrors, noPos, "while evaluating the set of all mirrors");
auto mirrorList = vMirrors.attrs->find(state.symbols.create(mirrorName));
if (mirrorList == vMirrors.attrs->end())
throw Error("unknown mirror name '%s'", mirrorName);
- state.forceList(*mirrorList->value, noPos);
+ state.forceList(*mirrorList->value, noPos, "while evaluating one mirror configuration");
if (mirrorList->value->listSize() < 1)
throw Error("mirror URL '%s' did not expand to anything", url);
- std::string mirror(state.forceString(*mirrorList->value->listElems()[0]));
+ std::string mirror(state.forceString(*mirrorList->value->listElems()[0], noPos, "while evaluating the first available mirror"));
return mirror + (hasSuffix(mirror, "/") ? "" : "/") + s.substr(p + 1);
}
@@ -196,29 +196,29 @@ static int main_nix_prefetch_url(int argc, char * * argv)
Value vRoot;
state->evalFile(path, vRoot);
Value & v(*findAlongAttrPath(*state, attrPath, autoArgs, vRoot).first);
- state->forceAttrs(v, noPos);
+ state->forceAttrs(v, noPos, "while evaluating the source attribute to prefetch");
/* Extract the URL. */
auto * attr = v.attrs->get(state->symbols.create("urls"));
if (!attr)
throw Error("attribute 'urls' missing");
- state->forceList(*attr->value, noPos);
+ state->forceList(*attr->value, noPos, "while evaluating the urls to prefetch");
if (attr->value->listSize() < 1)
throw Error("'urls' list is empty");
- url = state->forceString(*attr->value->listElems()[0]);
+ url = state->forceString(*attr->value->listElems()[0], noPos, "while evaluating the first url from the urls list");
/* Extract the hash mode. */
auto attr2 = v.attrs->get(state->symbols.create("outputHashMode"));
if (!attr2)
printInfo("warning: this does not look like a fetchurl call");
else
- unpack = state->forceString(*attr2->value) == "recursive";
+ unpack = state->forceString(*attr2->value, noPos, "while evaluating the outputHashMode of the source to prefetch") == "recursive";
/* Extract the name. */
if (!name) {
auto attr3 = v.attrs->get(state->symbols.create("name"));
if (!attr3)
- name = state->forceString(*attr3->value);
+ name = state->forceString(*attr3->value, noPos, "while evaluating the name of the source to prefetch");
}
}
diff --git a/src/nix/show-config.cc b/src/nix/show-config.cc
index 29944e748..3530584f9 100644
--- a/src/nix/show-config.cc
+++ b/src/nix/show-config.cc
@@ -9,15 +9,44 @@ using namespace nix;
struct CmdShowConfig : Command, MixJSON
{
+ std::optional<std::string> name;
+
+ CmdShowConfig() {
+ expectArgs({
+ .label = {"name"},
+ .optional = true,
+ .handler = {&name},
+ });
+ }
+
std::string description() override
{
- return "show the Nix configuration";
+ return "show the Nix configuration or the value of a specific setting";
}
Category category() override { return catUtility; }
void run() override
{
+ if (name) {
+ if (json) {
+ throw UsageError("'--json' is not supported when specifying a setting name");
+ }
+
+ std::map<std::string, Config::SettingInfo> settings;
+ globalConfig.getSettings(settings);
+ auto setting = settings.find(*name);
+
+ if (setting == settings.end()) {
+ throw Error("could not find setting '%1%'", *name);
+ } else {
+ const auto & value = setting->second.value;
+ logger->cout("%s", value);
+ }
+
+ return;
+ }
+
if (json) {
// FIXME: use appropriate JSON types (bool, ints, etc).
logger->cout("%s", globalConfig.toJSON().dump());
diff --git a/src/nix/upgrade-nix.cc b/src/nix/upgrade-nix.cc
index 2d2453395..17796d6b8 100644
--- a/src/nix/upgrade-nix.cc
+++ b/src/nix/upgrade-nix.cc
@@ -144,7 +144,7 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand
Bindings & bindings(*state->allocBindings(0));
auto v2 = findAlongAttrPath(*state, settings.thisSystem, bindings, *v).first;
- return store->parseStorePath(state->forceString(*v2));
+ return store->parseStorePath(state->forceString(*v2, noPos, "while evaluating the path tho latest nix version"));
}
};
diff --git a/tests/config.sh b/tests/config.sh
index 3d0da3cef..723f575ed 100644
--- a/tests/config.sh
+++ b/tests/config.sh
@@ -51,3 +51,8 @@ exp_features=$(nix show-config | grep '^experimental-features' | cut -d '=' -f 2
[[ $prev != $exp_cores ]]
[[ $exp_cores == "4242" ]]
[[ $exp_features == "flakes nix-command" ]]
+
+# Test that it's possible to retrieve a single setting's value
+val=$(nix show-config | grep '^warn-dirty' | cut -d '=' -f 2 | xargs)
+val2=$(nix show-config warn-dirty)
+[[ $val == $val2 ]]
diff --git a/tests/export-graph.sh b/tests/export-graph.sh
index a1449b34e..4954a6cbc 100644
--- a/tests/export-graph.sh
+++ b/tests/export-graph.sh
@@ -4,7 +4,7 @@ clearStore
clearProfiles
checkRef() {
- nix-store -q --references $TEST_ROOT/result | grep -q "$1" || fail "missing reference $1"
+ nix-store -q --references $TEST_ROOT/result | grep -q "$1"'$' || fail "missing reference $1"
}
# Test the export of the runtime dependency graph.
diff --git a/tests/lang/eval-okay-context-introspection.exp b/tests/lang/eval-okay-context-introspection.exp
index 27ba77dda..03b400cc8 100644
--- a/tests/lang/eval-okay-context-introspection.exp
+++ b/tests/lang/eval-okay-context-introspection.exp
@@ -1 +1 @@
-true
+[ true true true true true true ]
diff --git a/tests/lang/eval-okay-context-introspection.nix b/tests/lang/eval-okay-context-introspection.nix
index 43178bd2e..50a78d946 100644
--- a/tests/lang/eval-okay-context-introspection.nix
+++ b/tests/lang/eval-okay-context-introspection.nix
@@ -18,7 +18,24 @@ let
};
};
- legit-context = builtins.getContext "${path}${drv.outPath}${drv.foo.outPath}${drv.drvPath}";
+ combo-path = "${path}${drv.outPath}${drv.foo.outPath}${drv.drvPath}";
+ legit-context = builtins.getContext combo-path;
- constructed-context = builtins.getContext (builtins.appendContext "" desired-context);
-in legit-context == constructed-context
+ reconstructed-path = builtins.appendContext
+ (builtins.unsafeDiscardStringContext combo-path)
+ desired-context;
+
+ # Eta rule for strings with context.
+ etaRule = str:
+ str == builtins.appendContext
+ (builtins.unsafeDiscardStringContext str)
+ (builtins.getContext str);
+
+in [
+ (legit-context == desired-context)
+ (reconstructed-path == combo-path)
+ (etaRule "foo")
+ (etaRule drv.drvPath)
+ (etaRule drv.foo.outPath)
+ (etaRule (builtins.unsafeDiscardOutputDependency drv.drvPath))
+]
diff --git a/tests/lang/eval-okay-readDir.exp b/tests/lang/eval-okay-readDir.exp
index bf8d2c14e..6413f6d4f 100644
--- a/tests/lang/eval-okay-readDir.exp
+++ b/tests/lang/eval-okay-readDir.exp
@@ -1 +1 @@
-{ bar = "regular"; foo = "directory"; }
+{ bar = "regular"; foo = "directory"; ldir = "symlink"; linked = "symlink"; }
diff --git a/tests/lang/eval-okay-readFileType.exp b/tests/lang/eval-okay-readFileType.exp
new file mode 100644
index 000000000..6413f6d4f
--- /dev/null
+++ b/tests/lang/eval-okay-readFileType.exp
@@ -0,0 +1 @@
+{ bar = "regular"; foo = "directory"; ldir = "symlink"; linked = "symlink"; }
diff --git a/tests/lang/eval-okay-readFileType.nix b/tests/lang/eval-okay-readFileType.nix
new file mode 100644
index 000000000..174fb6c3a
--- /dev/null
+++ b/tests/lang/eval-okay-readFileType.nix
@@ -0,0 +1,6 @@
+{
+ bar = builtins.readFileType ./readDir/bar;
+ foo = builtins.readFileType ./readDir/foo;
+ linked = builtins.readFileType ./readDir/linked;
+ ldir = builtins.readFileType ./readDir/ldir;
+}
diff --git a/tests/lang/readDir/ldir b/tests/lang/readDir/ldir
new file mode 120000
index 000000000..191028156
--- /dev/null
+++ b/tests/lang/readDir/ldir
@@ -0,0 +1 @@
+foo \ No newline at end of file
diff --git a/tests/lang/readDir/linked b/tests/lang/readDir/linked
new file mode 120000
index 000000000..c503f86a0
--- /dev/null
+++ b/tests/lang/readDir/linked
@@ -0,0 +1 @@
+foo/git-hates-directories \ No newline at end of file