commit ef49bfa252e6ffb6a345ba3f0d5e48c0511b25a2
parent 9371d567fe1b4e0c5d22b2e861bbf0f37d31570f
Author: Erik Loualiche <[email protected]>
Date: Fri, 6 Feb 2026 09:28:57 -0600
Add native FFI protocol with binary type encoding
- Add nickel_eval_native() for binary-encoded native types
- Binary protocol preserves type information (null, bool, int, float, string, array, record)
- Use malachite RoundingFrom for accurate number conversion
- Add comprehensive tests for all Nickel types (33 tests total)
- Create CLAUDE.md with project development guide
- Avoid unwrap() - use proper error handling throughout
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Diffstat:
4 files changed, 690 insertions(+), 14 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -20,3 +20,4 @@ deps/*.dll
# OS
.DS_Store
Thumbs.db
+.claude/
diff --git a/CLAUDE.md b/CLAUDE.md
@@ -0,0 +1,166 @@
+# NickelEval.jl Development Guide
+
+## Project Overview
+
+NickelEval.jl provides Julia bindings for the [Nickel](https://nickel-lang.org/) configuration language. It supports both subprocess-based evaluation (using the Nickel CLI) and native FFI evaluation (using a Rust wrapper around nickel-lang-core).
+
+## Architecture
+
+```
+NickelEval/
+├── src/
+│ ├── NickelEval.jl # Main module
+│ ├── subprocess.jl # CLI-based evaluation
+│ └── ffi.jl # Native FFI bindings
+├── rust/
+│ └── nickel-jl/ # Rust FFI wrapper
+│ ├── Cargo.toml
+│ └── src/lib.rs
+├── deps/
+│ └── build.jl # Build script for FFI
+└── test/
+ └── test_subprocess.jl
+```
+
+## Key Design Decisions
+
+### 1. Use JSON.jl 1.0 (not JSON3.jl)
+
+JSON.jl 1.0 provides:
+- Native typed parsing with `JSON.parse(json, T)`
+- `JSON.Object` return type with dot-access for records
+- Better Julia integration
+
+### 2. Types from Nickel FFI, Not JSON
+
+The Rust FFI returns a binary protocol with native type information:
+- Type tags: 0=Null, 1=Bool, 2=Int64, 3=Float64, 4=String, 5=Array, 6=Record
+- Direct memory encoding without JSON serialization overhead
+- Preserves integer vs float distinction
+
+### 3. Avoid `unwrap()` in Rust
+
+Use proper error handling:
+```rust
+// Bad
+let f = value.to_f64().unwrap();
+
+// Good
+let f = f64::try_from(value).map_err(|e| format!("Error: {:?}", e))?;
+```
+
+For number conversion, use malachite's `RoundingFrom` trait to handle inexact conversions:
+```rust
+use malachite::rounding_modes::RoundingMode;
+use malachite::num::conversion::traits::RoundingFrom;
+
+let (f, _) = f64::rounding_from(&rational, RoundingMode::Nearest);
+```
+
+## Building
+
+### Rust FFI Library
+
+```bash
+cd rust/nickel-jl
+cargo build --release
+cp target/release/libnickel_jl.dylib ../../deps/ # macOS
+# or libnickel_jl.so on Linux, nickel_jl.dll on Windows
+```
+
+### Running Tests
+
+```bash
+# Rust tests
+cd rust/nickel-jl
+cargo test
+
+# Julia tests (requires Nickel CLI installed)
+julia --project=. -e 'using Pkg; Pkg.test()'
+```
+
+## Binary Protocol Specification
+
+The FFI uses a binary protocol for native type encoding:
+
+| Type Tag | Encoding |
+|----------|----------|
+| 0 (Null) | Just the tag byte |
+| 1 (Bool) | Tag + 1 byte (0=false, 1=true) |
+| 2 (Int64) | Tag + 8 bytes (little-endian i64) |
+| 3 (Float64) | Tag + 8 bytes (little-endian f64) |
+| 4 (String) | Tag + 4 bytes length + UTF-8 bytes |
+| 5 (Array) | Tag + 4 bytes count + elements |
+| 6 (Record) | Tag + 4 bytes field count + (key_len, key, value)* |
+
+## API Functions
+
+### Evaluation
+
+- `nickel_eval(code)` - Evaluate to `JSON.Object`
+- `nickel_eval(code, T)` - Evaluate and convert to type `T`
+- `nickel_eval_file(path)` - Evaluate a `.ncl` file
+- `nickel_eval_ffi(code)` - FFI-based evaluation (faster)
+
+### Export
+
+- `nickel_to_json(code)` - Export to JSON string
+- `nickel_to_toml(code)` - Export to TOML string
+- `nickel_to_yaml(code)` - Export to YAML string
+- `nickel_export(code; format=:json)` - Export to any format
+
+## Type Conversion
+
+| Nickel Type | Julia Type |
+|-------------|------------|
+| Null | `nothing` |
+| Bool | `Bool` |
+| Number (integer) | `Int64` |
+| Number (float) | `Float64` |
+| String | `String` |
+| Array | `Vector` or `JSON.Array` |
+| Record | `JSON.Object`, `Dict`, `NamedTuple`, or struct |
+
+## Nickel Language Reference
+
+Common patterns used in tests:
+
+```nickel
+# Let bindings
+let x = 1 in x + 2
+
+# Functions
+let double = fun x => x * 2 in double 21
+
+# Records
+{ name = "test", value = 42 }
+
+# Record merge
+{ a = 1 } & { b = 2 }
+
+# Arrays
+[1, 2, 3]
+
+# Array operations
+[1, 2, 3] |> std.array.map (fun x => x * 2)
+
+# Nested structures
+{ outer = { inner = 42 } }
+```
+
+## Dependencies
+
+### Julia
+- JSON.jl >= 1.0
+
+### Rust
+- nickel-lang-core = "0.9"
+- malachite = "0.4"
+- serde_json = "1.0"
+
+## Future Improvements
+
+1. Complete Julia-side binary protocol decoder
+2. Support for Nickel contracts/types in Julia
+3. Streaming evaluation for large configs
+4. REPL integration
diff --git a/rust/nickel-jl/Cargo.toml b/rust/nickel-jl/Cargo.toml
@@ -10,6 +10,7 @@ crate-type = ["cdylib"]
[dependencies]
nickel-lang-core = "0.9"
serde_json = "1.0"
+malachite = "0.4"
[profile.release]
opt-level = 3
diff --git a/rust/nickel-jl/src/lib.rs b/rust/nickel-jl/src/lib.rs
@@ -6,8 +6,10 @@
//! # Functions
//!
//! - `nickel_eval_string`: Evaluate Nickel code and return JSON string
+//! - `nickel_eval_native`: Evaluate Nickel code and return binary-encoded native types
//! - `nickel_get_error`: Get the last error message
//! - `nickel_free_string`: Free allocated string memory
+//! - `nickel_free_buffer`: Free allocated binary buffer
use std::ffi::{CStr, CString};
use std::io::Cursor;
@@ -17,12 +19,32 @@ use std::ptr;
use nickel_lang_core::eval::cache::lazy::CBNCache;
use nickel_lang_core::program::Program;
use nickel_lang_core::serialize::{self, ExportFormat};
+use nickel_lang_core::term::{RichTerm, Term};
+
+use malachite::rounding_modes::RoundingMode;
+use malachite::num::conversion::traits::RoundingFrom;
// Thread-local storage for the last error message
thread_local! {
static LAST_ERROR: std::cell::RefCell<Option<CString>> = const { std::cell::RefCell::new(None) };
}
+// Type tags for binary protocol
+const TYPE_NULL: u8 = 0;
+const TYPE_BOOL: u8 = 1;
+const TYPE_INT: u8 = 2;
+const TYPE_FLOAT: u8 = 3;
+const TYPE_STRING: u8 = 4;
+const TYPE_ARRAY: u8 = 5;
+const TYPE_RECORD: u8 = 6;
+
+/// Result buffer for native evaluation
+#[repr(C)]
+pub struct NativeBuffer {
+ pub data: *mut u8,
+ pub len: usize,
+}
+
/// Evaluate a Nickel code string and return the result as a JSON string.
///
/// # Safety
@@ -44,7 +66,7 @@ pub unsafe extern "C" fn nickel_eval_string(code: *const c_char) -> *const c_cha
}
};
- match eval_nickel(code_str) {
+ match eval_nickel_json(code_str) {
Ok(json) => {
match CString::new(json) {
Ok(cstr) => cstr.into_raw(),
@@ -61,25 +83,155 @@ pub unsafe extern "C" fn nickel_eval_string(code: *const c_char) -> *const c_cha
}
}
+/// Evaluate Nickel code and return binary-encoded native types.
+///
+/// Binary protocol:
+/// - Type tag (1 byte): 0=Null, 1=Bool, 2=Int64, 3=Float64, 4=String, 5=Array, 6=Record
+/// - Value data (varies by type)
+///
+/// # Safety
+/// - `code` must be a valid null-terminated C string
+/// - The returned buffer must be freed with `nickel_free_buffer`
+/// - Returns NativeBuffer with null data on error; use `nickel_get_error` for message
+#[no_mangle]
+pub unsafe extern "C" fn nickel_eval_native(code: *const c_char) -> NativeBuffer {
+ let null_buffer = NativeBuffer { data: ptr::null_mut(), len: 0 };
+
+ if code.is_null() {
+ set_error("Null pointer passed to nickel_eval_native");
+ return null_buffer;
+ }
+
+ let code_str = match CStr::from_ptr(code).to_str() {
+ Ok(s) => s,
+ Err(e) => {
+ set_error(&format!("Invalid UTF-8 in input: {}", e));
+ return null_buffer;
+ }
+ };
+
+ match eval_nickel_native(code_str) {
+ Ok(buffer) => {
+ let len = buffer.len();
+ let boxed = buffer.into_boxed_slice();
+ let data = Box::into_raw(boxed) as *mut u8;
+ NativeBuffer { data, len }
+ }
+ Err(e) => {
+ set_error(&e);
+ null_buffer
+ }
+ }
+}
+
/// Internal function to evaluate Nickel code and return JSON.
-fn eval_nickel(code: &str) -> Result<String, String> {
- // Create a source from the code string
+fn eval_nickel_json(code: &str) -> Result<String, String> {
let source = Cursor::new(code.as_bytes());
-
- // Create a program with a null trace (discard trace output)
let mut program: Program<CBNCache> = Program::new_from_source(source, "<ffi>", std::io::sink())
.map_err(|e| format!("Parse error: {}", e))?;
- // Evaluate the program fully for export
let result = program
.eval_full_for_export()
.map_err(|e| program.report_as_str(e))?;
- // Serialize to JSON
serialize::to_string(ExportFormat::Json, &result)
.map_err(|e| format!("Serialization error: {:?}", e))
}
+/// Internal function to evaluate Nickel code and return binary-encoded native types.
+fn eval_nickel_native(code: &str) -> Result<Vec<u8>, String> {
+ let source = Cursor::new(code.as_bytes());
+ let mut program: Program<CBNCache> = Program::new_from_source(source, "<ffi>", std::io::sink())
+ .map_err(|e| format!("Parse error: {}", e))?;
+
+ let result = program
+ .eval_full_for_export()
+ .map_err(|e| program.report_as_str(e))?;
+
+ let mut buffer = Vec::new();
+ encode_term(&result, &mut buffer)?;
+ Ok(buffer)
+}
+
+/// Encode a Nickel term to binary format
+fn encode_term(term: &RichTerm, buffer: &mut Vec<u8>) -> Result<(), String> {
+ match term.as_ref() {
+ Term::Null => {
+ buffer.push(TYPE_NULL);
+ }
+ Term::Bool(b) => {
+ buffer.push(TYPE_BOOL);
+ buffer.push(if *b { 1 } else { 0 });
+ }
+ Term::Num(n) => {
+ // Convert to f64 using nearest rounding mode
+ let (f, _) = f64::rounding_from(n, RoundingMode::Nearest);
+ // Try to represent as integer if possible
+ if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
+ buffer.push(TYPE_INT);
+ buffer.extend_from_slice(&(f as i64).to_le_bytes());
+ } else {
+ buffer.push(TYPE_FLOAT);
+ buffer.extend_from_slice(&f.to_le_bytes());
+ }
+ }
+ Term::Str(s) => {
+ buffer.push(TYPE_STRING);
+ let bytes = s.as_str().as_bytes();
+ buffer.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
+ buffer.extend_from_slice(bytes);
+ }
+ Term::Array(arr, _) => {
+ buffer.push(TYPE_ARRAY);
+ buffer.extend_from_slice(&(arr.len() as u32).to_le_bytes());
+ for elem in arr.iter() {
+ encode_term(elem, buffer)?;
+ }
+ }
+ Term::Record(record) => {
+ buffer.push(TYPE_RECORD);
+ let fields: Vec<_> = record.fields.iter().collect();
+ buffer.extend_from_slice(&(fields.len() as u32).to_le_bytes());
+ for (key, field) in fields {
+ // Encode field name
+ let key_bytes = key.label().as_bytes();
+ buffer.extend_from_slice(&(key_bytes.len() as u32).to_le_bytes());
+ buffer.extend_from_slice(key_bytes);
+ // Encode field value
+ if let Some(ref value) = field.value {
+ encode_term(value, buffer)?;
+ } else {
+ buffer.push(TYPE_NULL);
+ }
+ }
+ }
+ Term::EnumVariant { tag, arg, .. } => {
+ // Encode enum variants as records with _tag and _value fields
+ buffer.push(TYPE_RECORD);
+ buffer.extend_from_slice(&2u32.to_le_bytes()); // 2 fields
+
+ // _tag field
+ let tag_key = b"_tag";
+ buffer.extend_from_slice(&(tag_key.len() as u32).to_le_bytes());
+ buffer.extend_from_slice(tag_key);
+ buffer.push(TYPE_STRING);
+ let tag_bytes = tag.label().as_bytes();
+ buffer.extend_from_slice(&(tag_bytes.len() as u32).to_le_bytes());
+ buffer.extend_from_slice(tag_bytes);
+
+ // _value field
+ let value_key = b"_value";
+ buffer.extend_from_slice(&(value_key.len() as u32).to_le_bytes());
+ buffer.extend_from_slice(value_key);
+ encode_term(arg, buffer)?;
+ }
+ other => {
+ return Err(format!("Unsupported term type for native encoding: {:?}", other));
+ }
+ }
+ Ok(())
+}
+
/// Get the last error message.
///
/// # Safety
@@ -108,6 +260,18 @@ pub unsafe extern "C" fn nickel_free_string(ptr: *const c_char) {
}
}
+/// Free a binary buffer allocated by this library.
+///
+/// # Safety
+/// - `buffer` must have been returned by `nickel_eval_native`
+/// - The buffer must not be used after this call
+#[no_mangle]
+pub unsafe extern "C" fn nickel_free_buffer(buffer: NativeBuffer) {
+ if !buffer.data.is_null() && buffer.len > 0 {
+ let _ = Box::from_raw(std::slice::from_raw_parts_mut(buffer.data, buffer.len));
+ }
+}
+
fn set_error(msg: &str) {
LAST_ERROR.with(|e| {
*e.borrow_mut() = CString::new(msg).ok();
@@ -132,7 +296,6 @@ mod tests {
#[test]
fn test_free_null() {
unsafe {
- // Should not crash
nickel_free_string(ptr::null());
}
}
@@ -171,7 +334,6 @@ mod tests {
assert!(!result.is_null(), "Expected result, got error: {:?}",
CStr::from_ptr(nickel_get_error()).to_str());
let result_str = CStr::from_ptr(result).to_str().unwrap();
- // JSON output should have the fields
assert!(result_str.contains("\"x\""));
assert!(result_str.contains("\"y\""));
nickel_free_string(result);
@@ -186,7 +348,6 @@ mod tests {
assert!(!result.is_null(), "Expected result, got error: {:?}",
CStr::from_ptr(nickel_get_error()).to_str());
let result_str = CStr::from_ptr(result).to_str().unwrap();
- // JSON output is pretty-printed, so check for presence of elements
assert!(result_str.contains("1"));
assert!(result_str.contains("2"));
assert!(result_str.contains("3"));
@@ -221,13 +382,360 @@ mod tests {
}
#[test]
- fn test_eval_internal() {
- // Test the internal eval_nickel function directly
- let result = eval_nickel("42").unwrap();
+ fn test_native_int() {
+ unsafe {
+ let code = CString::new("42").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_INT);
+ let value = i64::from_le_bytes(data[1..9].try_into().unwrap());
+ assert_eq!(value, 42);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_float() {
+ unsafe {
+ let code = CString::new("3.14").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ if buffer.data.is_null() {
+ let err = nickel_get_error();
+ if !err.is_null() {
+ panic!("Error: {:?}", CStr::from_ptr(err).to_str());
+ }
+ }
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_FLOAT);
+ let value = f64::from_le_bytes(data[1..9].try_into().unwrap());
+ assert!((value - 3.14).abs() < 0.001);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_string() {
+ unsafe {
+ let code = CString::new(r#""hello""#).unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_STRING);
+ let len = u32::from_le_bytes(data[1..5].try_into().unwrap()) as usize;
+ let s = std::str::from_utf8(&data[5..5+len]).unwrap();
+ assert_eq!(s, "hello");
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_bool() {
+ unsafe {
+ let code = CString::new("true").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_BOOL);
+ assert_eq!(data[1], 1);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_array() {
+ unsafe {
+ let code = CString::new("[1, 2, 3]").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_ARRAY);
+ let len = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(len, 3);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_record() {
+ unsafe {
+ let code = CString::new("{ x = 1 }").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_RECORD);
+ let field_count = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(field_count, 1);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_eval_json_internal() {
+ let result = eval_nickel_json("42").unwrap();
assert_eq!(result, "42");
- let result = eval_nickel("{ a = 1 }").unwrap();
+ let result = eval_nickel_json("{ a = 1 }").unwrap();
assert!(result.contains("\"a\""));
assert!(result.contains("1"));
}
+
+ // Comprehensive tests for all Nickel types
+
+ #[test]
+ fn test_native_null() {
+ unsafe {
+ let code = CString::new("null").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_NULL);
+ assert_eq!(buffer.len, 1);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_bool_false() {
+ unsafe {
+ let code = CString::new("false").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_BOOL);
+ assert_eq!(data[1], 0);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_negative_int() {
+ unsafe {
+ let code = CString::new("-42").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_INT);
+ let value = i64::from_le_bytes(data[1..9].try_into().unwrap());
+ assert_eq!(value, -42);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_large_int() {
+ unsafe {
+ let code = CString::new("1000000000000").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_INT);
+ let value = i64::from_le_bytes(data[1..9].try_into().unwrap());
+ assert_eq!(value, 1000000000000i64);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_negative_float() {
+ unsafe {
+ let code = CString::new("-2.718").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_FLOAT);
+ let value = f64::from_le_bytes(data[1..9].try_into().unwrap());
+ assert!((value - (-2.718)).abs() < 0.001);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_empty_string() {
+ unsafe {
+ let code = CString::new(r#""""#).unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_STRING);
+ let len = u32::from_le_bytes(data[1..5].try_into().unwrap()) as usize;
+ assert_eq!(len, 0);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_unicode_string() {
+ unsafe {
+ let code = CString::new(r#""hello 世界 🌍""#).unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_STRING);
+ let len = u32::from_le_bytes(data[1..5].try_into().unwrap()) as usize;
+ let s = std::str::from_utf8(&data[5..5+len]).unwrap();
+ assert_eq!(s, "hello 世界 🌍");
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_empty_array() {
+ unsafe {
+ let code = CString::new("[]").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_ARRAY);
+ let len = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(len, 0);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_mixed_array() {
+ unsafe {
+ // Array with int, string, bool
+ let code = CString::new(r#"[1, "two", true]"#).unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_ARRAY);
+ let len = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(len, 3);
+ // First element: int 1
+ assert_eq!(data[5], TYPE_INT);
+ // (rest of elements follow)
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_nested_array() {
+ unsafe {
+ let code = CString::new("[[1, 2], [3, 4]]").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_ARRAY);
+ let len = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(len, 2);
+ // First element should be an array
+ assert_eq!(data[5], TYPE_ARRAY);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_empty_record() {
+ unsafe {
+ let code = CString::new("{}").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_RECORD);
+ let field_count = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(field_count, 0);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_nested_record() {
+ unsafe {
+ let code = CString::new("{ outer = { inner = 42 } }").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_RECORD);
+ let field_count = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(field_count, 1);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_record_with_mixed_types() {
+ unsafe {
+ let code = CString::new(r#"{ name = "test", count = 42, active = true, data = null }"#).unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_RECORD);
+ let field_count = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(field_count, 4);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_computed_value() {
+ unsafe {
+ let code = CString::new("let x = 10 in let y = 20 in x + y").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_INT);
+ let value = i64::from_le_bytes(data[1..9].try_into().unwrap());
+ assert_eq!(value, 30);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_function_result() {
+ unsafe {
+ let code = CString::new("let double = fun x => x * 2 in double 21").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_INT);
+ let value = i64::from_le_bytes(data[1..9].try_into().unwrap());
+ assert_eq!(value, 42);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_array_operations() {
+ unsafe {
+ // Test array map
+ let code = CString::new("[1, 2, 3] |> std.array.map (fun x => x * 2)").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_ARRAY);
+ let len = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(len, 3);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_native_record_merge() {
+ unsafe {
+ let code = CString::new("{ a = 1 } & { b = 2 }").unwrap();
+ let buffer = nickel_eval_native(code.as_ptr());
+ assert!(!buffer.data.is_null());
+ let data = std::slice::from_raw_parts(buffer.data, buffer.len);
+ assert_eq!(data[0], TYPE_RECORD);
+ let field_count = u32::from_le_bytes(data[1..5].try_into().unwrap());
+ assert_eq!(field_count, 2);
+ nickel_free_buffer(buffer);
+ }
+ }
+
+ #[test]
+ fn test_json_all_types() {
+ // Test JSON serialization for all types
+ assert_eq!(eval_nickel_json("null").unwrap(), "null");
+ assert_eq!(eval_nickel_json("true").unwrap(), "true");
+ assert_eq!(eval_nickel_json("false").unwrap(), "false");
+ assert_eq!(eval_nickel_json("42").unwrap(), "42");
+ assert!(eval_nickel_json("3.14").unwrap().starts_with("3.14"));
+ assert_eq!(eval_nickel_json(r#""hello""#).unwrap(), "\"hello\"");
+ assert!(eval_nickel_json("[]").unwrap().contains("[]") || eval_nickel_json("[]").unwrap().contains("[\n]"));
+ }
}