Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/libexpr-tests/primops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,91 @@ namespace nix {
ASSERT_THAT(v, IsIntEq(1));
}

TEST_F(PrimOpTest, catchEvalErrorFailureThrow) {
auto v = eval("builtins.catchEvalError (throw \"\")");
ASSERT_THAT(v, IsAttrsOfSize(1));
auto s = createSymbol("success");
auto p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsFalse());
}

TEST_F(PrimOpTest, catchEvalErrorFailureAbort) {
auto v = eval("builtins.catchEvalError (abort \"\")");
ASSERT_THAT(v, IsAttrsOfSize(1));
auto s = createSymbol("success");
auto p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsFalse());
}

TEST_F(PrimOpTest, catchEvalErrorFailureMissingAttr) {
auto v = eval("builtins.catchEvalError {}.a");
ASSERT_THAT(v, IsAttrsOfSize(1));
auto s = createSymbol("success");
auto p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsFalse());
}

TEST_F(PrimOpTest, catchEvalErrorFailureImportFileNotFound) {
auto v = eval("builtins.catchEvalError (import /does-not-exist)");
ASSERT_THAT(v, IsAttrsOfSize(1));
auto s = createSymbol("success");
auto p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsFalse());
}

TEST_F(PrimOpTest, catchEvalErrorFailureReadFileFileNotFound) {
auto v = eval("builtins.catchEvalError (builtins.readFile /does-not-exist)");
ASSERT_THAT(v, IsAttrsOfSize(1));
auto s = createSymbol("success");
auto p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsFalse());
}

TEST_F(PrimOpTest, catchEvalErrorFailureFromJSON) {
auto v = eval("builtins.catchEvalError (builtins.fromJSON \"{\")");
ASSERT_THAT(v, IsAttrsOfSize(1));
auto s = createSymbol("success");
auto p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsFalse());
}

TEST_F(PrimOpTest, catchEvalErrorFailureInfRec) {
auto v = eval("builtins.catchEvalError (let a = b; b = a; in a)");
ASSERT_THAT(v, IsAttrsOfSize(1));
auto s = createSymbol("success");
auto p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsFalse());
}

TEST_F(PrimOpTest, catchEvalErrorFailureOutOfBounds) {
auto v = eval("builtins.catchEvalError (builtins.head [])");
ASSERT_THAT(v, IsAttrsOfSize(1));
auto s = createSymbol("success");
auto p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsFalse());
}

TEST_F(PrimOpTest, catchEvalErrorSuccess) {
auto v = eval("builtins.catchEvalError 123");
ASSERT_THAT(v, IsAttrs());
auto s = createSymbol("success");
auto p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsTrue());
s = createSymbol("value");
p = v.attrs()->get(s);
ASSERT_NE(p, nullptr);
ASSERT_THAT(*p->value, IsIntEq(123));
}

TEST_F(PrimOpTest, tryEvalFailure) {
auto v = eval("builtins.tryEval (throw \"\")");
ASSERT_THAT(v, IsAttrsOfSize(2));
Expand Down
52 changes: 52 additions & 0 deletions src/libexpr/primops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,58 @@ static RegisterPrimOp primop_tryEval({
.fun = prim_tryEval,
});

/* Similar to `builtins.tryEval` but catches more, `value` attribute provided only on success. */
static void prim_catchEvalError(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
auto attrs = state.buildBindings(2);

/* increment state.trylevel, and decrement it when this function returns. */
MaintainCount trylevel(state.trylevel);

ReplExitStatus (* savedDebugRepl)(ref<EvalState> es, const ValMap & extraEnv) = nullptr;
if (state.debugRepl && state.settings.ignoreExceptionsDuringTry)
{
/* to prevent starting the repl from exceptions withing a tryEval, null it. */
savedDebugRepl = state.debugRepl;
state.debugRepl = nullptr;
}

try {
state.forceValue(*args[0], pos);
attrs.insert(state.sValue, args[0]);
attrs.insert(state.symbols.create("success"), &state.vTrue);
} catch (EvalError & e) {
attrs.insert(state.symbols.create("success"), &state.vFalse);
} catch (FileNotFound & e) {
attrs.insert(state.symbols.create("success"), &state.vFalse);
}

// restore the debugRepl pointer if we saved it earlier.
if (savedDebugRepl)
state.debugRepl = savedDebugRepl;

v.mkAttrs(attrs);
}

static RegisterPrimOp primop_catchEvalError({
.name = "__catchEvalError",
.args = {"e"},
.doc = R"(
Similar to `builtins.tryEval` except that it catches
- `throw`
- `assert`
- `abort`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing aborts to be caught is probably going to lead to a reallyAbort primop that isn't caught by catchEvalError...

- missing attribute
- out of bounds indexing
- file not found such as `import` and `builtins.readFile`
- deserialization such as `builtins.fromJSON`
- detectable infinite recursion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never be caught since non-termination is undecidable. Our current blackhole detection is "best effort" since it was easy to implement, but for instance it doesn't currently work with the multi-threaded evaluator.


And that `value` attribute exists only on success.
)",
.fun = prim_catchEvalError,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be gated behind an experimental feature or enabled by a CLI flag, since allowing a general catch-all sounds like it could lead to some really bad antipatterns (especially if Nixpkgs or the NixOS module system starts using it).

});

/* Return an environment variable. Use with care. */
static void prim_getEnv(EvalState & state, const PosIdx pos, Value * * args, Value & v)
{
Expand Down
Loading