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)