use std::{
collections::HashSet,
ffi::CString,
marker::PhantomData,
os::raw::c_void,
panic::{self, AssertUnwindSafe},
sync::{Arc, Mutex, OnceLock},
};
use crate::{Context, Error, Result, ValueType, check_err, sys};
type PrimOpFn =
dyn Fn(&[PrimOpArg<'_>], &mut PrimOpRet<'_>) -> Result<()> + Send + Sync;
struct ClosureData {
arity: usize,
f: Box<PrimOpFn>,
}
unsafe extern "C" fn trampoline(
user_data: *mut c_void,
context: *mut sys::nix_c_context,
state: *mut sys::EvalState,
args: *mut *mut sys::nix_value,
ret: *mut sys::nix_value,
) {
let data = unsafe { &*(user_data as *const ClosureData) };
let arg_wrappers: Vec<PrimOpArg<'_>> = if data.arity == 0 {
Vec::new()
} else {
let arg_slice = unsafe { std::slice::from_raw_parts(args, data.arity) };
arg_slice
.iter()
.map(|&p| {
PrimOpArg {
inner: p,
ctx: context,
state,
_phantom: PhantomData,
}
})
.collect()
};
let mut ret_wrapper = PrimOpRet {
inner: ret,
ctx: context,
state,
written: false,
_phantom: PhantomData,
};
let result = panic::catch_unwind(AssertUnwindSafe(|| {
(data.f)(&arg_wrappers, &mut ret_wrapper)
}));
let err_msg = match result {
Ok(Ok(())) => {
if ret_wrapper.written {
return;
}
"primop returned Ok(()) without calling a set_* method on the return slot"
.to_string()
},
Ok(Err(e)) => format!("primop error: {e}"),
Err(payload) => {
let detail = if let Some(s) = payload.downcast_ref::<&'static str>() {
(*s).to_owned()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic payload".to_owned()
};
format!("primop panicked: {detail}")
},
};
let msg_c = CString::new(err_msg)
.unwrap_or_else(|_| CString::new("primop error").unwrap());
unsafe {
sys::nix_set_err_msg(
context,
sys::nix_err_NIX_ERR_NIX_ERROR,
msg_c.as_ptr(),
);
}
}
unsafe extern "C" fn drop_closure_finalizer(
_obj: *mut c_void,
cd: *mut c_void,
) {
let _ = unsafe { Box::from_raw(cd as *mut ClosureData) };
}
pub use crate::value_ops::NixValueOps;
use crate::value_ops::NixValueRaw;
pub struct PrimOpArg<'a> {
inner: *mut sys::nix_value,
ctx: *mut sys::nix_c_context,
state: *mut sys::EvalState,
_phantom: PhantomData<&'a ()>,
}
impl NixValueRaw for PrimOpArg<'_> {
fn raw_ctx(&self) -> *mut sys::nix_c_context {
self.ctx
}
fn raw_state(&self) -> *mut sys::EvalState {
self.state
}
fn raw_inner(&self) -> *mut sys::nix_value {
self.inner
}
}
impl PrimOpArg<'_> {
pub fn as_attrs(&self) -> Result<ArgAttrs<'_>> {
self.force()?;
if self.value_type() != ValueType::Attrs {
return Err(Error::InvalidType {
expected: "attrs",
actual: self.value_type().to_string(),
});
}
Ok(ArgAttrs {
inner: self.inner,
ctx: self.ctx,
state: self.state,
_phantom: PhantomData,
})
}
pub fn as_list(&self) -> Result<ArgList<'_>> {
self.force()?;
if self.value_type() != ValueType::List {
return Err(Error::InvalidType {
expected: "list",
actual: self.value_type().to_string(),
});
}
Ok(ArgList {
inner: self.inner,
ctx: self.ctx,
state: self.state,
_phantom: PhantomData,
})
}
}
pub struct PrimOpRet<'a> {
inner: *mut sys::nix_value,
ctx: *mut sys::nix_c_context,
state: *mut sys::EvalState,
written: bool,
_phantom: PhantomData<&'a mut ()>,
}
impl<'a> PrimOpRet<'a> {
fn mark_written(&mut self) {
self.written = true;
}
pub fn set_int(&mut self, i: i64) -> Result<()> {
unsafe { check_err(self.ctx, sys::nix_init_int(self.ctx, self.inner, i)) }?;
self.mark_written();
Ok(())
}
pub fn set_float(&mut self, f: f64) -> Result<()> {
unsafe {
check_err(self.ctx, sys::nix_init_float(self.ctx, self.inner, f))
}?;
self.mark_written();
Ok(())
}
pub fn set_bool(&mut self, b: bool) -> Result<()> {
unsafe {
check_err(self.ctx, sys::nix_init_bool(self.ctx, self.inner, b))
}?;
self.mark_written();
Ok(())
}
pub fn set_null(&mut self) -> Result<()> {
unsafe { check_err(self.ctx, sys::nix_init_null(self.ctx, self.inner)) }?;
self.mark_written();
Ok(())
}
pub fn set_string(&mut self, s: &str) -> Result<()> {
let s_c = CString::new(s)?;
unsafe {
check_err(
self.ctx,
sys::nix_init_string(self.ctx, self.inner, s_c.as_ptr()),
)
}?;
self.mark_written();
Ok(())
}
pub fn set_path(&mut self, p: &str) -> Result<()> {
let p_c = CString::new(p)?;
unsafe {
check_err(
self.ctx,
sys::nix_init_path_string(
self.ctx,
self.state,
self.inner,
p_c.as_ptr(),
),
)
}?;
self.mark_written();
Ok(())
}
#[cfg(all(feature = "shim", feature = "store"))]
pub fn set_store_path_typed(
&mut self,
store: &crate::store::Store,
path: &crate::store::StorePath,
) -> Result<()> {
let rendered = store.print_path(path)?;
self.set_store_path(&rendered)
}
#[cfg(feature = "shim")]
pub fn set_store_path(&mut self, p: &str) -> Result<()> {
let p_c = CString::new(p)?;
unsafe {
check_err(
self.ctx,
sys::nix_eval_state_allow_path(self.ctx, self.state, p_c.as_ptr()),
)?;
check_err(
self.ctx,
sys::nix_init_path_string(
self.ctx,
self.state,
self.inner,
p_c.as_ptr(),
),
)
}?;
self.mark_written();
Ok(())
}
pub unsafe fn copy_from_raw(
&mut self,
src: *mut sys::nix_value,
) -> Result<()> {
unsafe {
check_err(self.ctx, sys::nix_copy_value(self.ctx, self.inner, src))
}?;
self.mark_written();
Ok(())
}
pub fn set_attrs(
&mut self,
pairs: &[(&str, &PrimOpValue<'_>)],
) -> Result<()> {
let builder = unsafe {
sys::nix_make_bindings_builder(self.ctx, self.state, pairs.len().max(1))
};
if builder.is_null() {
return Err(Error::NullPointer);
}
struct BuilderGuard(*mut sys::BindingsBuilder);
impl Drop for BuilderGuard {
fn drop(&mut self) {
unsafe { sys::nix_bindings_builder_free(self.0) };
}
}
let _guard = BuilderGuard(builder);
for (key, value) in pairs {
let key_c = CString::new(*key)?;
unsafe {
check_err(
self.ctx,
sys::nix_bindings_builder_insert(
self.ctx,
builder,
key_c.as_ptr(),
value.inner,
),
)?;
}
}
unsafe {
check_err(self.ctx, sys::nix_make_attrs(self.ctx, self.inner, builder))
}?;
self.mark_written();
Ok(())
}
pub fn set_list(&mut self, items: &[&PrimOpValue<'_>]) -> Result<()> {
let builder = unsafe {
sys::nix_make_list_builder(self.ctx, self.state, items.len().max(1))
};
if builder.is_null() {
return Err(Error::NullPointer);
}
struct ListGuard(*mut sys::ListBuilder);
impl Drop for ListGuard {
fn drop(&mut self) {
unsafe { sys::nix_list_builder_free(self.0) };
}
}
let _guard = ListGuard(builder);
for (i, value) in items.iter().enumerate() {
unsafe {
check_err(
self.ctx,
sys::nix_list_builder_insert(
self.ctx,
builder,
i as std::os::raw::c_uint,
value.inner,
),
)?;
}
}
unsafe {
check_err(self.ctx, sys::nix_make_list(self.ctx, builder, self.inner))
}?;
self.mark_written();
Ok(())
}
pub fn make_int(&self, i: i64) -> Result<PrimOpValue<'a>> {
let v = PrimOpValue::alloc(self.ctx, self.state)?;
unsafe {
check_err(self.ctx, sys::nix_init_int(self.ctx, v.inner, i))?;
}
Ok(v)
}
pub fn make_float(&self, f: f64) -> Result<PrimOpValue<'a>> {
let v = PrimOpValue::alloc(self.ctx, self.state)?;
unsafe {
check_err(self.ctx, sys::nix_init_float(self.ctx, v.inner, f))?;
}
Ok(v)
}
pub fn make_bool(&self, b: bool) -> Result<PrimOpValue<'a>> {
let v = PrimOpValue::alloc(self.ctx, self.state)?;
unsafe {
check_err(self.ctx, sys::nix_init_bool(self.ctx, v.inner, b))?;
}
Ok(v)
}
pub fn make_null(&self) -> Result<PrimOpValue<'a>> {
let v = PrimOpValue::alloc(self.ctx, self.state)?;
unsafe {
check_err(self.ctx, sys::nix_init_null(self.ctx, v.inner))?;
}
Ok(v)
}
pub fn make_string(&self, s: &str) -> Result<PrimOpValue<'a>> {
let v = PrimOpValue::alloc(self.ctx, self.state)?;
let s_c = CString::new(s)?;
unsafe {
check_err(
self.ctx,
sys::nix_init_string(self.ctx, v.inner, s_c.as_ptr()),
)?;
}
Ok(v)
}
pub fn make_path(&self, p: &str) -> Result<PrimOpValue<'a>> {
let v = PrimOpValue::alloc(self.ctx, self.state)?;
let p_c = CString::new(p)?;
unsafe {
check_err(
self.ctx,
sys::nix_init_path_string(self.ctx, self.state, v.inner, p_c.as_ptr()),
)?;
}
Ok(v)
}
#[cfg(all(feature = "shim", feature = "store"))]
pub fn make_store_path_typed(
&self,
store: &crate::store::Store,
path: &crate::store::StorePath,
) -> Result<PrimOpValue<'a>> {
let rendered = store.print_path(path)?;
self.make_store_path(&rendered)
}
#[cfg(feature = "shim")]
pub fn make_store_path(&self, p: &str) -> Result<PrimOpValue<'a>> {
let v = PrimOpValue::alloc(self.ctx, self.state)?;
let p_c = CString::new(p)?;
unsafe {
check_err(
self.ctx,
sys::nix_eval_state_allow_path(self.ctx, self.state, p_c.as_ptr()),
)?;
check_err(
self.ctx,
sys::nix_init_path_string(self.ctx, self.state, v.inner, p_c.as_ptr()),
)?;
}
Ok(v)
}
}
pub struct PrimOpValue<'a> {
inner: *mut sys::nix_value,
ctx: *mut sys::nix_c_context,
state: *mut sys::EvalState,
_phantom: PhantomData<&'a ()>,
}
impl NixValueRaw for PrimOpValue<'_> {
fn raw_ctx(&self) -> *mut sys::nix_c_context {
self.ctx
}
fn raw_state(&self) -> *mut sys::EvalState {
self.state
}
fn raw_inner(&self) -> *mut sys::nix_value {
self.inner
}
}
impl<'a> PrimOpValue<'a> {
fn alloc(
ctx: *mut sys::nix_c_context,
state: *mut sys::EvalState,
) -> Result<Self> {
let inner = unsafe { sys::nix_alloc_value(ctx, state) };
if inner.is_null() {
return Err(Error::NullPointer);
}
Ok(PrimOpValue {
inner,
ctx,
state,
_phantom: PhantomData,
})
}
pub fn as_attrs(&self) -> Result<ArgAttrs<'_>> {
self.force()?;
if self.value_type() != ValueType::Attrs {
return Err(Error::InvalidType {
expected: "attrs",
actual: self.value_type().to_string(),
});
}
Ok(ArgAttrs {
inner: self.inner,
ctx: self.ctx,
state: self.state,
_phantom: PhantomData,
})
}
pub fn as_list(&self) -> Result<ArgList<'_>> {
self.force()?;
if self.value_type() != ValueType::List {
return Err(Error::InvalidType {
expected: "list",
actual: self.value_type().to_string(),
});
}
Ok(ArgList {
inner: self.inner,
ctx: self.ctx,
state: self.state,
_phantom: PhantomData,
})
}
}
impl Drop for PrimOpValue<'_> {
fn drop(&mut self) {
unsafe {
sys::nix_value_decref(self.ctx, self.inner);
}
}
}
pub struct ArgAttrs<'a> {
inner: *mut sys::nix_value,
ctx: *mut sys::nix_c_context,
state: *mut sys::EvalState,
_phantom: PhantomData<&'a ()>,
}
impl<'a> ArgAttrs<'a> {
pub fn get(&self, key: &str) -> Result<PrimOpValue<'a>> {
let key_c = CString::new(key)?;
let ptr = unsafe {
sys::nix_get_attr_byname(self.ctx, self.inner, self.state, key_c.as_ptr())
};
if ptr.is_null() {
return Err(Error::KeyNotFound(key.to_string()));
}
Ok(PrimOpValue {
inner: ptr,
ctx: self.ctx,
state: self.state,
_phantom: PhantomData,
})
}
pub fn has(&self, key: &str) -> Result<bool> {
let key_c = CString::new(key)?;
let result = unsafe {
sys::nix_has_attr_byname(self.ctx, self.inner, self.state, key_c.as_ptr())
};
Ok(result)
}
pub fn keys(&self) -> Result<Vec<String>> {
let count = unsafe { sys::nix_get_attrs_size(self.ctx, self.inner) };
let mut keys = Vec::with_capacity(count as usize);
for i in 0..count {
let name_ptr = unsafe {
sys::nix_get_attr_name_byidx(self.ctx, self.inner, self.state, i)
};
if name_ptr.is_null() {
continue;
}
let name = unsafe {
std::ffi::CStr::from_ptr(name_ptr)
.to_string_lossy()
.into_owned()
};
keys.push(name);
}
Ok(keys)
}
#[must_use]
pub fn len(&self) -> usize {
unsafe { sys::nix_get_attrs_size(self.ctx, self.inner) as usize }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct ArgList<'a> {
inner: *mut sys::nix_value,
ctx: *mut sys::nix_c_context,
state: *mut sys::EvalState,
_phantom: PhantomData<&'a ()>,
}
impl<'a> ArgList<'a> {
pub fn get(&self, index: usize) -> Result<PrimOpValue<'a>> {
let length = self.len();
if index >= length {
return Err(Error::IndexOutOfBounds { index, length });
}
let ptr = unsafe {
sys::nix_get_list_byidx(
self.ctx,
self.inner,
self.state,
index as std::os::raw::c_uint,
)
};
if ptr.is_null() {
return Err(Error::NullPointer);
}
Ok(PrimOpValue {
inner: ptr,
ctx: self.ctx,
state: self.state,
_phantom: PhantomData,
})
}
#[must_use]
pub fn len(&self) -> usize {
unsafe { sys::nix_get_list_size(self.ctx, self.inner) as usize }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct PrimOp {
inner: *mut sys::PrimOp,
context: Arc<Context>,
name: String,
registered: bool,
}
static REGISTERED_PRIMOPS: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
unsafe impl Send for PrimOp {}
impl PrimOp {
pub fn new<F>(
context: &Arc<Context>,
name: &str,
arity: u32,
doc: Option<&str>,
f: F,
) -> Result<Self>
where
F: Fn(&[PrimOpArg<'_>], &mut PrimOpRet<'_>) -> Result<()>
+ Send
+ Sync
+ 'static,
{
let name_c = CString::new(name)?;
let doc_c = doc.map(CString::new).transpose()?;
let empty_doc;
let doc_ptr = match doc_c {
Some(ref c) => c.as_ptr(),
None => {
empty_doc = CString::default();
empty_doc.as_ptr()
},
};
let data = Box::new(ClosureData {
arity: arity as usize,
f: Box::new(f),
});
let data_raw = Box::into_raw(data) as *mut c_void;
let primop_ptr = unsafe {
sys::nix_alloc_primop(
context.as_ptr(),
Some(trampoline),
arity as std::os::raw::c_int,
name_c.as_ptr(),
std::ptr::null_mut(), doc_ptr,
data_raw,
)
};
if primop_ptr.is_null() {
let _ = unsafe { Box::from_raw(data_raw as *mut ClosureData) };
unsafe {
check_err(context.as_ptr(), sys::nix_err_code(context.as_ptr()))?;
}
return Err(Error::NullPointer);
}
unsafe {
sys::nix_gc_register_finalizer(
primop_ptr as *mut c_void,
data_raw,
Some(drop_closure_finalizer),
);
}
Ok(PrimOp {
inner: primop_ptr,
context: Arc::clone(context),
name: name.to_string(),
registered: false,
})
}
pub fn register(mut self, context: &Context) -> Result<()> {
{
let names = REGISTERED_PRIMOPS.get_or_init(|| Mutex::new(HashSet::new()));
let mut guard = names.lock().expect("REGISTERED_PRIMOPS poisoned");
if !guard.insert(self.name.clone()) {
return Err(Error::Unknown(format!(
"primop '{}' is already registered globally",
self.name
)));
}
}
let err = unsafe { sys::nix_register_primop(context.as_ptr(), self.inner) };
check_err(unsafe { self.context.as_ptr() }, err)?;
self.registered = true;
Ok(())
}
pub fn into_value(
self,
state: &crate::EvalState,
) -> Result<crate::Value<'_>> {
let v = state.alloc_value()?;
unsafe {
check_err(
state.context.as_ptr(),
sys::nix_init_primop(
state.context.as_ptr(),
v.inner.as_ptr(),
self.inner,
),
)?;
}
Ok(v)
}
}
impl Drop for PrimOp {
fn drop(&mut self) {
if !self.registered && !self.inner.is_null() {
unsafe {
let _ = sys::nix_gc_decref(
self.context.as_ptr(),
self.inner as *const c_void,
);
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use serial_test::serial;
use super::*;
use crate::{Context, EvalStateBuilder, Store};
#[test]
#[serial]
fn test_primop_into_value() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let primop =
PrimOp::new(&ctx, "negate", 1, Some("Negate an integer"), |args, ret| {
let n = args[0].as_int()?;
ret.set_int(-n)
})
.expect("Failed to create primop");
let func = primop
.into_value(&state)
.expect("Failed to embed primop as value");
let arg = state.make_int(7).unwrap();
let result = func.call(&arg).expect("Failed to call primop");
assert_eq!(result.as_int().unwrap(), -7);
}
#[test]
#[serial]
fn test_primop_into_value_string() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let primop = PrimOp::new(&ctx, "hello", 1, None, |_args, ret| {
ret.set_string("hello from primop")
})
.expect("Failed to create primop");
let func = primop
.into_value(&state)
.expect("Failed to embed primop as value");
let arg = state.make_null().unwrap();
let result = func.call(&arg).expect("Failed to call primop");
assert_eq!(result.as_string().unwrap(), "hello from primop");
}
#[test]
#[serial]
fn test_primop_path_roundtrip() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let primop = PrimOp::new(&ctx, "echo_path", 1, None, |args, ret| {
let p = args[0].as_path()?;
assert_eq!(p, "/tmp/nix-bindings-path-in");
let v = ret.make_path("/tmp/nix-bindings-path-mid")?;
assert_eq!(v.as_path()?, "/tmp/nix-bindings-path-mid");
ret.set_path("/tmp/nix-bindings-path-out")
})
.expect("Failed to create primop");
let func = primop
.into_value(&state)
.expect("Failed to embed primop as value");
let arg = state
.make_path("/tmp/nix-bindings-path-in")
.expect("make_path failed");
let result = func.call(&arg).expect("Failed to call primop");
assert_eq!(result.value_type(), ValueType::Path);
let out = result.as_path().expect("Value::as_path failed");
assert_eq!(out.to_str(), Some("/tmp/nix-bindings-path-out"));
}
#[test]
#[serial]
fn test_primop_arg_as_list() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let list_val = state
.eval_from_string("[10 20 30]", "<eval>")
.expect("Failed to evaluate list");
let primop = PrimOp::new(&ctx, "list_sum", 1, None, |args, ret| {
let list = args[0].as_list()?;
let mut sum = 0i64;
for i in 0..list.len() {
sum += list.get(i)?.as_int()?;
}
ret.set_int(sum)
})
.expect("Failed to create primop");
let func = primop
.into_value(&state)
.expect("Failed to embed primop as value");
let result = func.call(&list_val).expect("Failed to call primop");
assert_eq!(result.as_int().unwrap(), 60);
}
#[test]
#[serial]
fn test_primop_arg_as_attrs() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let attrs_val = state
.eval_from_string("{ foo = 42; bar = 13; }", "<eval>")
.expect("Failed to evaluate attrs");
let primop = PrimOp::new(&ctx, "attr_sum", 1, None, |args, ret| {
let attrs = args[0].as_attrs()?;
let foo = attrs.get("foo")?.as_int()?;
let bar = attrs.get("bar")?.as_int()?;
ret.set_int(foo + bar)
})
.expect("Failed to create primop");
let func = primop
.into_value(&state)
.expect("Failed to embed primop as value");
let result = func.call(&attrs_val).expect("Failed to call primop");
assert_eq!(result.as_int().unwrap(), 55);
}
#[test]
#[serial]
fn test_primop_arg_attrs_has_and_keys() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let attrs_val = state
.eval_from_string("{ a = 1; b = 2; c = 3; }", "<eval>")
.expect("Failed to evaluate attrs");
let primop = PrimOp::new(&ctx, "check_attrs", 1, None, |args, ret| {
let attrs = args[0].as_attrs()?;
assert_eq!(attrs.len(), 3);
assert!(!attrs.is_empty());
assert!(attrs.has("a")?);
assert!(attrs.has("b")?);
assert!(attrs.has("c")?);
assert!(!attrs.has("zzz")?);
let keys = attrs.keys()?;
assert_eq!(keys.len(), 3);
ret.set_null()
})
.expect("Failed to create primop");
let func = primop
.into_value(&state)
.expect("Failed to embed primop as value");
func.call(&attrs_val).expect("Failed to call primop");
}
#[test]
#[serial]
fn test_primop_empty_attrs_and_list() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let empty_attrs = state
.eval_from_string("{}", "<eval>")
.expect("Failed to evaluate empty attrs");
let primop = PrimOp::new(&ctx, "empty_check", 1, None, |args, ret| {
match args[0].as_attrs() {
Ok(a) => {
assert!(a.is_empty());
assert_eq!(a.len(), 0);
},
Err(_) => {
let list = args[0].as_list()?;
assert!(list.is_empty());
assert_eq!(list.len(), 0);
},
}
ret.set_null()
})
.expect("Failed to create primop");
let func = primop
.into_value(&state)
.expect("Failed to embed primop as value");
func
.call(&empty_attrs)
.expect("call with empty attrs failed");
let empty_list = state
.eval_from_string("[]", "<eval>")
.expect("Failed to evaluate empty list");
func.call(&empty_list).expect("call with empty list failed");
}
#[test]
#[serial]
fn test_primop_ret_set_attrs() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
PrimOp::new(&ctx, "mk_attrs_test", 0, None, |_args, ret| {
let a = ret.make_int(100)?;
let b = ret.make_string("hi")?;
ret.set_attrs(&[("x", &a), ("y", &b)])
})
.expect("Failed to create primop")
.register(&ctx)
.expect("Failed to register primop");
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let result = state
.eval_from_string("builtins.mk_attrs_test", "<eval>")
.expect("Failed to evaluate expression");
assert_eq!(result.value_type(), ValueType::Attrs);
assert_eq!(result.attr_keys().unwrap().len(), 2);
let x = result.get_attr("x").expect("missing x");
assert_eq!(x.as_int().unwrap(), 100);
let y = result.get_attr("y").expect("missing y");
assert_eq!(y.as_string().unwrap(), "hi");
}
#[test]
#[serial]
fn test_primop_ret_set_list() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
PrimOp::new(&ctx, "mk_list_test", 0, None, |_args, ret| {
let a = ret.make_int(7)?;
let b = ret.make_string("hi")?;
let c = ret.make_bool(true)?;
ret.set_list(&[&a, &b, &c])
})
.expect("Failed to create primop")
.register(&ctx)
.expect("Failed to register primop");
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let result = state
.eval_from_string("builtins.mk_list_test", "<eval>")
.expect("Failed to evaluate expression");
assert_eq!(result.value_type(), ValueType::List);
assert_eq!(result.list_len().unwrap(), 3);
let first = result.list_get(0).unwrap();
assert_eq!(first.as_int().unwrap(), 7);
let second = result.list_get(1).unwrap();
assert_eq!(second.as_string().unwrap(), "hi");
let third = result.list_get(2).unwrap();
assert!(third.as_bool().unwrap());
}
#[test]
#[serial]
fn test_primop_ret_make_types() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
PrimOp::new(&ctx, "mk_types_test", 0, None, |_args, ret| {
let int_val = ret.make_int(-42)?;
assert_eq!(int_val.value_type(), ValueType::Int);
assert_eq!(int_val.as_int()?, -42);
let float_val = ret.make_float(2.5)?;
assert_eq!(float_val.value_type(), ValueType::Float);
assert!((float_val.as_float()? - 2.5).abs() < 1e-9);
let bool_val = ret.make_bool(true)?;
assert_eq!(bool_val.value_type(), ValueType::Bool);
assert!(bool_val.as_bool()?);
let null_val = ret.make_null()?;
assert_eq!(null_val.value_type(), ValueType::Null);
let str_val = ret.make_string("ok")?;
assert_eq!(str_val.value_type(), ValueType::String);
assert_eq!(str_val.as_string()?, "ok");
ret.set_int(0)
})
.expect("Failed to create primop")
.register(&ctx)
.expect("Failed to register primop");
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let result = state
.eval_from_string("builtins.mk_types_test", "<eval>")
.expect("Failed to evaluate expression");
assert_eq!(result.as_int().unwrap(), 0);
}
#[test]
#[serial]
fn test_primop_value_as_attrs_chained() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let nested = state
.eval_from_string("{ inner = { x = 99; }; }", "<eval>")
.expect("Failed to evaluate nested attrs");
let primop = PrimOp::new(&ctx, "nested_get", 1, None, |args, ret| {
let outer = args[0].as_attrs()?;
let inner = outer.get("inner")?;
let inner_attrs = inner.as_attrs()?;
let x = inner_attrs.get("x")?.as_int()?;
ret.set_int(x)
})
.expect("Failed to create primop");
let func = primop
.into_value(&state)
.expect("Failed to embed primop as value");
let result = func.call(&nested).expect("Failed to call primop");
assert_eq!(result.as_int().unwrap(), 99);
}
#[test]
#[serial]
fn test_primop_unwritten_slot_is_diagnosed() {
let ctx = Arc::new(Context::new().expect("Failed to create context"));
let store =
Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
let state = EvalStateBuilder::new(&store)
.expect("Failed to create builder")
.build()
.expect("Failed to build state");
let primop =
PrimOp::new(&ctx, "broken_primop", 1, None, |_args, _ret| Ok(()))
.expect("Failed to create primop");
let func = primop
.into_value(&state)
.expect("Failed to embed primop as value");
let arg = state.make_int(1).expect("Failed to make int");
let result = func.call(&arg);
assert!(
result.is_err(),
"Expected an error when the primop returns without writing the slot, \
got {:?}",
result.map(|v| v.value_type()),
);
}
}