These libraries are experimental. APIs may change without notice. Generated from source with koruc 0.1.7 on 8/19/2026.

yyjson

@korulang/yyjson@0.1.0

yyjson (fast conformant JSON) for Koru with phantom obligation types — parse, read, build and render

yyjson/index.kz · 26 tors

@korulang/yyjson — the definitive Koru yyjson edition (read + write) · 59 more lines
@korulang/yyjson — the definitive Koru yyjson edition (read + write) Lifts yyjson (the fastest conformant JSON library — single-file C, used by PHP internals, ScyllaDB, and others) into a phantom-obligation-typed Koru edition. yyjson's read API hands back an arena-owned `yyjson_doc`: every `yyjson_val*` you navigate to (object members, array elements, scalars) is a raw pointer INTO that arena. Free the doc and every value pointer you were holding is instantly dangling — a use-after-free that in C is a silent footgun and in production is a CVE waiting to happen. This edition compiles that footgun away: • The parsed document is a phantom obligation (`open`): the build FAILS if you forget to free it (`close`), same shape as sqlite3's Connection / pcre2's Regex. • Every value-reading event (`root`, `object.get`, `array.get`, `array.each`, `object.each`, `as.*`) takes `doc: *Doc<open>` as a BORROW, not just documentation — the compiler requires the caller to still hold the live `open!` obligation on that exact binding. Call `close` first and then try to read a value and the phantom checker rejects it with "Use-after-discharge" — the value cannot be read once its doc is gone, enforced at compile time, not discovered at 3am from a crash dump. • Wrong-type reads (`as.string` on a number, `as.int` on an object) are a loud `wrong-type` branch, never a silently-defaulted zero or empty string — yyjson's own C accessors return 0/NULL on type mismatch, which we refuse to expose directly because it hides bugs. USAGE: ~import koru/yyjson ~koru/yyjson:parse(text: "{\"name\": \"ada\", \"age\": 36}") | ok doc |> koru/yyjson:root(doc): root_v |> koru/yyjson:object.get(doc, v: root_v, key: "name") | found name_v |> koru/yyjson:as.string(doc, v: name_v) | ok name |> std/io:print.ln("name: {{ name:s }}") |> koru/yyjson:close(doc) | wrong-type |> std/io:print.ln("name was not a string") |> koru/yyjson:close(doc) | not-found |> std/io:print.ln("no name field") |> koru/yyjson:close(doc) | err e |> std/io:print.ln("parse failed at byte {{ e.pos:d }}: {{ e.msg:s }}") (Note the explicit `v: name_v` label on the second call — bare positional args only punning-match when the local binding's name equals the callee's own parameter name, as `doc` does above; `root_v`/`name_v` do not match `v` and need the label. See tests/basic.kz for this exact flow, verified end-to-end.) Scope: BOTH directions. The read path (`parse` … `close`, above) navigates objects/arrays, iterates both, and reads scalar leaves. The write path (`build` … `build.close`, at the bottom of this file) mints nodes, assembles objects and arrays, and renders to text — with the rendered buffer carrying its own second obligation, because yyjson allocates it from libc rather than the document arena. Not bound, deliberately: reading or writing files by path (`yyjson_read_file` / `yyjson_mut_write_file` — koru already has `std/io:read-file`, and a second file API in a JSON package earns nothing), the immutable/mutable bridges (`yyjson_doc_mut_copy`, `yyjson_mut_doc_imut_copy`), JSON Pointer / Patch / Merge-Patch, unsigned 64-bit integers (no `as.uint` on the read side to mirror, so the hole is symmetric), and all write flags but `PRETTY`. See README for the reasoning on each.

Phantom lifecycles

Derived from the phantom labels in the declarations below — state! issues an obligation the compiler will chase, !state discharges it, a bare state holds it without moving it. Nothing here is hand-drawn.

Doc 1 state open!
Builder 1 state open!
string 1 state rendered!

parse

<open!> index.kz:119

close

<!open> index.kz:141

root

<open> index.kz:157

object.get

<open> index.kz:172

array.get

<open> index.kz:188

array.each

<open> index.kz:211

object.each

<open> index.kz:240

as.string

<open> index.kz:268

as.int

<open> index.kz:281

as.double

<open> index.kz:291

as.bool

<open> index.kz:301

build

<open!> index.kz:394

build.close

<!open> index.kz:403

new.object

<open> index.kz:424

new.array

<open> index.kz:430

new.string

<open> index.kz:436

new.int

<open> index.kz:449

new.double

<open> index.kz:455

new.bool

<open> index.kz:461

new.null

<open> index.kz:467

object.set

<open> index.kz:500

array.push

<open> index.kz:513

root.set

<open> index.kz:533

render

<open><rendered!> index.kz:555

render.pretty

<open><rendered!> index.kz:574

render.free

<!rendered> index.kz:593