NickelEval.jl

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

commit 50af2fdc9689846ef1f28348cb088c8b6e4a8b9c
parent 9444b313180cbd90431b3f41bb865accca7fbf01
Author: Erik Loualiche <[email protected]>
Date:   Fri,  6 Feb 2026 16:57:52 -0600

Add comprehensive enum documentation and tests

Documentation:
- Simple enums (no argument)
- Enums with primitive arguments (int, float, string, bool)
- Enums with record arguments
- Enums with array arguments
- Nested enums (enums containing enums)
- Pattern matching examples
- Real-world patterns (Result, Option, state machine)

Tests:
- 9 new enum test sets covering all scenarios
- 167 total tests passing

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

Diffstat:
Mdocs/src/man/ffi.md | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mtest/test_ffi.jl | 226++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 387 insertions(+), 31 deletions(-)

diff --git a/docs/src/man/ffi.md b/docs/src/man/ffi.md @@ -54,44 +54,198 @@ nickel_eval_ffi("{ a = 1 }", Dict{String, Int}) # Typed Dict ### Enums -Nickel enums are converted to the `NickelEnum` type, preserving enum semantics: +Nickel enums (also called "enum tags" or "variants") are converted to the `NickelEnum` type, preserving enum semantics distinct from regular records. + +#### The NickelEnum Type ```julia struct NickelEnum - tag::Symbol - arg::Any # nothing for simple enums + tag::Symbol # The variant name as a Julia Symbol + arg::Any # The argument (nothing for simple enums) end ``` -**Simple enum** (no argument): -```julia -result = nickel_eval_native("let x = 'Foo in x") -# => NickelEnum(:Foo, nothing) +#### Simple Enums (No Argument) + +Simple enums are tags without associated data, commonly used for status flags or options: -result.tag # => :Foo +```julia +# Boolean-like enums +result = nickel_eval_native("let x = 'True in x") +result.tag # => :True result.arg # => nothing -result == :Foo # => true (convenience comparison) + +# Status enums +result = nickel_eval_native("let status = 'Pending in status") +result == :Pending # => true (convenience comparison) + +# Multiple variants +code = \"\"\" +let color = 'Red in color +\"\"\" +result = nickel_eval_native(code) +result.tag # => :Red +``` + +#### Enums with Primitive Arguments + +Enums can carry a single value of any type: + +```julia +# Integer argument +result = nickel_eval_native("let x = 'Count 42 in x") +result.tag # => :Count +result.arg # => 42 (Int64) + +# String argument +result = nickel_eval_native("let x = 'Message \"hello\" in x") +result.tag # => :Message +result.arg # => "hello" + +# Float argument +result = nickel_eval_native("let x = 'Temperature 98.6 in x") +result.arg # => 98.6 (Float64) + +# Boolean argument +result = nickel_eval_native("let x = 'Flag true in x") +result.arg # => true ``` -**Enum with argument**: +#### Enums with Record Arguments + +Enums can carry complex record data: + ```julia +# Record argument +code = \"\"\" +let result = 'Ok { value = 123, message = "success" } in result +\"\"\" +result = nickel_eval_native(code) +result.tag # => :Ok +result.arg # => Dict{String, Any} +result.arg["value"] # => 123 +result.arg["message"] # => "success" + +# Error with details +code = \"\"\" +let err = 'Error { code = 404, reason = "not found" } in err +\"\"\" +result = nickel_eval_native(code) +result.tag # => :Error +result.arg["code"] # => 404 +result.arg["reason"] # => "not found" +``` + +#### Enums with Array Arguments + +```julia +# Array argument +code = \"\"\" +let batch = 'Batch [1, 2, 3, 4, 5] in batch +\"\"\" +result = nickel_eval_native(code) +result.tag # => :Batch +result.arg # => Any[1, 2, 3, 4, 5] +``` + +#### Nested Enums + +Enums can contain other enums: + +```julia +# Nested enum in record +code = \"\"\" +let outer = 'Container { inner = 'Value 42 } in outer +\"\"\" +result = nickel_eval_native(code) +result.tag # => :Container +result.arg["inner"].tag # => :Value +result.arg["inner"].arg # => 42 + +# Enum in array +code = \"\"\" +let items = 'List ['Some 1, 'None, 'Some 3] in items +\"\"\" +result = nickel_eval_native(code) +result.tag # => :List +result.arg[1].tag # => :Some +result.arg[1].arg # => 1 +result.arg[2].tag # => :None +result.arg[2].arg # => nothing +``` + +#### Pattern Matching with Nickel + +When Nickel's `match` expression resolves an enum, you get the matched value: + +```julia +# Match resolves to the extracted value +code = \"\"\" +let x = 'Some 42 in +x |> match { + 'Some v => v, + 'None => 0 +} +\"\"\" +result = nickel_eval_native(code) +# => 42 (the matched value, not an enum) + +# Match with record destructuring +code = \"\"\" +let result = 'Ok { value = 100 } in +result |> match { + 'Ok r => r.value, + 'Error _ => -1 +} +\"\"\" +result = nickel_eval_native(code) +# => 100 +``` + +#### Working with NickelEnum in Julia + +```julia +# Type checking result = nickel_eval_native("let x = 'Some 42 in x") -# => NickelEnum(:Some, 42) +result isa NickelEnum # => true -result.tag # => :Some -result.arg # => 42 +# Symbol comparison (both directions work) +result == :Some # => true +:Some == result # => true -result = nickel_eval_native("let x = 'Ok { value = 123 } in x") -result.arg["value"] # => 123 +# Accessing fields +result.tag # => :Some +result.arg # => 42 + +# Pretty printing +repr(result) # => "'Some 42" + +# Simple enum printing +repr(nickel_eval_native("let x = 'None in x")) # => "'None" ``` -**Pretty printing**: +#### Real-World Example: Result Type + ```julia -repr(nickel_eval_native("let x = 'None in x")) # => "'None" -repr(nickel_eval_native("let x = 'Some 42 in x")) # => "'Some 42" +# Simulating Rust-like Result type +code = \"\"\" +let divide = fun a b => + if b == 0 then + 'Err "division by zero" + else + 'Ok (a / b) +in +divide 10 2 +\"\"\" +result = nickel_eval_native(code) +if result == :Ok + println("Result: ", result.arg) # => 5 +else + println("Error: ", result.arg) +end ``` -This mirrors Nickel's `std.enum.to_tag_and_arg` semantics while using a proper Julia type. +This representation mirrors Nickel's `std.enum.to_tag_and_arg` semantics while providing a proper Julia type that preserves enum identity. ### Nested Structures diff --git a/test/test_ffi.jl b/test/test_ffi.jl @@ -83,34 +83,236 @@ @test result == Any[2, 4, 6] end - @testset "Enums" begin - # Simple enum (no argument) + @testset "Enums - Simple (no argument)" begin + # Basic simple enum result = nickel_eval_native("let x = 'Foo in x") @test result isa NickelEnum @test result.tag == :Foo @test result.arg === nothing - @test result == :Foo # convenience comparison - # Enum with integer argument - result = nickel_eval_native("let x = 'Some 42 in x") - @test result isa NickelEnum - @test result.tag == :Some + # Convenience comparison + @test result == :Foo + @test :Foo == result + @test result != :Bar + + # Various tag names + @test nickel_eval_native("let x = 'None in x").tag == :None + @test nickel_eval_native("let x = 'True in x").tag == :True + @test nickel_eval_native("let x = 'False in x").tag == :False + @test nickel_eval_native("let x = 'Pending in x").tag == :Pending + @test nickel_eval_native("let x = 'Red in x").tag == :Red + end + + @testset "Enums - With primitive arguments" begin + # Integer argument + result = nickel_eval_native("let x = 'Count 42 in x") + @test result.tag == :Count @test result.arg === Int64(42) - @test result == :Some - # Enum with record argument + # Negative integer (needs parentheses in Nickel) + result = nickel_eval_native("let x = 'Offset (-100) in x") + @test result.arg === Int64(-100) + + # Float argument + result = nickel_eval_native("let x = 'Temperature 98.6 in x") + @test result.tag == :Temperature + @test result.arg ≈ 98.6 + + # String argument + result = nickel_eval_native("let x = 'Message \"hello world\" in x") + @test result.tag == :Message + @test result.arg == "hello world" + + # Empty string argument + result = nickel_eval_native("let x = 'Empty \"\" in x") + @test result.arg == "" + + # Boolean arguments + result = nickel_eval_native("let x = 'Flag true in x") + @test result.arg === true + result = nickel_eval_native("let x = 'Flag false in x") + @test result.arg === false + + # Null argument + result = nickel_eval_native("let x = 'Nullable null in x") + @test result.arg === nothing + end + + @testset "Enums - With record arguments" begin + # Simple record argument result = nickel_eval_native("let x = 'Ok { value = 123 } in x") @test result.tag == :Ok @test result.arg isa Dict{String, Any} @test result.arg["value"] === Int64(123) - # Match expression (returns the matched value, not an enum) - result = nickel_eval_native("let x = 'Success 42 in x |> match { 'Success v => v, 'Failure _ => 0 }") + # Record with multiple fields + code = """ + let result = 'Ok { value = 123, message = "success" } in result + """ + result = nickel_eval_native(code) + @test result.arg["value"] === Int64(123) + @test result.arg["message"] == "success" + + # Error with details + code = """ + let err = 'Error { code = 404, reason = "not found" } in err + """ + result = nickel_eval_native(code) + @test result.tag == :Error + @test result.arg["code"] === Int64(404) + @test result.arg["reason"] == "not found" + + # Nested record in enum + code = """ + let x = 'Data { outer = { inner = 42 } } in x + """ + result = nickel_eval_native(code) + @test result.arg["outer"]["inner"] === Int64(42) + end + + @testset "Enums - With array arguments" begin + # Array of integers + result = nickel_eval_native("let x = 'Batch [1, 2, 3, 4, 5] in x") + @test result.tag == :Batch + @test result.arg == Any[1, 2, 3, 4, 5] + + # Empty array + result = nickel_eval_native("let x = 'Empty [] in x") + @test result.arg == Any[] + + # Array of strings + result = nickel_eval_native("let x = 'Names [\"alice\", \"bob\"] in x") + @test result.arg == Any["alice", "bob"] + + # Array of records + code = """ + let x = 'Users [{ name = "alice" }, { name = "bob" }] in x + """ + result = nickel_eval_native(code) + @test result.arg[1]["name"] == "alice" + @test result.arg[2]["name"] == "bob" + end + + @testset "Enums - Nested enums" begin + # Enum inside record inside enum + code = """ + let outer = 'Container { inner = 'Value 42 } in outer + """ + result = nickel_eval_native(code) + @test result.tag == :Container + @test result.arg["inner"] isa NickelEnum + @test result.arg["inner"].tag == :Value + @test result.arg["inner"].arg === Int64(42) + + # Array of enums inside enum + code = """ + let items = 'List ['Some 1, 'None, 'Some 3] in items + """ + result = nickel_eval_native(code) + @test result.tag == :List + @test length(result.arg) == 3 + @test result.arg[1].tag == :Some + @test result.arg[1].arg === Int64(1) + @test result.arg[2].tag == :None + @test result.arg[2].arg === nothing + @test result.arg[3].tag == :Some + @test result.arg[3].arg === Int64(3) + + # Deeply nested enums + code = """ + let x = 'L1 { a = 'L2 { b = 'L3 42 } } in x + """ + result = nickel_eval_native(code) + @test result.arg["a"].arg["b"].arg === Int64(42) + end + + @testset "Enums - Pattern matching" begin + # Match resolves to extracted value + code = """ + let x = 'Some 42 in + x |> match { + 'Some v => v, + 'None => 0 + } + """ + result = nickel_eval_native(code) @test result === Int64(42) - # Pretty printing + # Match with record destructuring + code = """ + let result = 'Ok { value = 100 } in + result |> match { + 'Ok r => r.value, + 'Error _ => -1 + } + """ + result = nickel_eval_native(code) + @test result === Int64(100) + + # Match returning enum + code = """ + let x = 'Some 42 in + x |> match { + 'Some v => 'Doubled (v * 2), + 'None => 'Zero 0 + } + """ + result = nickel_eval_native(code) + @test result.tag == :Doubled + @test result.arg === Int64(84) + end + + @testset "Enums - Pretty printing" begin + # Simple enum @test repr(nickel_eval_native("let x = 'None in x")) == "'None" + @test repr(nickel_eval_native("let x = 'Foo in x")) == "'Foo" + + # Enum with simple argument @test repr(nickel_eval_native("let x = 'Some 42 in x")) == "'Some 42" + + # Enum with string argument + result = nickel_eval_native("let x = 'Msg \"hi\" in x") + @test startswith(repr(result), "'Msg") + end + + @testset "Enums - Real-world patterns" begin + # Result type pattern + code = """ + let divide = fun a b => + if b == 0 then + 'Err "division by zero" + else + 'Ok (a / b) + in + divide 10 2 + """ + result = nickel_eval_native(code) + @test result == :Ok + @test result.arg === Int64(5) + + # Option type pattern + code = """ + let find = fun arr pred => + let matches = std.array.filter pred arr in + if std.array.length matches == 0 then + 'None + else + 'Some (std.array.first matches) + in + find [1, 2, 3, 4] (fun x => x > 2) + """ + result = nickel_eval_native(code) + @test result == :Some + @test result.arg === Int64(3) + + # State machine pattern + code = """ + let state = 'Running { progress = 75, task = "downloading" } in state + """ + result = nickel_eval_native(code) + @test result.tag == :Running + @test result.arg["progress"] === Int64(75) + @test result.arg["task"] == "downloading" end @testset "Deeply nested structures" begin