NickelEval.jl

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

commit 3338d91571992a4f6b87c17e0bd33a23d9ae36c8
parent 45ac6a4201d3e18fb659e25c5123c58a3db7f513
Author: Erik Loualiche <[email protected]>
Date:   Sat, 21 Mar 2026 21:14:35 -0400

Merge pull request #15 from LouLouLibs/docs/restructure-pages

docs: restructure into installation, examples, and API reference
Diffstat:
Mdocs/make.jl | 9+++------
Mdocs/src/index.md | 79+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mdocs/src/lib/public.md | 1+
Adocs/src/man/detailed.md | 279+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/src/man/examples.md | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddocs/src/man/quickstart.md | 112-------------------------------------------------------------------------------
6 files changed, 453 insertions(+), 146 deletions(-)

diff --git a/docs/make.jl b/docs/make.jl @@ -15,12 +15,9 @@ makedocs( authors = "LouLouLibs Contributors", pages = [ "Home" => "index.md", - "Manual" => [ - "man/quickstart.md", - ], - "Library" => [ - "lib/public.md", - ] + "Quick Examples" => "man/examples.md", + "Detailed Examples" => "man/detailed.md", + "API Reference" => "lib/public.md", ] ) diff --git a/docs/src/index.md b/docs/src/index.md @@ -1,17 +1,31 @@ # NickelEval.jl -Julia bindings for the [Nickel](https://nickel-lang.org/) configuration language. +Julia bindings for the [Nickel](https://nickel-lang.org/) configuration language, using the official Nickel C API. + +Evaluate Nickel code directly from Julia and get back native Julia types — no CLI, no intermediate files, no serialization overhead. ## Features -- **Evaluate Nickel code** directly from Julia -- **Native type conversion** to Julia types (`Dict`, `NamedTuple`, custom structs) -- **Export to multiple formats** (JSON, TOML, YAML) -- **High-performance C API** using the official Nickel C API — no CLI needed +- **Direct evaluation** of Nickel expressions and files via the C API +- **Native type mapping** — records become `Dict`, arrays become `Vector`, enums become `NickelEnum` +- **Typed evaluation** — request results as `Dict{String,Int}`, `Vector{Float64}`, `NamedTuple`, etc. +- **Export to JSON, TOML, YAML** — serialize Nickel configurations to standard formats +- **File evaluation with imports** — evaluate `.ncl` files that reference other Nickel files + +## Documentation + +```@contents +Pages = [ + "man/examples.md", + "man/detailed.md", + "lib/public.md", +] +Depth = 1 +``` ## Installation -### From LouLouLibs Registry (Recommended) +### From the LouLouLibs Registry ```julia using Pkg @@ -19,44 +33,53 @@ Pkg.Registry.add(url="https://github.com/LouLouLibs/loulouJL") Pkg.add("NickelEval") ``` -### From GitHub URL +### From GitHub ```julia using Pkg Pkg.add(url="https://github.com/LouLouLibs/NickelEval.jl") ``` -No external tools are required. The Nickel evaluator is bundled as a pre-built native library. +Pre-built native libraries are provided for **macOS (Apple Silicon)** and **Linux (x86\_64)**. On supported platforms, the library downloads automatically when first needed. -## Quick Example +### Building from Source + +If the pre-built binary doesn't work on your system — or if no binary is available for your platform — you can build the Nickel C API library from source. This requires [Rust](https://rustup.rs/). ```julia using NickelEval +build_ffi() +``` -# Simple evaluation -nickel_eval("1 + 2") # => 3 +This clones the Nickel repository, compiles the C API library with `cargo`, and installs it into the package's `deps/` directory. The FFI is re-initialized automatically — no Julia restart needed. -# Records return Dict{String, Any} -config = nickel_eval("{ host = \"localhost\", port = 8080 }") -config["host"] # => "localhost" -config["port"] # => 8080 +You can also trigger the build during package installation: + +```julia +ENV["NICKELEVAL_BUILD_FFI"] = "true" +using Pkg +Pkg.build("NickelEval") +``` -# Typed evaluation -nickel_eval("{ x = 1, y = 2 }", Dict{String, Int}) -# => Dict{String, Int64}("x" => 1, "y" => 2) +### Older Linux Systems (glibc Compatibility) -# Export to TOML -nickel_to_toml("{ name = \"myapp\", version = \"1.0\" }") -# => "name = \"myapp\"\nversion = \"1.0\"\n" +The pre-built Linux binary is compiled against a relatively recent version of glibc. On older distributions — CentOS 7, older Ubuntu LTS, or many HPC clusters — you may see an error like: + +``` +/lib64/libm.so.6: version `GLIBC_2.29' not found ``` -## Why Nickel? +The fix is to build from source: -[Nickel](https://nickel-lang.org/) is a configuration language designed to be: +```julia +using NickelEval +build_ffi() +``` + +This compiles Nickel against your system's glibc, producing a compatible binary. The only requirement is a working Rust toolchain (`cargo`), which can be installed without root access via [rustup](https://rustup.rs/): -- **Programmable**: Functions, let bindings, and standard library -- **Typed**: Optional contracts for validation -- **Mergeable**: Combine configurations with `&` -- **Safe**: No side effects, pure functional +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` -NickelEval.jl lets you leverage Nickel's power directly in your Julia workflows. +After installing Rust, restart your Julia session and run `build_ffi()`. diff --git a/docs/src/lib/public.md b/docs/src/lib/public.md @@ -19,6 +19,7 @@ nickel_to_yaml ```@docs check_ffi_available +build_ffi ``` ## String Macro diff --git a/docs/src/man/detailed.md b/docs/src/man/detailed.md @@ -0,0 +1,279 @@ +# Detailed Examples + +## Type Conversions + +### Nickel to Julia Type Mapping + +| Nickel Type | Julia Type | +|:------------|:-----------| +| Null | `nothing` | +| Bool | `Bool` | +| Number (integer) | `Int64` | +| Number (float) | `Float64` | +| String | `String` | +| Array | `Vector{Any}` | +| Record | `Dict{String, Any}` | +| Enum (tag only) | `NickelEnum(tag, nothing)` | +| Enum (with argument) | `NickelEnum(tag, arg)` | + +### Typed Evaluation + +Pass a type as the second argument to `nickel_eval` to convert the result: + +```julia +nickel_eval("42", Int) # => 42::Int64 +nickel_eval("3.14", Float64) # => 3.14::Float64 +nickel_eval("\"hello\"", String) # => "hello"::String +``` + +#### Typed Dicts + +```julia +nickel_eval("{ a = 1, b = 2 }", Dict{String, Int}) +# => Dict{String, Int64}("a" => 1, "b" => 2) + +nickel_eval("{ x = 1.5, y = 2.5 }", Dict{Symbol, Float64}) +# => Dict{Symbol, Float64}(:x => 1.5, :y => 2.5) +``` + +#### Typed Vectors + +```julia +nickel_eval("[1, 2, 3]", Vector{Int}) +# => [1, 2, 3]::Vector{Int64} + +nickel_eval("[\"a\", \"b\", \"c\"]", Vector{String}) +# => ["a", "b", "c"]::Vector{String} +``` + +#### NamedTuples + +Records can be converted to `NamedTuple` for convenient field access: + +```julia +server = nickel_eval( + "{ host = \"localhost\", port = 8080 }", + @NamedTuple{host::String, port::Int} +) +server.host # => "localhost" +server.port # => 8080 +``` + +## Enums — `NickelEnum` + +Nickel enums are represented as `NickelEnum`, a struct with two fields: +- `tag::Symbol` — the variant name +- `arg::Any` — the payload (`nothing` for bare tags) + +### Simple Enum Tags + +```julia +result = nickel_eval("let x = 'Foo in x") +result.tag # => :Foo +result.arg # => nothing +``` + +`NickelEnum` supports direct comparison with `Symbol`: + +```julia +result == :Foo # => true +result == :Bar # => false +``` + +### Enums with Arguments + +Enum variants can carry a value of any type: + +```julia +# Integer payload +result = nickel_eval("let x = 'Some 42 in x") +result.tag # => :Some +result.arg # => 42 + +# String payload +result = nickel_eval("let x = 'Message \"hello\" in x") +result.tag # => :Message +result.arg # => "hello" + +# Record payload +result = nickel_eval("let x = 'Ok { value = 123, status = \"done\" } in x") +result.tag # => :Ok +result.arg["value"] # => 123 +result.arg["status"] # => "done" + +# Array payload +result = nickel_eval("let x = 'Batch [1, 2, 3] in x") +result.arg # => Any[1, 2, 3] +``` + +### Nested Enums + +Enums can appear inside records, arrays, or other enums: + +```julia +# Enum inside a record +result = nickel_eval("{ status = 'Active, result = 'Ok 42 }") +result["status"] == :Active # => true +result["result"].arg # => 42 + +# Enum inside another enum +result = nickel_eval("let x = 'Container { inner = 'Value 42 } in x") +result.tag # => :Container +result.arg["inner"].tag # => :Value +result.arg["inner"].arg # => 42 + +# Array of enums +result = nickel_eval("let x = 'List ['Some 1, 'None, 'Some 3] in x") +result.arg[1].tag # => :Some +result.arg[1].arg # => 1 +result.arg[2] == :None # => true +``` + +### Pattern Matching + +Nickel's `match` resolves before reaching Julia — you get back the matched value: + +```julia +result = nickel_eval(""" +let x = 'Some 42 in +x |> match { + 'Some v => v, + 'None => 0 +} +""") +# => 42 + +result = nickel_eval(""" +let x = 'Some 42 in +x |> match { + 'Some v => 'Doubled (v * 2), + 'None => 'Zero 0 +} +""") +result.tag # => :Doubled +result.arg # => 84 +``` + +### Pretty Printing + +`NickelEnum` displays in Nickel's own syntax: + +```julia +repr(nickel_eval("let x = 'None in x")) # => "'None" +repr(nickel_eval("let x = 'Some 42 in x")) # => "'Some 42" +repr(nickel_eval("let x = 'Msg \"hi\" in x")) # => "'Msg \"hi\"" +``` + +### Real-World Patterns + +#### Result Type + +```julia +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(code) +result == :Ok # => true +result.arg # => 5 +``` + +#### Option Type + +```julia +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(code) +result == :Some # => true +result.arg # => 3 +``` + +#### State Machine + +```julia +result = nickel_eval(""" +let state = 'Running { progress = 75, task = "downloading" } in state +""") +result.tag # => :Running +result.arg["progress"] # => 75 +result.arg["task"] # => "downloading" +``` + +## Export Formats + +Evaluate Nickel code and serialize to JSON, TOML, or YAML: + +```julia +nickel_to_json("{ name = \"myapp\", version = \"1.0\" }") +# => "{\n \"name\": \"myapp\",\n \"version\": \"1.0\"\n}" + +nickel_to_yaml("{ name = \"myapp\", version = \"1.0\" }") +# => "name: myapp\nversion: '1.0'\n" + +nickel_to_toml("{ name = \"myapp\", version = \"1.0\" }") +# => "name = \"myapp\"\nversion = \"1.0\"\n" +``` + +## File Evaluation with Imports + +Nickel files can import other Nickel files. `nickel_eval_file` resolves imports relative to the file's directory. + +Given these files: + +```nickel +# shared.ncl +{ + project_name = "MyProject", + version = "1.0.0" +} +``` + +```nickel +# config.ncl +let shared = import "shared.ncl" in +{ + name = shared.project_name, + version = shared.version, + debug = true +} +``` + +```julia +config = nickel_eval_file("config.ncl") +config["name"] # => "MyProject" +config["version"] # => "1.0.0" +config["debug"] # => true +``` + +Subdirectory imports also work: + +```nickel +# lib/utils.ncl +{ + helper = fun x => x * 2 +} +``` + +```nickel +# main.ncl +let utils = import "lib/utils.ncl" in +{ result = utils.helper 21 } +``` + +```julia +nickel_eval_file("main.ncl") +# => Dict("result" => 42) +``` diff --git a/docs/src/man/examples.md b/docs/src/man/examples.md @@ -0,0 +1,119 @@ +# Quick Examples + +## Evaluating Expressions + +```julia +using NickelEval + +nickel_eval("1 + 2") # => 3 +nickel_eval("true") # => true +nickel_eval("\"hello\"") # => "hello" +nickel_eval("null") # => nothing +``` + +Integers return `Int64`, decimals return `Float64`: + +```julia +nickel_eval("42") # => 42 (Int64) +nickel_eval("3.14") # => 3.14 (Float64) +``` + +## Records + +Nickel records map to `Dict{String, Any}`: + +```julia +config = nickel_eval(""" +{ + host = "localhost", + port = 8080, + debug = true +} +""") + +config["host"] # => "localhost" +config["port"] # => 8080 +config["debug"] # => true +``` + +Nested records work as expected: + +```julia +result = nickel_eval("{ database = { host = \"localhost\", port = 5432 } }") +result["database"]["host"] # => "localhost" +``` + +## Arrays + +Nickel arrays map to `Vector{Any}`: + +```julia +nickel_eval("[1, 2, 3]") # => Any[1, 2, 3] +nickel_eval("[\"a\", \"b\", \"c\"]") # => Any["a", "b", "c"] +``` + +Use the Nickel standard library for transformations: + +```julia +nickel_eval("[1, 2, 3] |> std.array.map (fun x => x * 2)") +# => Any[2, 4, 6] +``` + +## Let Bindings and Functions + +```julia +nickel_eval("let x = 10 in x * 2") # => 20 + +nickel_eval(""" +let double = fun x => x * 2 in +double 21 +""") # => 42 + +nickel_eval(""" +let add = fun x y => x + y in +add 3 4 +""") # => 7 +``` + +## Record Merge + +Nickel's `&` operator merges records: + +```julia +nickel_eval("{ a = 1 } & { b = 2 }") +# => Dict("a" => 1, "b" => 2) +``` + +## String Macro + +The `ncl"..."` macro provides a shorthand for `nickel_eval`: + +```julia +ncl"1 + 1" # => 2 +ncl"{ x = 10 }"["x"] # => 10 +ncl"[1, 2, 3]" # => Any[1, 2, 3] +``` + +## Evaluating Files + +Evaluate `.ncl` files directly. Imports are resolved relative to the file's directory: + +```julia +# config.ncl contains: { host = "localhost", port = 8080 } +config = nickel_eval_file("config.ncl") +config["host"] # => "localhost" +``` + +## Error Handling + +Invalid Nickel code throws a `NickelError`: + +```julia +try + nickel_eval("{ x = }") +catch e + if e isa NickelError + println(e.message) # Nickel's formatted error message + end +end +``` diff --git a/docs/src/man/quickstart.md b/docs/src/man/quickstart.md @@ -1,112 +0,0 @@ -# Quick Start - -## Installation - -### From LouLouLibs Registry (Recommended) - -```julia -using Pkg -Pkg.Registry.add(url="https://github.com/LouLouLibs/loulouJL") -Pkg.add("NickelEval") -``` - -### From GitHub URL - -```julia -using Pkg -Pkg.add(url="https://github.com/LouLouLibs/NickelEval.jl") -``` - -No external tools or CLI are required. NickelEval uses the official Nickel C API, -bundled as a pre-built native library for your platform. - -## Basic Usage - -```julia -using NickelEval - -# Evaluate simple expressions -nickel_eval("1 + 2") # => 3 -nickel_eval("true") # => true -nickel_eval("\"hello\"") # => "hello" -``` - -## Working with Records - -Nickel records become `Dict{String, Any}`: - -```julia -config = nickel_eval(""" -{ - database = { - host = "localhost", - port = 5432 - }, - debug = true -} -""") - -config["database"]["host"] # => "localhost" -config["database"]["port"] # => 5432 -config["debug"] # => true -``` - -## Let Bindings and Functions - -```julia -# Let bindings -nickel_eval("let x = 10 in x * 2") # => 20 - -# Functions -nickel_eval(""" -let double = fun x => x * 2 in -double 21 -""") # => 42 -``` - -## Arrays - -```julia -nickel_eval("[1, 2, 3]") # => [1, 2, 3] - -# Array operations with std library -nickel_eval("[1, 2, 3] |> std.array.map (fun x => x * 2)") -# => [2, 4, 6] -``` - -## Record Merge - -```julia -nickel_eval("{ a = 1 } & { b = 2 }") -# => Dict{String, Any}("a" => 1, "b" => 2) -``` - -## String Macro - -For inline Nickel code: - -```julia -ncl"1 + 1" # => 2 - -config = ncl"{ host = \"localhost\" }" -config["host"] # => "localhost" -``` - -## File Evaluation - -```julia -# Evaluate a .ncl file -config = nickel_eval_file("config.ncl") -``` - -## Error Handling - -```julia -try - nickel_eval("{ x = }") # syntax error -catch e - if e isa NickelError - println("Error: ", e.message) - end -end -```