NickelEval.jl

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

CLAUDE.md (6651B)


      1 # NickelEval.jl Development Guide
      2 
      3 ## Project Overview
      4 
      5 NickelEval.jl provides Julia bindings for the [Nickel](https://nickel-lang.org/) configuration language using the official Nickel C API.
      6 
      7 ## Architecture
      8 
      9 ```
     10 NickelEval/
     11 ├── src/
     12 │   ├── NickelEval.jl    # Main module
     13 │   ├── libnickel.jl     # Generated ccall wrappers (Clang.jl from nickel_lang.h)
     14 │   └── ffi.jl           # High-level Julia API (nickel_eval, tree-walk, etc.)
     15 ├── deps/
     16 │   ├── build.jl         # Build nickel-lang from source (fallback)
     17 │   ├── generate_bindings.jl  # Clang.jl binding regeneration (dev tool)
     18 │   └── nickel_lang.h    # C header from cbindgen
     19 ├── Artifacts.toml       # Pre-built library URLs/hashes (aarch64-darwin, x86_64-linux)
     20 ├── .github/workflows/
     21 │   ├── CI.yml           # Julia tests
     22 │   ├── Documentation.yml
     23 │   └── build-ffi.yml    # Cross-platform FFI builds
     24 └── test/
     25     └── runtests.jl
     26 ```
     27 
     28 ## Key Design Decisions
     29 
     30 ### 1. Official Nickel C API
     31 
     32 NickelEval uses the official C API exposed by the `nickel-lang` crate (v2.0.0+) built with `--features capi`. This provides:
     33 - A stable, supported interface to the Nickel evaluator
     34 - Tree-walk value extraction without a custom binary protocol
     35 - No Nickel CLI dependency
     36 
     37 ### 2. Types via C API Tree-Walk
     38 
     39 The C API is walked recursively on the Julia side using `ccall`. Values are converted to Julia native types:
     40 - Records → `Dict{String, Any}`
     41 - Arrays → `Vector{Any}`
     42 - Integers → `Int64`, Floats → `Float64`
     43 - Enums → `NickelEnum(tag, arg)`
     44 
     45 ### 3. Avoid `unwrap()` in Rust
     46 
     47 Use proper error handling in any Rust glue code:
     48 ```rust
     49 // Bad
     50 let f = value.to_f64().unwrap();
     51 
     52 // Good
     53 let f = f64::try_from(value).map_err(|e| format!("Error: {:?}", e))?;
     54 ```
     55 
     56 ## Building
     57 
     58 ### Nickel C API Library
     59 
     60 ```bash
     61 cd rust/nickel-lang
     62 cargo build -p nickel-lang --features capi --release
     63 cp target/release/libnickel_lang.dylib ../../deps/  # macOS
     64 # or libnickel_lang.so on Linux
     65 ```
     66 
     67 ### Running Tests
     68 
     69 ```bash
     70 # Julia tests
     71 julia --project=. -e 'using Pkg; Pkg.test()'
     72 ```
     73 
     74 ## Release Process
     75 
     76 **Before tagging a new version, ALL CI workflows must pass:**
     77 
     78 1. Run tests locally: `julia --project=. -e 'using Pkg; Pkg.test()'`
     79 2. Push changes to main
     80 3. Wait for CI to complete and verify all workflows pass (both CI and Documentation)
     81 4. Only then tag and register the new version
     82 
     83 ```bash
     84 # Check CI status before tagging
     85 gh run list --repo LouLouLibs/NickelEval.jl --limit 5
     86 
     87 # All workflows should show "success" before proceeding with:
     88 git tag -a vX.Y.Z -m "vX.Y.Z: Description"
     89 git push origin vX.Y.Z
     90 ```
     91 
     92 ### Version Bumping Checklist
     93 
     94 1. Update `version` in `Project.toml`
     95 2. Update `## Current Version` in `TODO.md`
     96 3. Commit these changes
     97 4. Wait for CI to pass
     98 5. Tag the release
     99 6. If FFI changed, build and upload new artifacts (see FFI Artifact Release below)
    100 7. Update loulouJL registry with correct tree SHA
    101 
    102 ### FFI Artifact Release
    103 
    104 When the C API library changes, new pre-built binaries must be released:
    105 
    106 1. **Trigger the build workflow:**
    107    ```bash
    108    gh workflow run build-ffi.yml --ref main
    109    ```
    110 
    111 2. **Download built artifacts** from the workflow run (2 platforms: aarch64-darwin, x86_64-linux)
    112 
    113 3. **Create GitHub Release** and upload the `.tar.gz` files
    114 
    115 4. **Calculate tree hashes** for each artifact:
    116    ```bash
    117    # For each tarball:
    118    tar -xzf libnickel_lang-PLATFORM.tar.gz
    119    julia -e 'using Pkg; println(Pkg.GitTools.tree_hash("."))'
    120    ```
    121 
    122 5. **Update Artifacts.toml** with new SHA256 checksums and tree hashes
    123 
    124 ### Artifacts.toml Format
    125 
    126 Use the `[[artifact_name]]` array format with platform properties:
    127 
    128 ```toml
    129 [[libnickel_lang]]
    130 arch = "aarch64"
    131 git-tree-sha1 = "TREE_HASH_HERE"
    132 os = "macos"
    133 lazy = true
    134 
    135     [[libnickel_lang.download]]
    136     url = "https://github.com/LouLouLibs/NickelEval.jl/releases/download/vX.Y.Z/libnickel_lang-aarch64-apple-darwin.tar.gz"
    137     sha256 = "SHA256_HASH_HERE"
    138 ```
    139 
    140 Platform values:
    141 - `os`: "macos", "linux"
    142 - `arch`: "aarch64", "x86_64"
    143 
    144 ### Documentation Requirements
    145 
    146 Any new exported function must be added to `docs/src/lib/public.md` in the appropriate section to avoid documentation build failures.
    147 
    148 ### Registry (loulouJL)
    149 
    150 Location: `/Users/loulou/Dropbox/projects_code/julia_packages/loulouJL/N/NickelEval/`
    151 
    152 **Files to update:**
    153 
    154 1. **Versions.toml** - Add new version entry:
    155    ```toml
    156    ["X.Y.Z"]
    157    git-tree-sha1 = "TREE_SHA_HERE"
    158    ```
    159    Get tree SHA: `git rev-parse vX.Y.Z^{tree}`
    160 
    161 2. **Deps.toml** - If dependencies changed, add new version range:
    162    ```toml
    163    ["X.Y-0"]
    164    Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
    165    LazyArtifacts = "4af54fe1-eca0-43a8-85a7-787d91b784e3"
    166    ```
    167    **Important:** Version ranges must not overlap. Use `"X.Y-Z.W"` for ranges.
    168 
    169 3. **Compat.toml** - If compat bounds changed, update accordingly.
    170 
    171 **Registry format rules:**
    172 - Section headers must be quoted: `["0.5"]` not `[0.5]`
    173 - Version ranges: `"0.4-0.5"` (from 0.4 to 0.5), `"0.5-0"` (from 0.5 to end of 0.x)
    174 - No spaces in ranges
    175 - Ranges must not overlap for the same dependency
    176 
    177 ## API Functions
    178 
    179 ### Evaluation
    180 
    181 - `nickel_eval(code)` - Evaluate Nickel code, returns Julia-native types
    182 - `nickel_eval(code, T)` - Evaluate and convert to type `T`
    183 - `nickel_eval_file(path)` - Evaluate a `.ncl` file (supports imports)
    184 - `check_ffi_available()` - Check if the C API library is loaded
    185 
    186 ### Export
    187 
    188 - `nickel_to_json(code)` - Export to JSON string
    189 - `nickel_to_toml(code)` - Export to TOML string
    190 - `nickel_to_yaml(code)` - Export to YAML string
    191 
    192 ### String Macro
    193 
    194 - `ncl"..."` / `@ncl_str` - Evaluate inline Nickel code
    195 
    196 ## Type Conversion
    197 
    198 | Nickel Type | Julia Type |
    199 |-------------|------------|
    200 | Null | `nothing` |
    201 | Bool | `Bool` |
    202 | Number (integer) | `Int64` |
    203 | Number (float) | `Float64` |
    204 | String | `String` |
    205 | Array | `Vector{Any}` |
    206 | Record | `Dict{String, Any}` |
    207 | Enum | `NickelEnum(tag, arg)` |
    208 
    209 ## Nickel Language Reference
    210 
    211 Common patterns used in tests:
    212 
    213 ```nickel
    214 # Let bindings
    215 let x = 1 in x + 2
    216 
    217 # Functions
    218 let double = fun x => x * 2 in double 21
    219 
    220 # Records
    221 { name = "test", value = 42 }
    222 
    223 # Record merge
    224 { a = 1 } & { b = 2 }
    225 
    226 # Arrays
    227 [1, 2, 3]
    228 
    229 # Array operations
    230 [1, 2, 3] |> std.array.map (fun x => x * 2)
    231 
    232 # Nested structures
    233 { outer = { inner = 42 } }
    234 ```
    235 
    236 ## Dependencies
    237 
    238 ### Julia
    239 - Artifacts (stdlib)
    240 - LazyArtifacts (stdlib)
    241 
    242 ### Rust (for building C API library locally)
    243 - nickel-lang = "2.0.0" with `--features capi`
    244 
    245 ## Future Improvements
    246 
    247 1. Support for Nickel contracts/types in Julia
    248 2. Streaming evaluation for large configs
    249 3. REPL integration