These libraries are experimental. APIs may change without notice. Generated from source with koruc 0.1.7 on 8/19/2026.
YAML
@korulang/yaml@0.0.1Streaming YAML parser for Koru wrapping libyaml with phantom obligation types
yaml/index.kz · 8 tors
@korulang/yaml - Streaming YAML parser for Koru · 52 more lines
@korulang/yaml - Streaming YAML parser for Koru
Wraps libyaml (the reference C implementation — the same engine behind
PyYAML, Ruby's Psych, and most language YAML bindings) and lifts its
notoriously leak-prone streaming event API into Koru's phantom obligation
system.
libyaml's parser is a two-level resource machine, and BOTH levels leak if you
forget a single call:
* the parser itself must be `yaml_parser_delete`d, and
* EVERY event returned by `yaml_parser_parse` must be `yaml_event_delete`d
(each event owns heap-allocated scalar / tag / anchor buffers).
The classic footgun is the second one: a `while` loop that parses events but
forgets `yaml_event_delete` inside the loop body leaks on every single event.
In C that is a silent, unbounded leak. Here it is a compile error.
This edition fuses the two resources into ONE obligation-bearing handle — a
`Cursor<live!>` — exactly the way the language's own resource-lifecycle
idiom fuses a connection and its open transaction into a single tracked
state (koru tests/regression/.../2104 `app/db`: `Transaction<active!>`). A
live cursor means "the parser is open AND a current event is undeleted"; you
thread that one obligation, and the compiler will not let you drop it:
* `advance` consumes a live cursor and mints a fresh one (delete-then-parse)
* `finish` consumes a live cursor and fully cleans up (delete + close)
Forgetting either — leaking the cursor — does not compile.
USAGE (straight-line; see examples/ for a walked document):
~import libs/yaml
~libs/yaml:open(input: yaml_text)
| ok p |> libs/yaml:begin(parser: p)
| at c |> libs/yaml:kind(cur: c): k |> ...inspect c... |> libs/yaml:finish(cur: c)
| eof pp |> libs/yaml:close(parser: pp)
| err ep |> libs/yaml:close(parser: ep)
| err e |> ...
─────────────────────────────────────────────────────────────────────────
⚠ DESIGN-DEBT / REVISIT (grep: DESIGN-DEBT yaml): this library is a bit
wonky and does not yet play fully to Koru's strengths.
* FIXED: the read-only accessors (`kind`, `scalar`, `error-text`) BORROW
the handle (`<live>` / `<open>`) instead of consuming + re-minting it —
the clean accessor form (cf. pcre2 `group.text { m: *Match<match> }`).
* STILL WONKY: `begin`/`advance` return a MULTI-branch shape
(`at` cursor | `eof`/`err` parser), so they re-mint the parser
obligation on the non-cursor branches. There IS a real reason for that
(each branch needs its own single obligation) — it is not ceremony.
* THE FIX (deferred): add an OPTIONAL effect-branch parse form — a
library-driven `walk` yielding each event via `! event e` (modelled on
pcre2 `find.all ! match` / sqlite3 `! row`), keeping the manual
primitives for fine control. That removes the re-minting entirely.
─────────────────────────────────────────────────────────────────────────
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.
Parser 1 state open!Cursor 1 state live!// Parser lifecycle
//
// Open a parser over an in-memory YAML document. The returned Parser carries
// the `open!` obligation: it MUST reach `begin` (which converts it to a live
// cursor) or `close`, or the program will not compile.
~pub tor open { input: string, allocator: ?std.mem.Allocator }
| ok *Parser<open!>
| err string// Close an open parser, discharging the `open!` obligation. Used on the `eof`
// and `err` branches of `begin` / `advance`, where there is no live event.
~pub tor close { parser: *Parser<!open> }// The text of the last parse error on a parser. BORROWS the parser (`<open>`)
// so you can read the message and then still `close` it — no threading.
~pub tor error-text { parser: *Parser<open> } -> string// Cursor: the fused parser+event obligation
//
// Parse the first event and hand back a live cursor. Consumes the parser's
// `open!` and mints a `live!` cursor. On an empty stream returns the parser
// (still open) via `eof`; on a parse error, sets `parser.last_error` and
// returns the parser (still open) via `err` so you can inspect and close it.
~pub tor begin { parser: *Parser<!open> }
| at *Cursor<live!>
| eof *Parser<open!>
| err *Parser<open!>// Delete the current event and parse the next one, threading the single `live!`
// obligation. On stream end deletes the final event, frees the cursor wrapper,
// and returns the parser (still open) via `eof`. On error likewise returns the
// open parser via `err` with `parser.last_error` set.
~pub tor advance { cur: *Cursor<!live> }
| at *Cursor<live!>
| eof *Parser<open!>
| err *Parser<open!>// Delete the current event and close the parser in one move — the full-cleanup
// discharge of a live cursor. Use this to stop walking early.
~pub tor finish { cur: *Cursor<!live> }// Event inspection (all accessors thread the cursor's `live!` obligation)
//
// The human-readable kind of the current event, as a stable static string.
~pub tor kind { cur: *Cursor<live> } -> string// The scalar text of the current event. BORROWS the cursor (`<live>`); on a
// non-scalar event takes the `other` branch with no value. The returned slice
// borrows the event's internal buffer and is valid only until the next
// `advance` / `finish` — read it before advancing (the cursor stays live).
~pub tor scalar { cur: *Cursor<live> }
| value string
| other