NickelEval.jl

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

commit 157c16d32a7a8b1b0c4158a1c7b4dc383b711233
parent 03074280f75e2fe223bc12db75aa6f7775ef815f
Author: Erik Loualiche <[email protected]>
Date:   Fri,  6 Feb 2026 11:03:15 -0600

Add enum support and tests for nested structures

Rust FFI:
- Add Term::Enum handling for simple enums (encoded as {_tag: "Name"})
- Term::EnumVariant already handled (encoded as {_tag: "Name", _value: ...})
- 36 Rust tests passing

Julia tests:
- Add enum tests (simple, with args, with records, match expressions)
- Add deeply nested structure tests
- 110 total tests passing

Co-Authored-By: Claude Opus 4.5 <[email protected]>

Diffstat:
Mrust/nickel-jl/src/lib.rs | 60+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtest/test_ffi.jl | 44++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 103 insertions(+), 1 deletion(-)

diff --git a/rust/nickel-jl/src/lib.rs b/rust/nickel-jl/src/lib.rs @@ -205,8 +205,22 @@ fn encode_term(term: &RichTerm, buffer: &mut Vec<u8>) -> Result<(), String> { } } } + Term::Enum(tag) => { + // Simple enum without argument: encode as record with just _tag + buffer.push(TYPE_RECORD); + buffer.extend_from_slice(&1u32.to_le_bytes()); // 1 field + + // _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); + } Term::EnumVariant { tag, arg, .. } => { - // Encode enum variants as records with _tag and _value fields + // Enum variant with argument: encode as record with _tag and _value buffer.push(TYPE_RECORD); buffer.extend_from_slice(&2u32.to_le_bytes()); // 2 fields @@ -738,4 +752,48 @@ mod tests { assert_eq!(eval_nickel_json(r#""hello""#).unwrap(), "\"hello\""); assert!(eval_nickel_json("[]").unwrap().contains("[]") || eval_nickel_json("[]").unwrap().contains("[\n]")); } + + #[test] + fn test_native_simple_enum() { + unsafe { + let code = CString::new("let x = 'Foo in x").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); + // Should be a record with 1 field (_tag) + 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_enum_variant() { + unsafe { + let code = CString::new("let x = 'Some 42 in x").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); + // Should be a record with 2 fields (_tag and _value) + 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_native_enum_with_record() { + unsafe { + let code = CString::new("let x = 'Ok { value = 123 } in x").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); + } + } } diff --git a/test/test_ffi.jl b/test/test_ffi.jl @@ -82,6 +82,50 @@ result = nickel_eval_native("[1, 2, 3] |> std.array.map (fun x => x * 2)") @test result == Any[2, 4, 6] end + + @testset "Enums" begin + # Simple enum (no argument) + result = nickel_eval_native("let x = 'Foo in x") + @test result isa Dict{String, Any} + @test result["_tag"] == "Foo" + @test !haskey(result, "_value") + + # Enum with integer argument + result = nickel_eval_native("let x = 'Some 42 in x") + @test result["_tag"] == "Some" + @test result["_value"] === Int64(42) + + # Enum with record argument + result = nickel_eval_native("let x = 'Ok { value = 123 } in x") + @test result["_tag"] == "Ok" + @test result["_value"]["value"] === Int64(123) + + # Match expression + result = nickel_eval_native("let x = 'Success 42 in x |> match { 'Success v => v, 'Failure _ => 0 }") + @test result === Int64(42) + end + + @testset "Deeply nested structures" begin + # Deep nesting + result = nickel_eval_native("{ a = { b = { c = { d = 42 } } } }") + @test result["a"]["b"]["c"]["d"] === Int64(42) + + # Array of records + result = nickel_eval_native("[{ x = 1 }, { x = 2 }, { x = 3 }]") + @test length(result) == 3 + @test result[1]["x"] === Int64(1) + @test result[3]["x"] === Int64(3) + + # Records containing arrays + result = nickel_eval_native("{ items = [1, 2, 3], name = \"test\" }") + @test result["items"] == Any[1, 2, 3] + @test result["name"] == "test" + + # Mixed deep nesting + result = nickel_eval_native("{ data = [{ a = 1 }, { b = [true, false] }] }") + @test result["data"][1]["a"] === Int64(1) + @test result["data"][2]["b"] == Any[true, false] + end end @testset "FFI JSON Evaluation" begin