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:
| M | docs/src/man/ffi.md | | | 192 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- |
| M | test/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