NickelEval.jl

Julia FFI bindings for Nickel configuration language
Log | Files | Refs | README | LICENSE

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:
M.gitignore | 1+
ACLAUDE.md | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrust/nickel-jl/Cargo.toml | 1+
Mrust/nickel-jl/src/lib.rs | 536++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
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]")); + } }