1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-08 19:46:02 +01:00

C API: Add lazy attribute value and list item accessors

This commit is contained in:
Robert Hensing 2025-09-19 23:06:51 +02:00
parent 7c553a30a9
commit 3d777eb37f
3 changed files with 382 additions and 2 deletions

View file

@ -339,6 +339,26 @@ nix_value * nix_get_list_byidx(nix_c_context * context, const nix_value * value,
NIXC_CATCH_ERRS_NULL
}
nix_value *
nix_get_list_byidx_lazy(nix_c_context * context, const nix_value * value, EvalState * state, unsigned int ix)
{
if (context)
context->last_err_code = NIX_OK;
try {
auto & v = check_value_in(value);
assert(v.type() == nix::nList);
if (ix >= v.listSize()) {
nix_set_err_msg(context, NIX_ERR_KEY, "list index out of bounds");
return nullptr;
}
auto * p = v.listView()[ix];
nix_gc_incref(nullptr, p);
// Note: intentionally NOT calling forceValue() to keep the element lazy
return as_nix_value_ptr(p);
}
NIXC_CATCH_ERRS_NULL
}
nix_value * nix_get_attr_byname(nix_c_context * context, const nix_value * value, EvalState * state, const char * name)
{
if (context)
@ -359,6 +379,27 @@ nix_value * nix_get_attr_byname(nix_c_context * context, const nix_value * value
NIXC_CATCH_ERRS_NULL
}
nix_value *
nix_get_attr_byname_lazy(nix_c_context * context, const nix_value * value, EvalState * state, const char * name)
{
if (context)
context->last_err_code = NIX_OK;
try {
auto & v = check_value_in(value);
assert(v.type() == nix::nAttrs);
nix::Symbol s = state->state.symbols.create(name);
auto attr = v.attrs()->get(s);
if (attr) {
nix_gc_incref(nullptr, attr->value);
// Note: intentionally NOT calling forceValue() to keep the attribute lazy
return as_nix_value_ptr(attr->value);
}
nix_set_err_msg(context, NIX_ERR_KEY, "missing attribute");
return nullptr;
}
NIXC_CATCH_ERRS_NULL
}
bool nix_has_attr_byname(nix_c_context * context, const nix_value * value, EvalState * state, const char * name)
{
if (context)
@ -406,6 +447,27 @@ nix_get_attr_byidx(nix_c_context * context, nix_value * value, EvalState * state
NIXC_CATCH_ERRS_NULL
}
nix_value * nix_get_attr_byidx_lazy(
nix_c_context * context, nix_value * value, EvalState * state, unsigned int i, const char ** name)
{
if (context)
context->last_err_code = NIX_OK;
try {
auto & v = check_value_in(value);
collapse_attrset_layer_chain_if_needed(v, state);
if (i >= v.attrs()->size()) {
nix_set_err_msg(context, NIX_ERR_KEY, "attribute index out of bounds (Nix C API contract violation)");
return nullptr;
}
const nix::Attr & a = (*v.attrs())[i];
*name = state->state.symbols[a.name].c_str();
nix_gc_incref(nullptr, a.value);
// Note: intentionally NOT calling forceValue() to keep the attribute lazy
return as_nix_value_ptr(a.value);
}
NIXC_CATCH_ERRS_NULL
}
const char * nix_get_attr_name_byidx(nix_c_context * context, nix_value * value, EvalState * state, unsigned int i)
{
if (context)

View file

@ -265,10 +265,25 @@ ExternalValue * nix_get_external(nix_c_context * context, nix_value * value);
*/
nix_value * nix_get_list_byidx(nix_c_context * context, const nix_value * value, EvalState * state, unsigned int ix);
/** @brief Get an attr by name
/** @brief Get the ix'th element of a list without forcing evaluation of the element
*
* Returns the list element without forcing its evaluation, allowing access to lazy values.
* The list value itself must already be evaluated.
*
* Owned by the GC. Use nix_gc_decref when you're done with the pointer
* @param[out] context Optional, stores error information
* @param[in] value Nix value to inspect (must be an evaluated list)
* @param[in] state nix evaluator state
* @param[in] ix list element to get
* @return value, NULL in case of errors
*/
nix_value *
nix_get_list_byidx_lazy(nix_c_context * context, const nix_value * value, EvalState * state, unsigned int ix);
/** @brief Get an attr by name
*
* Use nix_gc_decref when you're done with the pointer
* @param[out] context Optional, stores error information
* @param[in] value Nix value to inspect
* @param[in] state nix evaluator state
* @param[in] name attribute name
@ -276,6 +291,21 @@ nix_value * nix_get_list_byidx(nix_c_context * context, const nix_value * value,
*/
nix_value * nix_get_attr_byname(nix_c_context * context, const nix_value * value, EvalState * state, const char * name);
/** @brief Get an attribute value by attribute name, without forcing evaluation of the attribute's value
*
* Returns the attribute value without forcing its evaluation, allowing access to lazy values.
* The attribute set value itself must already be evaluated.
*
* Use nix_gc_decref when you're done with the pointer
* @param[out] context Optional, stores error information
* @param[in] value Nix value to inspect (must be an evaluated attribute set)
* @param[in] state nix evaluator state
* @param[in] name attribute name
* @return value, NULL in case of errors
*/
nix_value *
nix_get_attr_byname_lazy(nix_c_context * context, const nix_value * value, EvalState * state, const char * name);
/** @brief Check if an attribute name exists on a value
* @param[out] context Optional, stores error information
* @param[in] value Nix value to inspect
@ -289,7 +319,7 @@ bool nix_has_attr_byname(nix_c_context * context, const nix_value * value, EvalS
*
* Also gives you the name.
*
* Owned by the GC. Use nix_gc_decref when you're done with the pointer
* Use nix_gc_decref when you're done with the pointer
* @param[out] context Optional, stores error information
* @param[in] value Nix value to inspect
* @param[in] state nix evaluator state
@ -300,6 +330,22 @@ bool nix_has_attr_byname(nix_c_context * context, const nix_value * value, EvalS
nix_value *
nix_get_attr_byidx(nix_c_context * context, nix_value * value, EvalState * state, unsigned int i, const char ** name);
/** @brief Get an attribute by index in the sorted bindings, without forcing evaluation of the attribute's value
*
* Also gives you the name. Returns the attribute value without forcing its evaluation, allowing access to lazy values.
* The attribute set value itself must already be evaluated.
*
* Use nix_gc_decref when you're done with the pointer
* @param[out] context Optional, stores error information
* @param[in] value Nix value to inspect (must be an evaluated attribute set)
* @param[in] state nix evaluator state
* @param[in] i attribute index
* @param[out] name will store a pointer to the attribute name
* @return value, NULL in case of errors
*/
nix_value * nix_get_attr_byidx_lazy(
nix_c_context * context, nix_value * value, EvalState * state, unsigned int i, const char ** name);
/** @brief Get an attribute name by index in the sorted bindings
*
* Returns the attribute name without forcing evaluation of the attribute's value.

View file

@ -185,6 +185,91 @@ TEST_F(nix_api_expr_test, nix_get_list_byidx_large_indices)
nix_gc_decref(ctx, intValue);
}
TEST_F(nix_api_expr_test, nix_get_list_byidx_lazy)
{
// Create a list with a throwing lazy element, an already-evaluated int, and a lazy function call
// 1. Throwing lazy element - create a function application thunk that will throw when forced
nix_value * throwingFn = nix_alloc_value(ctx, state);
nix_value * throwingValue = nix_alloc_value(ctx, state);
nix_expr_eval_from_string(
ctx,
state,
R"(
_: throw "This should not be evaluated by the lazy accessor"
)",
"<test>",
throwingFn);
assert_ctx_ok();
nix_init_apply(ctx, throwingValue, throwingFn, throwingFn);
assert_ctx_ok();
// 2. Already evaluated int (not lazy)
nix_value * intValue = nix_alloc_value(ctx, state);
nix_init_int(ctx, intValue, 42);
assert_ctx_ok();
// 3. Lazy function application that would compute increment 5 = 6
nix_value * lazyApply = nix_alloc_value(ctx, state);
nix_value * incrementFn = nix_alloc_value(ctx, state);
nix_value * argFive = nix_alloc_value(ctx, state);
nix_expr_eval_from_string(ctx, state, "x: x + 1", "<test>", incrementFn);
assert_ctx_ok();
nix_init_int(ctx, argFive, 5);
// Create a lazy application: (x: x + 1) 5
nix_init_apply(ctx, lazyApply, incrementFn, argFive);
assert_ctx_ok();
ListBuilder * builder = nix_make_list_builder(ctx, state, 3);
nix_list_builder_insert(ctx, builder, 0, throwingValue);
nix_list_builder_insert(ctx, builder, 1, intValue);
nix_list_builder_insert(ctx, builder, 2, lazyApply);
nix_make_list(ctx, builder, value);
nix_list_builder_free(builder);
// Test 1: Lazy accessor should return the throwing element without forcing evaluation
nix_value * lazyThrowingElement = nix_get_list_byidx_lazy(ctx, value, state, 0);
assert_ctx_ok();
ASSERT_NE(nullptr, lazyThrowingElement);
// Verify the element is still lazy by checking that forcing it throws
nix_value_force(ctx, state, lazyThrowingElement);
assert_ctx_err();
ASSERT_THAT(
nix_err_msg(nullptr, ctx, nullptr), testing::HasSubstr("This should not be evaluated by the lazy accessor"));
// Test 2: Lazy accessor should return the already-evaluated int
nix_value * intElement = nix_get_list_byidx_lazy(ctx, value, state, 1);
assert_ctx_ok();
ASSERT_NE(nullptr, intElement);
ASSERT_EQ(42, nix_get_int(ctx, intElement));
// Test 3: Lazy accessor should return the lazy function application without forcing
nix_value * lazyFunctionElement = nix_get_list_byidx_lazy(ctx, value, state, 2);
assert_ctx_ok();
ASSERT_NE(nullptr, lazyFunctionElement);
// Force the lazy function application - should compute 5 + 1 = 6
nix_value_force(ctx, state, lazyFunctionElement);
assert_ctx_ok();
ASSERT_EQ(6, nix_get_int(ctx, lazyFunctionElement));
// Clean up
nix_gc_decref(ctx, throwingFn);
nix_gc_decref(ctx, throwingValue);
nix_gc_decref(ctx, intValue);
nix_gc_decref(ctx, lazyApply);
nix_gc_decref(ctx, incrementFn);
nix_gc_decref(ctx, argFive);
nix_gc_decref(ctx, lazyThrowingElement);
nix_gc_decref(ctx, intElement);
nix_gc_decref(ctx, lazyFunctionElement);
}
TEST_F(nix_api_expr_test, nix_build_and_init_attr_invalid)
{
ASSERT_EQ(nullptr, nix_get_attr_byname(ctx, nullptr, state, 0));
@ -299,6 +384,193 @@ TEST_F(nix_api_expr_test, nix_get_attr_byidx_large_indices)
free(out_name);
}
TEST_F(nix_api_expr_test, nix_get_attr_byname_lazy)
{
// Create an attribute set with a throwing lazy attribute, an already-evaluated int, and a lazy function call
// 1. Throwing lazy element - create a function application thunk that will throw when forced
nix_value * throwingFn = nix_alloc_value(ctx, state);
nix_value * throwingValue = nix_alloc_value(ctx, state);
nix_expr_eval_from_string(
ctx,
state,
R"(
_: throw "This should not be evaluated by the lazy accessor"
)",
"<test>",
throwingFn);
assert_ctx_ok();
nix_init_apply(ctx, throwingValue, throwingFn, throwingFn);
assert_ctx_ok();
// 2. Already evaluated int (not lazy)
nix_value * intValue = nix_alloc_value(ctx, state);
nix_init_int(ctx, intValue, 42);
assert_ctx_ok();
// 3. Lazy function application that would compute increment 7 = 8
nix_value * lazyApply = nix_alloc_value(ctx, state);
nix_value * incrementFn = nix_alloc_value(ctx, state);
nix_value * argSeven = nix_alloc_value(ctx, state);
nix_expr_eval_from_string(ctx, state, "x: x + 1", "<test>", incrementFn);
assert_ctx_ok();
nix_init_int(ctx, argSeven, 7);
// Create a lazy application: (x: x + 1) 7
nix_init_apply(ctx, lazyApply, incrementFn, argSeven);
assert_ctx_ok();
BindingsBuilder * builder = nix_make_bindings_builder(ctx, state, 3);
nix_bindings_builder_insert(ctx, builder, "throwing", throwingValue);
nix_bindings_builder_insert(ctx, builder, "normal", intValue);
nix_bindings_builder_insert(ctx, builder, "lazy", lazyApply);
nix_make_attrs(ctx, value, builder);
nix_bindings_builder_free(builder);
// Test 1: Lazy accessor should return the throwing attribute without forcing evaluation
nix_value * lazyThrowingAttr = nix_get_attr_byname_lazy(ctx, value, state, "throwing");
assert_ctx_ok();
ASSERT_NE(nullptr, lazyThrowingAttr);
// Verify the attribute is still lazy by checking that forcing it throws
nix_value_force(ctx, state, lazyThrowingAttr);
assert_ctx_err();
ASSERT_THAT(
nix_err_msg(nullptr, ctx, nullptr), testing::HasSubstr("This should not be evaluated by the lazy accessor"));
// Test 2: Lazy accessor should return the already-evaluated int
nix_value * intAttr = nix_get_attr_byname_lazy(ctx, value, state, "normal");
assert_ctx_ok();
ASSERT_NE(nullptr, intAttr);
ASSERT_EQ(42, nix_get_int(ctx, intAttr));
// Test 3: Lazy accessor should return the lazy function application without forcing
nix_value * lazyFunctionAttr = nix_get_attr_byname_lazy(ctx, value, state, "lazy");
assert_ctx_ok();
ASSERT_NE(nullptr, lazyFunctionAttr);
// Force the lazy function application - should compute 7 + 1 = 8
nix_value_force(ctx, state, lazyFunctionAttr);
assert_ctx_ok();
ASSERT_EQ(8, nix_get_int(ctx, lazyFunctionAttr));
// Test 4: Missing attribute should return NULL with NIX_ERR_KEY
nix_value * missingAttr = nix_get_attr_byname_lazy(ctx, value, state, "nonexistent");
ASSERT_EQ(nullptr, missingAttr);
ASSERT_EQ(NIX_ERR_KEY, nix_err_code(ctx));
// Clean up
nix_gc_decref(ctx, throwingFn);
nix_gc_decref(ctx, throwingValue);
nix_gc_decref(ctx, intValue);
nix_gc_decref(ctx, lazyApply);
nix_gc_decref(ctx, incrementFn);
nix_gc_decref(ctx, argSeven);
nix_gc_decref(ctx, lazyThrowingAttr);
nix_gc_decref(ctx, intAttr);
nix_gc_decref(ctx, lazyFunctionAttr);
}
TEST_F(nix_api_expr_test, nix_get_attr_byidx_lazy)
{
// Create an attribute set with a throwing lazy attribute, an already-evaluated int, and a lazy function call
// 1. Throwing lazy element - create a function application thunk that will throw when forced
nix_value * throwingFn = nix_alloc_value(ctx, state);
nix_value * throwingValue = nix_alloc_value(ctx, state);
nix_expr_eval_from_string(
ctx,
state,
R"(
_: throw "This should not be evaluated by the lazy accessor"
)",
"<test>",
throwingFn);
assert_ctx_ok();
nix_init_apply(ctx, throwingValue, throwingFn, throwingFn);
assert_ctx_ok();
// 2. Already evaluated int (not lazy)
nix_value * intValue = nix_alloc_value(ctx, state);
nix_init_int(ctx, intValue, 99);
assert_ctx_ok();
// 3. Lazy function application that would compute increment 10 = 11
nix_value * lazyApply = nix_alloc_value(ctx, state);
nix_value * incrementFn = nix_alloc_value(ctx, state);
nix_value * argTen = nix_alloc_value(ctx, state);
nix_expr_eval_from_string(ctx, state, "x: x + 1", "<test>", incrementFn);
assert_ctx_ok();
nix_init_int(ctx, argTen, 10);
// Create a lazy application: (x: x + 1) 10
nix_init_apply(ctx, lazyApply, incrementFn, argTen);
assert_ctx_ok();
BindingsBuilder * builder = nix_make_bindings_builder(ctx, state, 3);
nix_bindings_builder_insert(ctx, builder, "a_throwing", throwingValue);
nix_bindings_builder_insert(ctx, builder, "b_normal", intValue);
nix_bindings_builder_insert(ctx, builder, "c_lazy", lazyApply);
nix_make_attrs(ctx, value, builder);
nix_bindings_builder_free(builder);
// Proper usage: first get the size and gather all attributes into a map
unsigned int attrCount = nix_get_attrs_size(ctx, value);
assert_ctx_ok();
ASSERT_EQ(3u, attrCount);
// Gather all attributes into a map (proper contract usage)
std::map<std::string, nix_value *> attrMap;
const char * name;
for (unsigned int i = 0; i < attrCount; i++) {
nix_value * attr = nix_get_attr_byidx_lazy(ctx, value, state, i, &name);
assert_ctx_ok();
ASSERT_NE(nullptr, attr);
attrMap[std::string(name)] = attr;
}
// Now test the gathered attributes
ASSERT_EQ(3u, attrMap.size());
ASSERT_TRUE(attrMap.count("a_throwing"));
ASSERT_TRUE(attrMap.count("b_normal"));
ASSERT_TRUE(attrMap.count("c_lazy"));
// Test 1: Throwing attribute should be lazy
nix_value * throwingAttr = attrMap["a_throwing"];
nix_value_force(ctx, state, throwingAttr);
assert_ctx_err();
ASSERT_THAT(
nix_err_msg(nullptr, ctx, nullptr), testing::HasSubstr("This should not be evaluated by the lazy accessor"));
// Test 2: Normal attribute should be already evaluated
nix_value * normalAttr = attrMap["b_normal"];
ASSERT_EQ(99, nix_get_int(ctx, normalAttr));
// Test 3: Lazy function should compute when forced
nix_value * lazyAttr = attrMap["c_lazy"];
nix_value_force(ctx, state, lazyAttr);
assert_ctx_ok();
ASSERT_EQ(11, nix_get_int(ctx, lazyAttr));
// Clean up
nix_gc_decref(ctx, throwingFn);
nix_gc_decref(ctx, throwingValue);
nix_gc_decref(ctx, intValue);
nix_gc_decref(ctx, lazyApply);
nix_gc_decref(ctx, incrementFn);
nix_gc_decref(ctx, argTen);
for (auto & pair : attrMap) {
nix_gc_decref(ctx, pair.second);
}
}
TEST_F(nix_api_expr_test, nix_value_init)
{
// Setup