diff --git a/src/libexpr-c/nix_api_expr.h b/src/libexpr-c/nix_api_expr.h index 7504b5d7a..fd9746ab7 100644 --- a/src/libexpr-c/nix_api_expr.h +++ b/src/libexpr-c/nix_api_expr.h @@ -93,6 +93,8 @@ nix_err nix_expr_eval_from_string( * @param[in] arg The argument to pass to the function. * @param[out] value The result of the function call. * @return NIX_OK if the function call was successful, an error code otherwise. + * @see nix_init_apply() for a similar function that does not performs the call immediately, but stores it as a thunk. + * Note the different argument order. */ nix_err nix_value_call(nix_c_context * context, EvalState * state, Value * fn, Value * arg, Value * value); diff --git a/src/libexpr-c/nix_api_value.cc b/src/libexpr-c/nix_api_value.cc index 79e62a1d2..2550e975a 100644 --- a/src/libexpr-c/nix_api_value.cc +++ b/src/libexpr-c/nix_api_value.cc @@ -404,6 +404,19 @@ nix_err nix_init_null(nix_c_context * context, Value * value) NIXC_CATCH_ERRS } +nix_err nix_init_apply(nix_c_context * context, Value * value, Value * fn, Value * arg) +{ + if (context) + context->last_err_code = NIX_OK; + try { + auto & v = check_value_not_null(value); + auto & f = check_value_not_null(fn); + auto & a = check_value_not_null(arg); + v.mkApp(&f, &a); + } + NIXC_CATCH_ERRS +} + nix_err nix_init_external(nix_c_context * context, Value * value, ExternalValue * val) { if (context) diff --git a/src/libexpr-c/nix_api_value.h b/src/libexpr-c/nix_api_value.h index e6744e610..d8bd77c33 100644 --- a/src/libexpr-c/nix_api_value.h +++ b/src/libexpr-c/nix_api_value.h @@ -342,8 +342,24 @@ nix_err nix_init_int(nix_c_context * context, Value * value, int64_t i); * @param[out] value Nix value to modify * @return error code, NIX_OK on success. */ - nix_err nix_init_null(nix_c_context * context, Value * value); + +/** @brief Set the value to a thunk that will perform a function application when needed. + * + * Thunks may be put into attribute sets and lists to perform some computation lazily; on demand. + * However, note that in some places, a thunk must not be returned, such as in the return value of a PrimOp. + * In such cases, you may use nix_value_call() instead (but note the different argument order). + * + * @param[out] context Optional, stores error information + * @param[out] value Nix value to modify + * @param[in] fn function to call + * @param[in] arg argument to pass + * @return error code, NIX_OK on successful initialization. + * @see nix_value_call() for a similar function that performs the call immediately and only stores the return value. + * Note the different argument order. + */ +nix_err nix_init_apply(nix_c_context * context, Value * value, Value * fn, Value * arg); + /** @brief Set an external value * @param[out] context Optional, stores error information * @param[out] value Nix value to modify @@ -421,7 +437,7 @@ BindingsBuilder * nix_make_bindings_builder(nix_c_context * context, EvalState * /** @brief Insert bindings into a builder * @param[out] context Optional, stores error information * @param[in] builder BindingsBuilder to insert into - * @param[in] name attribute name, copied into the symbol store + * @param[in] name attribute name, only used for the duration of the call. * @param[in] value value to give the binding * @return error code, NIX_OK on success. */ diff --git a/tests/unit/libexpr/nix_api_value.cc b/tests/unit/libexpr/nix_api_value.cc index 7fbb2bbdc..ac0cdb9c4 100644 --- a/tests/unit/libexpr/nix_api_value.cc +++ b/tests/unit/libexpr/nix_api_value.cc @@ -8,6 +8,7 @@ #include "tests/nix_api_expr.hh" #include "tests/string_callback.hh" +#include "gmock/gmock.h" #include #include @@ -187,4 +188,127 @@ TEST_F(nix_api_expr_test, nix_build_and_init_attr) free(out_name); } +TEST_F(nix_api_expr_test, nix_value_init) +{ + // Setup + + // two = 2; + // f = a: a * a; + + Value * two = nix_alloc_value(ctx, state); + nix_init_int(ctx, two, 2); + + Value * f = nix_alloc_value(ctx, state); + nix_expr_eval_from_string( + ctx, state, R"( + a: a * a + )", + "", f); + + // Test + + // r = f two; + + Value * r = nix_alloc_value(ctx, state); + nix_init_apply(ctx, r, f, two); + assert_ctx_ok(); + + ValueType t = nix_get_type(ctx, r); + assert_ctx_ok(); + + ASSERT_EQ(t, NIX_TYPE_THUNK); + + nix_value_force(ctx, state, r); + + t = nix_get_type(ctx, r); + assert_ctx_ok(); + + ASSERT_EQ(t, NIX_TYPE_INT); + + int n = nix_get_int(ctx, r); + assert_ctx_ok(); + + ASSERT_EQ(n, 4); + + // Clean up + nix_gc_decref(ctx, two); + nix_gc_decref(ctx, f); + nix_gc_decref(ctx, r); +} + +TEST_F(nix_api_expr_test, nix_value_init_apply_error) +{ + Value * some_string = nix_alloc_value(ctx, state); + nix_init_string(ctx, some_string, "some string"); + assert_ctx_ok(); + + Value * v = nix_alloc_value(ctx, state); + nix_init_apply(ctx, v, some_string, some_string); + assert_ctx_ok(); + + // All ok. Call has not been evaluated yet. + + // Evaluate it + nix_value_force(ctx, state, v); + ASSERT_EQ(ctx->last_err_code, NIX_ERR_NIX_ERROR); + ASSERT_THAT(ctx->last_err.value(), testing::HasSubstr("attempt to call something which is not a function but")); + + // Clean up + nix_gc_decref(ctx, some_string); + nix_gc_decref(ctx, v); +} + +TEST_F(nix_api_expr_test, nix_value_init_apply_lazy_arg) +{ + // f is a lazy function: it does not evaluate its argument before returning its return value + // g is a helper to produce e + // e is a thunk that throws an exception + // + // r = f e + // r should not throw an exception, because e is not evaluated + + Value * f = nix_alloc_value(ctx, state); + nix_expr_eval_from_string( + ctx, state, R"( + a: { foo = a; } + )", + "", f); + assert_ctx_ok(); + + Value * e = nix_alloc_value(ctx, state); + { + Value * g = nix_alloc_value(ctx, state); + nix_expr_eval_from_string( + ctx, state, R"( + _ignore: throw "error message for test case nix_value_init_apply_lazy_arg" + )", + "", g); + assert_ctx_ok(); + + nix_init_apply(ctx, e, g, g); + assert_ctx_ok(); + nix_gc_decref(ctx, g); + } + + Value * r = nix_alloc_value(ctx, state); + nix_init_apply(ctx, r, f, e); + assert_ctx_ok(); + + nix_value_force(ctx, state, r); + assert_ctx_ok(); + + auto n = nix_get_attrs_size(ctx, r); + assert_ctx_ok(); + ASSERT_EQ(1, n); + + // nix_get_attr_byname isn't lazy (it could have been) so it will throw the exception + Value * foo = nix_get_attr_byname(ctx, r, state, "foo"); + ASSERT_EQ(nullptr, foo); + ASSERT_THAT(ctx->last_err.value(), testing::HasSubstr("error message for test case nix_value_init_apply_lazy_arg")); + + // Clean up + nix_gc_decref(ctx, f); + nix_gc_decref(ctx, e); +} + }