commit 157c16d32a7a8b1b0c4158a1c7b4dc383b711233
parent 03074280f75e2fe223bc12db75aa6f7775ef815f
Author: Erik Loualiche <eloualic@umn.edu>
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 <noreply@anthropic.com>
Diffstat:
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