Skip to content

Commit bdd3eb4

Browse files
authored
Merge pull request #14030 from roberth/c-api-item-access
C API: Various improvements to attribute/item access
2 parents ae896be + a08ae1d commit bdd3eb4

File tree

6 files changed

+564
-9
lines changed

6 files changed

+564
-9
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
synopsis: "C API: Add lazy attribute and list item accessors"
3+
prs: [14030]
4+
---
5+
6+
The C API now includes lazy accessor functions for retrieving values from lists and attribute sets without forcing evaluation:
7+
8+
- `nix_get_list_byidx_lazy()` - Get a list element without forcing its evaluation
9+
- `nix_get_attr_byname_lazy()` - Get an attribute value by name without forcing evaluation
10+
- `nix_get_attr_byidx_lazy()` - Get an attribute by index without forcing evaluation
11+
12+
These functions are useful when forwarding unevaluated sub-values to other lists, attribute sets, or function calls. They allow more efficient handling of Nix values by deferring evaluation until actually needed.
13+
14+
Additionally, bounds checking has been improved for all `_byidx` functions to properly validate indices before access, preventing potential out-of-bounds errors.
15+
16+
The documentation for `NIX_ERR_KEY` error handling has also been clarified to specify when this error code is returned.

src/libexpr-c/nix_api_value.cc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,10 @@ nix_value * nix_get_list_byidx(nix_c_context * context, const nix_value * value,
326326
try {
327327
auto & v = check_value_in(value);
328328
assert(v.type() == nix::nList);
329+
if (ix >= v.listSize()) {
330+
nix_set_err_msg(context, NIX_ERR_KEY, "list index out of bounds");
331+
return nullptr;
332+
}
329333
auto * p = v.listView()[ix];
330334
nix_gc_incref(nullptr, p);
331335
if (p != nullptr)
@@ -335,6 +339,26 @@ nix_value * nix_get_list_byidx(nix_c_context * context, const nix_value * value,
335339
NIXC_CATCH_ERRS_NULL
336340
}
337341

342+
nix_value *
343+
nix_get_list_byidx_lazy(nix_c_context * context, const nix_value * value, EvalState * state, unsigned int ix)
344+
{
345+
if (context)
346+
context->last_err_code = NIX_OK;
347+
try {
348+
auto & v = check_value_in(value);
349+
assert(v.type() == nix::nList);
350+
if (ix >= v.listSize()) {
351+
nix_set_err_msg(context, NIX_ERR_KEY, "list index out of bounds");
352+
return nullptr;
353+
}
354+
auto * p = v.listView()[ix];
355+
nix_gc_incref(nullptr, p);
356+
// Note: intentionally NOT calling forceValue() to keep the element lazy
357+
return as_nix_value_ptr(p);
358+
}
359+
NIXC_CATCH_ERRS_NULL
360+
}
361+
338362
nix_value * nix_get_attr_byname(nix_c_context * context, const nix_value * value, EvalState * state, const char * name)
339363
{
340364
if (context)
@@ -355,6 +379,27 @@ nix_value * nix_get_attr_byname(nix_c_context * context, const nix_value * value
355379
NIXC_CATCH_ERRS_NULL
356380
}
357381

382+
nix_value *
383+
nix_get_attr_byname_lazy(nix_c_context * context, const nix_value * value, EvalState * state, const char * name)
384+
{
385+
if (context)
386+
context->last_err_code = NIX_OK;
387+
try {
388+
auto & v = check_value_in(value);
389+
assert(v.type() == nix::nAttrs);
390+
nix::Symbol s = state->state.symbols.create(name);
391+
auto attr = v.attrs()->get(s);
392+
if (attr) {
393+
nix_gc_incref(nullptr, attr->value);
394+
// Note: intentionally NOT calling forceValue() to keep the attribute lazy
395+
return as_nix_value_ptr(attr->value);
396+
}
397+
nix_set_err_msg(context, NIX_ERR_KEY, "missing attribute");
398+
return nullptr;
399+
}
400+
NIXC_CATCH_ERRS_NULL
401+
}
402+
358403
bool nix_has_attr_byname(nix_c_context * context, const nix_value * value, EvalState * state, const char * name)
359404
{
360405
if (context)
@@ -389,6 +434,10 @@ nix_get_attr_byidx(nix_c_context * context, nix_value * value, EvalState * state
389434
try {
390435
auto & v = check_value_in(value);
391436
collapse_attrset_layer_chain_if_needed(v, state);
437+
if (i >= v.attrs()->size()) {
438+
nix_set_err_msg(context, NIX_ERR_KEY, "attribute index out of bounds");
439+
return nullptr;
440+
}
392441
const nix::Attr & a = (*v.attrs())[i];
393442
*name = state->state.symbols[a.name].c_str();
394443
nix_gc_incref(nullptr, a.value);
@@ -398,13 +447,38 @@ nix_get_attr_byidx(nix_c_context * context, nix_value * value, EvalState * state
398447
NIXC_CATCH_ERRS_NULL
399448
}
400449

450+
nix_value * nix_get_attr_byidx_lazy(
451+
nix_c_context * context, nix_value * value, EvalState * state, unsigned int i, const char ** name)
452+
{
453+
if (context)
454+
context->last_err_code = NIX_OK;
455+
try {
456+
auto & v = check_value_in(value);
457+
collapse_attrset_layer_chain_if_needed(v, state);
458+
if (i >= v.attrs()->size()) {
459+
nix_set_err_msg(context, NIX_ERR_KEY, "attribute index out of bounds (Nix C API contract violation)");
460+
return nullptr;
461+
}
462+
const nix::Attr & a = (*v.attrs())[i];
463+
*name = state->state.symbols[a.name].c_str();
464+
nix_gc_incref(nullptr, a.value);
465+
// Note: intentionally NOT calling forceValue() to keep the attribute lazy
466+
return as_nix_value_ptr(a.value);
467+
}
468+
NIXC_CATCH_ERRS_NULL
469+
}
470+
401471
const char * nix_get_attr_name_byidx(nix_c_context * context, nix_value * value, EvalState * state, unsigned int i)
402472
{
403473
if (context)
404474
context->last_err_code = NIX_OK;
405475
try {
406476
auto & v = check_value_in(value);
407477
collapse_attrset_layer_chain_if_needed(v, state);
478+
if (i >= v.attrs()->size()) {
479+
nix_set_err_msg(context, NIX_ERR_KEY, "attribute index out of bounds (Nix C API contract violation)");
480+
return nullptr;
481+
}
408482
const nix::Attr & a = (*v.attrs())[i];
409483
return state->state.symbols[a.name].c_str();
410484
}

src/libexpr-c/nix_api_value.h

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,17 +265,47 @@ ExternalValue * nix_get_external(nix_c_context * context, nix_value * value);
265265
*/
266266
nix_value * nix_get_list_byidx(nix_c_context * context, const nix_value * value, EvalState * state, unsigned int ix);
267267

268-
/** @brief Get an attr by name
268+
/** @brief Get the ix'th element of a list without forcing evaluation of the element
269+
*
270+
* Returns the list element without forcing its evaluation, allowing access to lazy values.
271+
* The list value itself must already be evaluated.
269272
*
270273
* Owned by the GC. Use nix_gc_decref when you're done with the pointer
271274
* @param[out] context Optional, stores error information
275+
* @param[in] value Nix value to inspect (must be an evaluated list)
276+
* @param[in] state nix evaluator state
277+
* @param[in] ix list element to get
278+
* @return value, NULL in case of errors
279+
*/
280+
nix_value *
281+
nix_get_list_byidx_lazy(nix_c_context * context, const nix_value * value, EvalState * state, unsigned int ix);
282+
283+
/** @brief Get an attr by name
284+
*
285+
* Use nix_gc_decref when you're done with the pointer
286+
* @param[out] context Optional, stores error information
272287
* @param[in] value Nix value to inspect
273288
* @param[in] state nix evaluator state
274289
* @param[in] name attribute name
275290
* @return value, NULL in case of errors
276291
*/
277292
nix_value * nix_get_attr_byname(nix_c_context * context, const nix_value * value, EvalState * state, const char * name);
278293

294+
/** @brief Get an attribute value by attribute name, without forcing evaluation of the attribute's value
295+
*
296+
* Returns the attribute value without forcing its evaluation, allowing access to lazy values.
297+
* The attribute set value itself must already be evaluated.
298+
*
299+
* Use nix_gc_decref when you're done with the pointer
300+
* @param[out] context Optional, stores error information
301+
* @param[in] value Nix value to inspect (must be an evaluated attribute set)
302+
* @param[in] state nix evaluator state
303+
* @param[in] name attribute name
304+
* @return value, NULL in case of errors
305+
*/
306+
nix_value *
307+
nix_get_attr_byname_lazy(nix_c_context * context, const nix_value * value, EvalState * state, const char * name);
308+
279309
/** @brief Check if an attribute name exists on a value
280310
* @param[out] context Optional, stores error information
281311
* @param[in] value Nix value to inspect
@@ -285,11 +315,21 @@ nix_value * nix_get_attr_byname(nix_c_context * context, const nix_value * value
285315
*/
286316
bool nix_has_attr_byname(nix_c_context * context, const nix_value * value, EvalState * state, const char * name);
287317

288-
/** @brief Get an attribute by index in the sorted bindings
318+
/** @brief Get an attribute by index
289319
*
290320
* Also gives you the name.
291321
*
292-
* Owned by the GC. Use nix_gc_decref when you're done with the pointer
322+
* Attributes are returned in an unspecified order which is NOT suitable for
323+
* reproducible operations. In Nix's domain, reproducibility is paramount. The caller
324+
* is responsible for sorting the attributes or storing them in an ordered map to
325+
* ensure deterministic behavior in your application.
326+
*
327+
* @note When Nix does sort attributes, which it does for virtually all intermediate
328+
* operations and outputs, it uses byte-wise lexicographic order (equivalent to
329+
* lexicographic order by Unicode scalar value for valid UTF-8). We recommend
330+
* applying this same ordering for consistency.
331+
*
332+
* Use nix_gc_decref when you're done with the pointer
293333
* @param[out] context Optional, stores error information
294334
* @param[in] value Nix value to inspect
295335
* @param[in] state nix evaluator state
@@ -300,9 +340,47 @@ bool nix_has_attr_byname(nix_c_context * context, const nix_value * value, EvalS
300340
nix_value *
301341
nix_get_attr_byidx(nix_c_context * context, nix_value * value, EvalState * state, unsigned int i, const char ** name);
302342

303-
/** @brief Get an attribute name by index in the sorted bindings
343+
/** @brief Get an attribute by index, without forcing evaluation of the attribute's value
344+
*
345+
* Also gives you the name.
346+
*
347+
* Returns the attribute value without forcing its evaluation, allowing access to lazy values.
348+
* The attribute set value itself must already have been evaluated.
349+
*
350+
* Attributes are returned in an unspecified order which is NOT suitable for
351+
* reproducible operations. In Nix's domain, reproducibility is paramount. The caller
352+
* is responsible for sorting the attributes or storing them in an ordered map to
353+
* ensure deterministic behavior in your application.
354+
*
355+
* @note When Nix does sort attributes, which it does for virtually all intermediate
356+
* operations and outputs, it uses byte-wise lexicographic order (equivalent to
357+
* lexicographic order by Unicode scalar value for valid UTF-8). We recommend
358+
* applying this same ordering for consistency.
359+
*
360+
* Use nix_gc_decref when you're done with the pointer
361+
* @param[out] context Optional, stores error information
362+
* @param[in] value Nix value to inspect (must be an evaluated attribute set)
363+
* @param[in] state nix evaluator state
364+
* @param[in] i attribute index
365+
* @param[out] name will store a pointer to the attribute name
366+
* @return value, NULL in case of errors
367+
*/
368+
nix_value * nix_get_attr_byidx_lazy(
369+
nix_c_context * context, nix_value * value, EvalState * state, unsigned int i, const char ** name);
370+
371+
/** @brief Get an attribute name by index
372+
*
373+
* Returns the attribute name without forcing evaluation of the attribute's value.
374+
*
375+
* Attributes are returned in an unspecified order which is NOT suitable for
376+
* reproducible operations. In Nix's domain, reproducibility is paramount. The caller
377+
* is responsible for sorting the attributes or storing them in an ordered map to
378+
* ensure deterministic behavior in your application.
304379
*
305-
* Useful when you want the name but want to avoid evaluation.
380+
* @note When Nix does sort attributes, which it does for virtually all intermediate
381+
* operations and outputs, it uses byte-wise lexicographic order (equivalent to
382+
* lexicographic order by Unicode scalar value for valid UTF-8). We recommend
383+
* applying this same ordering for consistency.
306384
*
307385
* Owned by the nix EvalState
308386
* @param[out] context Optional, stores error information

src/libexpr-tests/nix_api_expr.cc

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,55 @@ TEST_F(nix_api_expr_test, nix_expr_primop_bad_return_thunk)
423423
ASSERT_THAT(nix_err_msg(nullptr, ctx, nullptr), testing::HasSubstr("badReturnThunk"));
424424
}
425425

426+
static void primop_with_nix_err_key(
427+
void * user_data, nix_c_context * context, EvalState * state, nix_value ** args, nix_value * ret)
428+
{
429+
nix_set_err_msg(context, NIX_ERR_KEY, "Test error from primop");
430+
}
431+
432+
TEST_F(nix_api_expr_test, nix_expr_primop_nix_err_key_conversion)
433+
{
434+
// Test that NIX_ERR_KEY from a custom primop gets converted to a generic EvalError
435+
//
436+
// RATIONALE: NIX_ERR_KEY must not be propagated from custom primops because it would
437+
// create semantic confusion. NIX_ERR_KEY indicates missing keys/indices in C API functions
438+
// (like nix_get_attr_byname, nix_get_list_byidx). If custom primops could return NIX_ERR_KEY,
439+
// an evaluation error would be indistinguishable from an actual missing attribute.
440+
//
441+
// For example, if nix_get_attr_byname returned NIX_ERR_KEY when the attribute is present
442+
// but the value evaluation fails, callers expecting NIX_ERR_KEY to mean "missing attribute"
443+
// would incorrectly handle evaluation failures as missing attributes. In places where
444+
// missing attributes are tolerated (like optional attributes), this would cause the
445+
// program to continue after swallowing the error, leading to silent failures.
446+
PrimOp * primop = nix_alloc_primop(
447+
ctx, primop_with_nix_err_key, 1, "testErrorPrimop", nullptr, "a test primop that sets NIX_ERR_KEY", nullptr);
448+
assert_ctx_ok();
449+
nix_value * primopValue = nix_alloc_value(ctx, state);
450+
assert_ctx_ok();
451+
nix_init_primop(ctx, primopValue, primop);
452+
assert_ctx_ok();
453+
454+
nix_value * arg = nix_alloc_value(ctx, state);
455+
assert_ctx_ok();
456+
nix_init_int(ctx, arg, 42);
457+
assert_ctx_ok();
458+
459+
nix_value * result = nix_alloc_value(ctx, state);
460+
assert_ctx_ok();
461+
nix_value_call(ctx, state, primopValue, arg, result);
462+
463+
// Verify that NIX_ERR_KEY gets converted to NIX_ERR_NIX_ERROR (generic evaluation error)
464+
ASSERT_EQ(nix_err_code(ctx), NIX_ERR_NIX_ERROR);
465+
ASSERT_THAT(nix_err_msg(nullptr, ctx, nullptr), testing::HasSubstr("Error from custom function"));
466+
ASSERT_THAT(nix_err_msg(nullptr, ctx, nullptr), testing::HasSubstr("Test error from primop"));
467+
ASSERT_THAT(nix_err_msg(nullptr, ctx, nullptr), testing::HasSubstr("testErrorPrimop"));
468+
469+
// Clean up
470+
nix_gc_decref(ctx, primopValue);
471+
nix_gc_decref(ctx, arg);
472+
nix_gc_decref(ctx, result);
473+
}
474+
426475
TEST_F(nix_api_expr_test, nix_value_call_multi_no_args)
427476
{
428477
nix_value * n = nix_alloc_value(ctx, state);

0 commit comments

Comments
 (0)