From 9061f04da5b08a5ddd30637956e846b926a47c13 Mon Sep 17 00:00:00 2001 From: "Shahar \"Dawn\" Or" Date: Fri, 21 Mar 2025 13:21:45 +0000 Subject: [PATCH] builtins.catchEvalError Co-authored-by: Alexander Foremny --- src/libexpr-tests/primops.cc | 85 ++++++++++++++++++++++++++++++++++++ src/libexpr/primops.cc | 52 ++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/src/libexpr-tests/primops.cc b/src/libexpr-tests/primops.cc index 2bf726477..05c48d69a 100644 --- a/src/libexpr-tests/primops.cc +++ b/src/libexpr-tests/primops.cc @@ -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)); diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 54682ea31..b1074c2f5 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -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 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` + - missing attribute + - out of bounds indexing + - file not found such as `import` and `builtins.readFile` + - deserialization such as `builtins.fromJSON` + - detectable infinite recursion + + And that `value` attribute exists only on success. + )", + .fun = prim_catchEvalError, +}); + /* Return an environment variable. Use with care. */ static void prim_getEnv(EvalState & state, const PosIdx pos, Value * * args, Value & v) {