NickelEval.jl

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

type-generation-feasibility.md (4905B)


      1 # Feasibility: Generating Julia Types from Nickel Contracts
      2 
      3 **Status:** Speculative / not planned
      4 **Date:** 2026-03-17
      5 
      6 ## Motivation
      7 
      8 Nickel has a rich type system including enum types (`[| 'active, 'inactive |]`), record contracts, and algebraic data types. It would be valuable to use Nickel as a schema language — define types in `.ncl` files and generate corresponding Julia structs and enums from them, enabling type-safe dispatch and validation on the Julia side.
      9 
     10 Envisioned usage:
     11 
     12 ```julia
     13 @nickel_types "schema.ncl"
     14 # generates StatusEnum, MyConfig, etc. as Julia types
     15 function process(s::StatusEnum) ... end
     16 ```
     17 
     18 ## Current State
     19 
     20 ### What works today
     21 
     22 - Enum **values** (`'Foo`, `'Some 42`) are fully supported via the binary protocol (TYPE_ENUM, tag 7)
     23 - They decode to `NickelEnum(tag::Symbol, arg::Any)` on the Julia side
     24 - Enum values constrained by enum types evaluate correctly: `nickel_eval_native("let x : [| 'a, 'b |] = 'a in x")` returns `NickelEnum(:a, nothing)`
     25 
     26 ### What doesn't exist
     27 
     28 - No way to extract **type definitions** themselves through the FFI
     29 - `eval_full_for_export()` produces values, not types — type information is erased during evaluation
     30 - `nickel-lang-core 0.9.1` does not expose a public API for type introspection
     31 - Nickel has no runtime reflection on types — you can't ask an enum type for its list of variants
     32 
     33 ## Nickel Type System Background
     34 
     35 - Enum type syntax: `[| 'Carnitas, 'Fish |]` (simple), `[| 'Ok Number, 'Err String |]` (with payloads)
     36 - Enum types are structural and compile-time — they exist for the typechecker, not at runtime
     37 - Types and contracts are interchangeable: `foo : T` and `foo | T` both enforce at runtime
     38 - Row polymorphism allows extensible enums: `[| 'Ok a ; tail |]`
     39 - `std.enum.to_tag_and_arg` decomposes enum values at runtime, but cannot inspect enum types
     40 - ADTs (enum variants with data) fully supported since Nickel 1.5
     41 
     42 Internally, `nickel-lang-core` represents types via the `TypeF` enum, which includes `TypeF::Enum(row_type)` for enum types. But this is internal API, not a stable public surface.
     43 
     44 ## Approaches Considered
     45 
     46 ### Approach 1: Convention-based (schema as value)
     47 
     48 Write schemas as Nickel **values** that describe types, not as actual type annotations:
     49 
     50 ```nickel
     51 {
     52   fields = {
     53     status = { type = "enum", variants = ["active", "inactive"] },
     54     name = { type = "string" },
     55     count = { type = "number" },
     56   }
     57 }
     58 ```
     59 
     60 Then `@nickel_types "schema.ncl"` evaluates this with the existing FFI and generates Julia types.
     61 
     62 - **Pro:** Works today with no Rust changes
     63 - **Con:** Redundant — writing schemas-about-schemas instead of using Nickel's native type syntax
     64 
     65 ### Approach 2: AST walking in Rust (recommended if pursued)
     66 
     67 Add a new Rust FFI function (`nickel_extract_types`) that parses a `.ncl` file, walks the AST, and extracts type annotations from record contracts. Returns a structured description of the type schema.
     68 
     69 The Rust side would:
     70 1. Parse the Nickel source into an AST
     71 2. Walk `Term::RecRecord` / `Term::Record` nodes looking for type annotations on fields
     72 3. For each annotated field, extract the `TypeF` structure
     73 4. Encode `TypeF::Enum(rows)` → list of variant names/types
     74 5. Encode `TypeF::Record(rows)` → list of field names/types
     75 6. Return as JSON or binary protocol
     76 
     77 The Julia side would:
     78 1. Call the FFI function to get the type description
     79 2. In a `@nickel_types` macro, generate `struct` definitions and enum-like types at compile time
     80 
     81 Estimated scope: ~200-400 lines of Rust, plus Julia macro (~100-200 lines).
     82 
     83 - **Pro:** Uses real Nickel type syntax. Elegant.
     84 - **Con:** Couples to `nickel-lang-core` internals (`TypeF` enum, AST structure). Could break across crate versions. Medium-to-large effort.
     85 
     86 ### Approach 3: Nickel-side reflection
     87 
     88 Use Nickel's runtime to reflect on contracts — e.g., `std.record.fields` to list record keys, pattern matching to decompose contracts.
     89 
     90 - **Pro:** No Rust changes
     91 - **Con:** Doesn't work for enum types — Nickel has no runtime mechanism to list the variants of `[| 'a, 'b |]`. Dead end for the core use case.
     92 
     93 ## Conclusion
     94 
     95 **Approach 2 is the only viable path** for using Nickel's native type syntax, but it's a significant investment that couples to unstable internal APIs. **Approach 1 is a pragmatic workaround** if the need becomes pressing.
     96 
     97 This is outside the current scope of NickelEval.jl, which focuses on evaluation, not type extraction. If `nickel-lang-core` ever exposes a public type introspection API, the picture changes significantly.
     98 
     99 ## Key Dependencies
    100 
    101 - `nickel-lang-core` would need to maintain a stable enough AST/type representation (currently internal)
    102 - Julia macro system for compile-time type generation (`@generated` or expression-based macros)
    103 - Decision on how to map Nickel's structural types to Julia's nominal type system (e.g., enum rows → `@enum` or union of `Symbol`s)