ffi.jl (27922B)
1 # FFI bindings to Nickel's official C API (v2.0.0) 2 # Low-level wrappers in libnickel.jl (generated by Clang.jl) 3 # This file provides the convenience layer: library discovery, eval, tree-walk. 4 5 using LazyArtifacts 6 using Libdl 7 8 # Platform-specific library name 9 const LIB_NAME = if Sys.isapple() 10 "libnickel_lang.dylib" 11 elseif Sys.iswindows() 12 "nickel_lang.dll" 13 else 14 "libnickel_lang.so" 15 end 16 17 # Find library: local deps/ -> artifact -> not found 18 function _find_library_path() 19 # Local deps/ (custom builds, HPC overrides) 20 local_path = joinpath(@__DIR__, "..", "deps", LIB_NAME) 21 if isfile(local_path) 22 return local_path 23 end 24 25 # Artifact (auto-selects platform, triggers lazy download) 26 try 27 artifact_dir = @artifact_str("libnickel_lang") 28 lib_path = joinpath(artifact_dir, LIB_NAME) 29 if isfile(lib_path) 30 return lib_path 31 end 32 catch 33 end 34 35 return nothing 36 end 37 38 # Library path and availability resolved at runtime via __init__(), 39 # not at precompile time. This allows Pkg.build() to install the library 40 # after initial precompilation (JLL pattern, requires Julia 1.6+). 41 libnickel_lang::String = "" 42 FFI_AVAILABLE::Bool = false 43 44 function __init_ffi__() 45 path = _find_library_path() 46 if path !== nothing 47 try 48 Libdl.dlopen(path) 49 global libnickel_lang = path 50 global FFI_AVAILABLE = true 51 catch e 52 @warn "Found libnickel_lang at $path but failed to load it: $e\n" * 53 "Build from source with: build_ffi()" 54 end 55 end 56 end 57 58 include("libnickel.jl") 59 import .LibNickel 60 const L = LibNickel 61 62 """ 63 check_ffi_available() -> Bool 64 65 Check if the Nickel C API library is available. 66 """ 67 check_ffi_available() = FFI_AVAILABLE 68 69 """ 70 build_ffi() 71 72 Build the Nickel C API library from source. Requires Rust (cargo). 73 Restarts the FFI after building so you don't need to restart Julia. 74 """ 75 function build_ffi() 76 build_script = joinpath(@__DIR__, "..", "deps", "build.jl") 77 @info "Building Nickel C API library from source..." 78 withenv("NICKELEVAL_BUILD_FFI" => "true") do 79 include(build_script) 80 end 81 # Re-initialize to pick up the newly built library 82 __init_ffi__() 83 if FFI_AVAILABLE 84 @info "FFI ready. nickel_eval() is now available." 85 else 86 error("Build completed but library still not loadable. Check the build output above.") 87 end 88 end 89 90 function _check_ffi_available() 91 FFI_AVAILABLE && return 92 error("Nickel C API library not available.\n\n" * 93 "Build from Julia: using NickelEval; build_ffi()\n" * 94 "Build from shell: NICKELEVAL_BUILD_FFI=true julia -e 'using Pkg; Pkg.build(\"NickelEval\")'\n") 95 end 96 97 # ── Tree-walk: convert C API expr to Julia value ───────────────────────────── 98 99 function _walk_expr(expr::Ptr{L.nickel_expr}) 100 if L.nickel_expr_is_null(expr) != 0 101 return nothing 102 elseif L.nickel_expr_is_bool(expr) != 0 103 return L.nickel_expr_as_bool(expr) != 0 104 elseif L.nickel_expr_is_number(expr) != 0 105 num = L.nickel_expr_as_number(expr) # borrowed, no free 106 if L.nickel_number_is_i64(num) != 0 107 return L.nickel_number_as_i64(num) 108 else 109 return Float64(L.nickel_number_as_f64(num)) 110 end 111 elseif L.nickel_expr_is_str(expr) != 0 112 out_ptr = Ref{Ptr{Cchar}}(C_NULL) 113 len = L.nickel_expr_as_str(expr, out_ptr) 114 return unsafe_string(out_ptr[], len) 115 elseif L.nickel_expr_is_array(expr) != 0 116 arr = L.nickel_expr_as_array(expr) # borrowed, no free 117 n = Int(L.nickel_array_len(arr)) 118 result = Vector{Any}(undef, n) 119 if n > 0 120 elem = L.nickel_expr_alloc() 121 try 122 for i in 0:(n-1) 123 L.nickel_array_get(arr, UInt(i), elem) 124 result[i+1] = _walk_expr(elem) 125 end 126 finally 127 L.nickel_expr_free(elem) 128 end 129 end 130 return result 131 elseif L.nickel_expr_is_record(expr) != 0 132 rec = L.nickel_expr_as_record(expr) # borrowed, no free 133 n = Int(L.nickel_record_len(rec)) 134 result = Dict{String, Any}() 135 if n > 0 136 key_ptr = Ref{Ptr{Cchar}}(C_NULL) 137 key_len = Ref{Csize_t}(0) 138 val_expr = L.nickel_expr_alloc() 139 try 140 for i in 0:(n-1) 141 L.nickel_record_key_value_by_index(rec, UInt(i), key_ptr, key_len, val_expr) 142 key = unsafe_string(key_ptr[], key_len[]) 143 result[key] = _walk_expr(val_expr) 144 end 145 finally 146 L.nickel_expr_free(val_expr) 147 end 148 end 149 return result 150 elseif L.nickel_expr_is_enum_variant(expr) != 0 151 out_ptr = Ref{Ptr{Cchar}}(C_NULL) 152 arg_expr = L.nickel_expr_alloc() 153 try 154 len = L.nickel_expr_as_enum_variant(expr, out_ptr, arg_expr) 155 tag = Symbol(unsafe_string(out_ptr[], len)) 156 arg = _walk_expr(arg_expr) 157 return NickelEnum(tag, arg) 158 finally 159 L.nickel_expr_free(arg_expr) 160 end 161 elseif L.nickel_expr_is_enum_tag(expr) != 0 162 out_ptr = Ref{Ptr{Cchar}}(C_NULL) 163 len = L.nickel_expr_as_enum_tag(expr, out_ptr) 164 tag = Symbol(unsafe_string(out_ptr[], len)) 165 return NickelEnum(tag, nothing) 166 else 167 error("Unknown Nickel expression type") 168 end 169 end 170 171 # ── Error extraction ────────────────────────────────────────────────────────── 172 173 function _throw_nickel_error(err::Ptr{L.nickel_error}) 174 out_str = L.nickel_string_alloc() 175 try 176 L.nickel_error_format_as_string(err, out_str, L.NICKEL_ERROR_FORMAT_TEXT) 177 data_ptr = Ref{Ptr{Cchar}}(C_NULL) 178 data_len = Ref{Csize_t}(0) 179 L.nickel_string_data(out_str, data_ptr, data_len) 180 msg = unsafe_string(data_ptr[], data_len[]) 181 throw(NickelError(msg)) 182 finally 183 L.nickel_string_free(out_str) 184 end 185 end 186 187 # ── Public API ──────────────────────────────────────────────────────────────── 188 189 """ 190 nickel_eval(code::String) -> Any 191 192 Evaluate Nickel code and return a Julia value. 193 194 Returns native Julia types: Int64, Float64, Bool, String, nothing, 195 Vector{Any}, Dict{String,Any}, or NickelEnum. 196 197 # Examples 198 ```julia 199 julia> nickel_eval("1 + 2") 200 3 201 202 julia> nickel_eval("{ a = 1, b = 2 }") 203 Dict{String, Any}("a" => 1, "b" => 2) 204 205 julia> nickel_eval("let x = 5 in x * 2") 206 10 207 ``` 208 """ 209 function nickel_eval(code::String) 210 _check_ffi_available() 211 ctx = L.nickel_context_alloc() 212 expr = L.nickel_expr_alloc() 213 err = L.nickel_error_alloc() 214 try 215 result = L.nickel_context_eval_deep(ctx, code, expr, err) 216 if result == L.NICKEL_RESULT_ERR 217 _throw_nickel_error(err) 218 end 219 return _walk_expr(expr) 220 finally 221 L.nickel_error_free(err) 222 L.nickel_expr_free(expr) 223 L.nickel_context_free(ctx) 224 end 225 end 226 227 """ 228 nickel_eval(code::String, ::Type{T}) -> T 229 230 Evaluate Nickel code and convert to type T. 231 Supports Dict, Vector, and NamedTuple conversions. 232 233 # Examples 234 ```julia 235 julia> nickel_eval("42", Int) 236 42 237 238 julia> nickel_eval("{ a = 1, b = 2 }", Dict{String, Int}) 239 Dict{String, Int64}("a" => 1, "b" => 2) 240 241 julia> nickel_eval("[1, 2, 3]", Vector{Int}) 242 [1, 2, 3] 243 244 julia> nickel_eval("{ x = 1.5, y = 2.5 }", @NamedTuple{x::Float64, y::Float64}) 245 (x = 1.5, y = 2.5) 246 ``` 247 """ 248 function nickel_eval(code::String, ::Type{T}) where T 249 result = nickel_eval(code) 250 return _convert_result(T, result) 251 end 252 253 # ── Type conversion helpers ─────────────────────────────────────────────────── 254 255 _convert_result(::Type{T}, x) where T = convert(T, x) 256 257 function _convert_result(::Type{T}, d::Dict{String,Any}) where T <: NamedTuple 258 fields = fieldnames(T) 259 types = fieldtypes(T) 260 values = Tuple(_convert_result(types[i], d[String(fields[i])]) for i in eachindex(fields)) 261 return T(values) 262 end 263 264 function _convert_result(::Type{Dict{K,V}}, d::Dict{String,Any}) where {K,V} 265 return Dict{K,V}(K(k) => _convert_result(V, v) for (k, v) in d) 266 end 267 268 function _convert_result(::Type{Vector{T}}, v::Vector{Any}) where T 269 return T[_convert_result(T, x) for x in v] 270 end 271 272 # ── File evaluation ─────────────────────────────────────────────────────────── 273 274 """ 275 nickel_eval_file(path::String) -> Any 276 277 Evaluate a Nickel file. Supports `import` statements resolved relative 278 to the file's directory. 279 280 Returns native Julia types: Int64, Float64, Bool, String, nothing, 281 Vector{Any}, Dict{String,Any}, or NickelEnum. 282 283 # Examples 284 ```julia 285 julia> nickel_eval_file("config.ncl") 286 Dict{String, Any}("host" => "localhost", "port" => 8080) 287 ``` 288 """ 289 function nickel_eval_file(path::String) 290 _check_ffi_available() 291 abs_path = abspath(path) 292 if !isfile(abs_path) 293 throw(NickelError("File not found: $abs_path")) 294 end 295 code = read(abs_path, String) 296 ctx = L.nickel_context_alloc() 297 expr = L.nickel_expr_alloc() 298 err = L.nickel_error_alloc() 299 try 300 # Set source name to the absolute file path so Nickel can resolve imports 301 # relative to the file's directory. 302 GC.@preserve abs_path begin 303 L.nickel_context_set_source_name(ctx, Base.unsafe_convert(Ptr{Cchar}, abs_path)) 304 end 305 result = L.nickel_context_eval_deep(ctx, code, expr, err) 306 if result == L.NICKEL_RESULT_ERR 307 _throw_nickel_error(err) 308 end 309 return _walk_expr(expr) 310 finally 311 L.nickel_error_free(err) 312 L.nickel_expr_free(expr) 313 L.nickel_context_free(ctx) 314 end 315 end 316 317 # ── Export (serialization) ──────────────────────────────────────────────────── 318 319 function _eval_and_serialize(code::String, serialize_fn) 320 _check_ffi_available() 321 ctx = L.nickel_context_alloc() 322 expr = L.nickel_expr_alloc() 323 err = L.nickel_error_alloc() 324 out_str = L.nickel_string_alloc() 325 try 326 result = L.nickel_context_eval_deep_for_export(ctx, code, expr, err) 327 if result == L.NICKEL_RESULT_ERR 328 _throw_nickel_error(err) 329 end 330 ser_result = serialize_fn(ctx, expr, out_str, err) 331 if ser_result == L.NICKEL_RESULT_ERR 332 _throw_nickel_error(err) 333 end 334 data_ptr = Ref{Ptr{Cchar}}(C_NULL) 335 data_len = Ref{Csize_t}(0) 336 L.nickel_string_data(out_str, data_ptr, data_len) 337 return unsafe_string(data_ptr[], data_len[]) 338 finally 339 L.nickel_string_free(out_str) 340 L.nickel_error_free(err) 341 L.nickel_expr_free(expr) 342 L.nickel_context_free(ctx) 343 end 344 end 345 346 """ 347 nickel_to_json(code::String) -> String 348 349 Evaluate Nickel code and export to a JSON string. 350 351 # Examples 352 ```julia 353 julia> nickel_to_json("{ a = 1, b = \"hello\" }") 354 "{\\"a\\": 1,\\"b\\": \\"hello\\"}" 355 ``` 356 """ 357 nickel_to_json(code::String) = _eval_and_serialize(code, L.nickel_context_expr_to_json) 358 359 """ 360 nickel_to_yaml(code::String) -> String 361 362 Evaluate Nickel code and export to a YAML string. 363 364 # Examples 365 ```julia 366 julia> nickel_to_yaml("{ a = 1 }") 367 "a: 1\\n" 368 ``` 369 """ 370 nickel_to_yaml(code::String) = _eval_and_serialize(code, L.nickel_context_expr_to_yaml) 371 372 """ 373 nickel_to_toml(code::String) -> String 374 375 Evaluate Nickel code and export to a TOML string. 376 377 # Examples 378 ```julia 379 julia> nickel_to_toml("{ a = 1 }") 380 "a = 1\\n" 381 ``` 382 """ 383 nickel_to_toml(code::String) = _eval_and_serialize(code, L.nickel_context_expr_to_toml) 384 385 # ── Lazy evaluation ────────────────────────────────────────────────────────── 386 387 function _check_session_open(session::NickelSession) 388 session.closed && throw(ArgumentError("NickelSession is closed")) 389 end 390 391 # Allocate a new expr tracked by the session 392 function _tracked_expr_alloc(session::NickelSession) 393 expr = L.nickel_expr_alloc() 394 push!(session.exprs, Ptr{Cvoid}(expr)) 395 return expr 396 end 397 398 function Base.close(session::NickelSession) 399 session.closed && return 400 session.closed = true 401 for expr_ptr in session.exprs 402 L.nickel_expr_free(Ptr{L.nickel_expr}(expr_ptr)) 403 end 404 empty!(session.exprs) 405 L.nickel_context_free(Ptr{L.nickel_context}(session.ctx)) 406 return nothing 407 end 408 409 Base.close(v::NickelValue) = close(getfield(v, :session)) 410 411 """ 412 nickel_open(f, code::String) 413 nickel_open(code::String) -> NickelValue 414 415 Evaluate Nickel code shallowly and return a lazy `NickelValue`. 416 Sub-expressions are evaluated on demand when accessed via `.field` or `["field"]`. 417 418 # Do-block (preferred) 419 ```julia 420 nickel_open("{ x = 1, y = 2 }") do cfg 421 cfg.x # => 1 (only evaluates x) 422 end 423 ``` 424 425 # Manual 426 ```julia 427 cfg = nickel_open("{ x = 1, y = 2 }") 428 cfg.x # => 1 429 close(cfg) 430 ``` 431 """ 432 function nickel_open(f::Function, path_or_code::String) 433 val = nickel_open(path_or_code) 434 try 435 return f(val) 436 finally 437 close(val) 438 end 439 end 440 441 function nickel_open(path_or_code::String) 442 _check_ffi_available() 443 # Detect file path: ends with .ncl AND exists on disk 444 if endswith(path_or_code, ".ncl") && isfile(abspath(path_or_code)) 445 return _nickel_open_file(path_or_code) 446 end 447 return _nickel_open_code(path_or_code) 448 end 449 450 function _nickel_open_file(path::String) 451 abs_path = abspath(path) 452 if !isfile(abs_path) 453 throw(NickelError("File not found: $abs_path")) 454 end 455 code = read(abs_path, String) 456 ctx = L.nickel_context_alloc() 457 session = NickelSession(Ptr{Cvoid}(ctx), Ptr{Cvoid}[], false) 458 finalizer(close, session) 459 expr = _tracked_expr_alloc(session) 460 err = L.nickel_error_alloc() 461 try 462 GC.@preserve abs_path begin 463 L.nickel_context_set_source_name(ctx, Base.unsafe_convert(Ptr{Cchar}, abs_path)) 464 end 465 result = L.nickel_context_eval_shallow(ctx, code, expr, err) 466 if result == L.NICKEL_RESULT_ERR 467 _throw_nickel_error(err) 468 end 469 return NickelValue(session, Ptr{Cvoid}(expr)) 470 catch 471 close(session) 472 rethrow() 473 finally 474 L.nickel_error_free(err) 475 end 476 end 477 478 function _nickel_open_code(code::String) 479 ctx = L.nickel_context_alloc() 480 session = NickelSession(Ptr{Cvoid}(ctx), Ptr{Cvoid}[], false) 481 finalizer(close, session) 482 expr = _tracked_expr_alloc(session) 483 err = L.nickel_error_alloc() 484 try 485 result = L.nickel_context_eval_shallow(ctx, code, expr, err) 486 if result == L.NICKEL_RESULT_ERR 487 _throw_nickel_error(err) 488 end 489 return NickelValue(session, Ptr{Cvoid}(expr)) 490 catch 491 close(session) 492 rethrow() 493 finally 494 L.nickel_error_free(err) 495 end 496 end 497 498 # Given a shallow-eval'd expr, return a Julia value (primitive) or NickelValue (compound). 499 # Primitives (null, bool, number, string, bare enum tag) are converted immediately. 500 # Compound types (record, array, enum variant) stay lazy as a new NickelValue. 501 function _resolve_value(session::NickelSession, expr::Ptr{L.nickel_expr}) 502 if L.nickel_expr_is_null(expr) != 0 503 return nothing 504 elseif L.nickel_expr_is_bool(expr) != 0 505 return L.nickel_expr_as_bool(expr) != 0 506 elseif L.nickel_expr_is_number(expr) != 0 507 num = L.nickel_expr_as_number(expr) 508 if L.nickel_number_is_i64(num) != 0 509 return L.nickel_number_as_i64(num) 510 else 511 return Float64(L.nickel_number_as_f64(num)) 512 end 513 elseif L.nickel_expr_is_str(expr) != 0 514 out_ptr = Ref{Ptr{Cchar}}(C_NULL) 515 len = L.nickel_expr_as_str(expr, out_ptr) 516 return unsafe_string(out_ptr[], len) 517 elseif L.nickel_expr_is_enum_tag(expr) != 0 518 out_ptr = Ref{Ptr{Cchar}}(C_NULL) 519 len = L.nickel_expr_as_enum_tag(expr, out_ptr) 520 tag = Symbol(unsafe_string(out_ptr[], len)) 521 return NickelEnum(tag, nothing) 522 else 523 # record, array, or enum variant — stay lazy 524 return NickelValue(session, Ptr{Cvoid}(expr)) 525 end 526 end 527 528 # Evaluate a sub-expression shallowly, then resolve to Julia value or NickelValue. 529 function _eval_and_resolve(session::NickelSession, sub_expr::Ptr{L.nickel_expr}) 530 ctx = Ptr{L.nickel_context}(session.ctx) 531 out_expr = _tracked_expr_alloc(session) 532 err = L.nickel_error_alloc() 533 try 534 result = L.nickel_context_eval_expr_shallow(ctx, sub_expr, out_expr, err) 535 if result == L.NICKEL_RESULT_ERR 536 _throw_nickel_error(err) 537 end 538 return _resolve_value(session, out_expr) 539 finally 540 L.nickel_error_free(err) 541 end 542 end 543 544 """ 545 nickel_kind(v::NickelValue) -> Symbol 546 547 Return the kind of a lazy Nickel value without evaluating its children. 548 549 Returns one of: `:record`, `:array`, `:number`, `:string`, `:bool`, `:null`, `:enum`. 550 """ 551 function nickel_kind(v::NickelValue) 552 _check_session_open(getfield(v, :session)) 553 expr = Ptr{L.nickel_expr}(getfield(v, :expr)) 554 if L.nickel_expr_is_null(expr) != 0 555 return :null 556 elseif L.nickel_expr_is_bool(expr) != 0 557 return :bool 558 elseif L.nickel_expr_is_number(expr) != 0 559 return :number 560 elseif L.nickel_expr_is_str(expr) != 0 561 return :string 562 elseif L.nickel_expr_is_array(expr) != 0 563 return :array 564 elseif L.nickel_expr_is_record(expr) != 0 565 return :record 566 elseif L.nickel_expr_is_enum_variant(expr) != 0 || L.nickel_expr_is_enum_tag(expr) != 0 567 return :enum 568 else 569 error("Unknown Nickel expression type") 570 end 571 end 572 573 function Base.show(io::IO, v::NickelValue) 574 session = getfield(v, :session) 575 if session.closed 576 print(io, "NickelValue(<closed>)") 577 return 578 end 579 k = nickel_kind(v) 580 expr = Ptr{L.nickel_expr}(getfield(v, :expr)) 581 if k == :record 582 rec = L.nickel_expr_as_record(expr) 583 n = Int(L.nickel_record_len(rec)) 584 print(io, "NickelValue(:record, $n field", n == 1 ? "" : "s", ")") 585 elseif k == :array 586 arr = L.nickel_expr_as_array(expr) 587 n = Int(L.nickel_array_len(arr)) 588 print(io, "NickelValue(:array, $n element", n == 1 ? "" : "s", ")") 589 else 590 print(io, "NickelValue(:$k)") 591 end 592 end 593 594 # ── Materialization ─────────────────────────────────────────────────────────── 595 596 """ 597 collect(v::NickelValue) -> Any 598 599 Recursively evaluate and materialize the entire subtree rooted at `v`. 600 Returns the same types as `nickel_eval`: Dict, Vector, Int64, Float64, etc. 601 """ 602 function Base.collect(v::NickelValue) 603 session = getfield(v, :session) 604 _check_session_open(session) 605 expr = Ptr{L.nickel_expr}(getfield(v, :expr)) 606 return _collect_expr(session, expr) 607 end 608 609 # Recursive collect: shallow-eval each sub-expression, then convert. 610 # Unlike _walk_expr, this must eval each child before inspecting its type. 611 function _collect_expr(session::NickelSession, expr::Ptr{L.nickel_expr}) 612 ctx = Ptr{L.nickel_context}(session.ctx) 613 614 if L.nickel_expr_is_null(expr) != 0 615 return nothing 616 elseif L.nickel_expr_is_bool(expr) != 0 617 return L.nickel_expr_as_bool(expr) != 0 618 elseif L.nickel_expr_is_number(expr) != 0 619 num = L.nickel_expr_as_number(expr) 620 if L.nickel_number_is_i64(num) != 0 621 return L.nickel_number_as_i64(num) 622 else 623 return Float64(L.nickel_number_as_f64(num)) 624 end 625 elseif L.nickel_expr_is_str(expr) != 0 626 out_ptr = Ref{Ptr{Cchar}}(C_NULL) 627 len = L.nickel_expr_as_str(expr, out_ptr) 628 return unsafe_string(out_ptr[], len) 629 elseif L.nickel_expr_is_array(expr) != 0 630 arr = L.nickel_expr_as_array(expr) 631 n = Int(L.nickel_array_len(arr)) 632 result = Vector{Any}(undef, n) 633 for i in 0:(n-1) 634 elem = _tracked_expr_alloc(session) 635 L.nickel_array_get(arr, Csize_t(i), elem) 636 evaled = _tracked_expr_alloc(session) 637 err = L.nickel_error_alloc() 638 try 639 r = L.nickel_context_eval_expr_shallow(ctx, elem, evaled, err) 640 if r == L.NICKEL_RESULT_ERR 641 _throw_nickel_error(err) 642 end 643 finally 644 L.nickel_error_free(err) 645 end 646 result[i+1] = _collect_expr(session, evaled) 647 end 648 return result 649 elseif L.nickel_expr_is_record(expr) != 0 650 rec = L.nickel_expr_as_record(expr) 651 n = Int(L.nickel_record_len(rec)) 652 result = Dict{String, Any}() 653 key_ptr = Ref{Ptr{Cchar}}(C_NULL) 654 key_len = Ref{Csize_t}(0) 655 for i in 0:(n-1) 656 val_expr = _tracked_expr_alloc(session) 657 L.nickel_record_key_value_by_index(rec, Csize_t(i), key_ptr, key_len, val_expr) 658 key = unsafe_string(key_ptr[], key_len[]) 659 evaled = _tracked_expr_alloc(session) 660 err = L.nickel_error_alloc() 661 try 662 r = L.nickel_context_eval_expr_shallow(ctx, val_expr, evaled, err) 663 if r == L.NICKEL_RESULT_ERR 664 _throw_nickel_error(err) 665 end 666 finally 667 L.nickel_error_free(err) 668 end 669 result[key] = _collect_expr(session, evaled) 670 end 671 return result 672 elseif L.nickel_expr_is_enum_variant(expr) != 0 673 out_ptr = Ref{Ptr{Cchar}}(C_NULL) 674 arg_expr = _tracked_expr_alloc(session) 675 len = L.nickel_expr_as_enum_variant(expr, out_ptr, arg_expr) 676 tag = Symbol(unsafe_string(out_ptr[], len)) 677 evaled = _tracked_expr_alloc(session) 678 err = L.nickel_error_alloc() 679 try 680 r = L.nickel_context_eval_expr_shallow(ctx, arg_expr, evaled, err) 681 if r == L.NICKEL_RESULT_ERR 682 _throw_nickel_error(err) 683 end 684 finally 685 L.nickel_error_free(err) 686 end 687 return NickelEnum(tag, _collect_expr(session, evaled)) 688 elseif L.nickel_expr_is_enum_tag(expr) != 0 689 out_ptr = Ref{Ptr{Cchar}}(C_NULL) 690 len = L.nickel_expr_as_enum_tag(expr, out_ptr) 691 return NickelEnum(Symbol(unsafe_string(out_ptr[], len)), nothing) 692 else 693 error("Unknown Nickel expression type") 694 end 695 end 696 697 # ── Inspection ──────────────────────────────────────────────────────────────── 698 699 function Base.keys(v::NickelValue) 700 session = getfield(v, :session) 701 _check_session_open(session) 702 expr = Ptr{L.nickel_expr}(getfield(v, :expr)) 703 if L.nickel_expr_is_record(expr) == 0 704 throw(ArgumentError("Cannot get keys: NickelValue is not a record")) 705 end 706 rec = L.nickel_expr_as_record(expr) 707 n = Int(L.nickel_record_len(rec)) 708 result = Vector{String}(undef, n) 709 key_ptr = Ref{Ptr{Cchar}}(C_NULL) 710 key_len = Ref{Csize_t}(0) 711 for i in 0:(n-1) 712 L.nickel_record_key_value_by_index(rec, Csize_t(i), key_ptr, key_len, 713 Ptr{L.nickel_expr}(C_NULL)) 714 result[i+1] = unsafe_string(key_ptr[], key_len[]) 715 end 716 return result 717 end 718 719 function Base.length(v::NickelValue) 720 session = getfield(v, :session) 721 _check_session_open(session) 722 expr = Ptr{L.nickel_expr}(getfield(v, :expr)) 723 if L.nickel_expr_is_record(expr) != 0 724 return Int(L.nickel_record_len(L.nickel_expr_as_record(expr))) 725 elseif L.nickel_expr_is_array(expr) != 0 726 return Int(L.nickel_array_len(L.nickel_expr_as_array(expr))) 727 else 728 throw(ArgumentError("Cannot get length: NickelValue is not a record or array")) 729 end 730 end 731 732 # ── Iteration ───────────────────────────────────────────────────────────────── 733 734 function Base.iterate(v::NickelValue, state=1) 735 session = getfield(v, :session) 736 _check_session_open(session) 737 expr = Ptr{L.nickel_expr}(getfield(v, :expr)) 738 739 if L.nickel_expr_is_record(expr) != 0 740 rec = L.nickel_expr_as_record(expr) 741 n = Int(L.nickel_record_len(rec)) 742 state > n && return nothing 743 key_ptr = Ref{Ptr{Cchar}}(C_NULL) 744 key_len = Ref{Csize_t}(0) 745 val_expr = _tracked_expr_alloc(session) 746 L.nickel_record_key_value_by_index(rec, Csize_t(state - 1), key_ptr, key_len, val_expr) 747 key = unsafe_string(key_ptr[], key_len[]) 748 val = _eval_and_resolve(session, val_expr) 749 return (key => val, state + 1) 750 elseif L.nickel_expr_is_array(expr) != 0 751 arr = L.nickel_expr_as_array(expr) 752 n = Int(L.nickel_array_len(arr)) 753 state > n && return nothing 754 elem = _tracked_expr_alloc(session) 755 L.nickel_array_get(arr, Csize_t(state - 1), elem) 756 val = _eval_and_resolve(session, elem) 757 return (val, state + 1) 758 else 759 throw(ArgumentError("Cannot iterate: NickelValue is not a record or array")) 760 end 761 end 762 763 # ── Navigation ─────────────────────────────────────────────────────────────── 764 765 function Base.getproperty(v::NickelValue, name::Symbol) 766 return _lazy_field_access(v, String(name)) 767 end 768 769 function Base.getindex(v::NickelValue, key::String) 770 return _lazy_field_access(v, key) 771 end 772 773 function Base.getindex(v::NickelValue, idx::Integer) 774 session = getfield(v, :session) 775 _check_session_open(session) 776 expr = Ptr{L.nickel_expr}(getfield(v, :expr)) 777 if L.nickel_expr_is_array(expr) == 0 778 throw(ArgumentError("Cannot index with integer: NickelValue is not an array")) 779 end 780 arr = L.nickel_expr_as_array(expr) 781 n = Int(L.nickel_array_len(arr)) 782 if idx < 1 || idx > n 783 throw(BoundsError(v, idx)) 784 end 785 out_expr = _tracked_expr_alloc(session) 786 L.nickel_array_get(arr, Csize_t(idx - 1), out_expr) # 0-based C API 787 return _eval_and_resolve(session, out_expr) 788 end 789 790 function _lazy_field_access(v::NickelValue, key::String) 791 session = getfield(v, :session) 792 _check_session_open(session) 793 expr = Ptr{L.nickel_expr}(getfield(v, :expr)) 794 if L.nickel_expr_is_record(expr) == 0 795 throw(ArgumentError("Cannot access field '$key': NickelValue is not a record")) 796 end 797 rec = L.nickel_expr_as_record(expr) 798 out_expr = _tracked_expr_alloc(session) 799 has_value = L.nickel_record_value_by_name(rec, key, out_expr) 800 if has_value == 0 801 # Check whether the key exists at all 802 n = Int(L.nickel_record_len(rec)) 803 found = false 804 key_ptr = Ref{Ptr{Cchar}}(C_NULL) 805 key_len = Ref{Csize_t}(0) 806 for i in 0:(n-1) 807 L.nickel_record_key_value_by_index(rec, Csize_t(i), key_ptr, key_len, 808 Ptr{L.nickel_expr}(C_NULL)) 809 if unsafe_string(key_ptr[], key_len[]) == key 810 found = true 811 break 812 end 813 end 814 if !found 815 throw(NickelError("Field '$key' not found in record")) 816 end 817 throw(NickelError("Field '$key' has no value (contract-only or unevaluated)")) 818 end 819 return _eval_and_resolve(session, out_expr) 820 end