# jm — Odin for scripts A collection of small packages and one runner that make Odin comfortable for the scripts Python and bash usually get. Everything builds on `core:`. The only system library is libcurl through `vendor:curl`; SQLite and the wasm3 interpreter are vendored and linked statically, so a script that uses either still installs nothing. ```odin #!/usr/bin/env odin-run package main import "core:fmt" import "jm:prelude" import "jm:sh" import "jm:path" must :: prelude.must die :: prelude.die main :: proc() { context = prelude.init() branch := must(sh.out("git rev-parse --abbrev-ref HEAD")) for f in must(path.walk(".")) { fmt.println(branch, f) } } ``` `chmod +x` and run it. The first run compiles; later runs start the cached binary. ## Packages | Package | Job | |-----------|-----| | `prelude` | `must`, `die`, `env`, `args`; arena or debug allocator; logfmt log file plus `deaths.log` audit trail | | `sh` | `out`, `lines`, `ok`, `run`, `capture` through the shell; `exec`, `exec_run` with argv; `which`, `quote`, `error` | | `http` | `get`, `post`, `post_json`, `get_json`, `download`, `request` over libcurl | | `path` | `expand`, `join`, `mkdirs`, `read`, `read_lines`, `write`, `append_file`, `list`, `walk`, `temp_dir`, `same`, per-user app dirs | | `timefmt` | strftime `format`, `local`, `parse`; `iso`, `stamp`, `date`, `duration` | | `debug` | guard-byte debug allocator (origin: sonar, which now imports this copy) | | `flow` | `width`, `each`, `manage`: lock-free worker pools where each worker owns one state slot and the caller merges afterwards | | `tar` | `read`, `extract`: `git archive` output without a tar program | | `sqlite3` | `open`, `exec`, `exec_args`, `query`/`next`, `prepare`, `transact` over a statically linked SQLite | | `wasm` | `open`, `load`, `find`, `call`, `link`, `run`: WebAssembly through a statically linked wasm3 | | `ui` | immediate-mode UI: ops → `flatten` → draw and hit lists; layout, theme, widgets, a `Probe` that clicks and types without a window | | `ui/render` | executes a `ui.Frame` on Blend2D (vendored binding in `ui/blend2d`), shapes text with it, and `snapshot`s a `ui` proc straight to a PNG | | `ui/sdl` | the SDL3 window and event loop for a `ui` app; `run_host` runs the same window against a subprocess instead of a local ui proc | | `ui/ipc` | length-prefixed frames over a pipe, and spawning a child process wired up for exactly that — the transport under `ui/sdl`'s host/subprocess split | | `ui/child` | the subprocess half of that split: owns the Model, the ui proc, `Router` and `Layout`, and speaks `ui`'s wire format over its own stdin/stdout | | `ui/diagram` | titled, accent-bordered groups of chips and arrows (solid or dashed) for an architecture diagram, over plain `ui` calls | | `ui/material` | Material 3 Expressive on `ui`: the colour scheme, type scale, shape, motion springs and state tokens generated from the m3e-kit into `ui/material/tokens`, Material Symbols icons as paths, and the components (buttons, text fields, selection controls, chips, cards, lists, navigation, app bars, tabs), each able to paint any spec state on demand | | `ui/testutil` | `count_ops`: assertions a `ui` package's own tests and a downstream package's tests both want, without an import cycle | | `pg_query` | `parse`, `split`, `is_utility`, `fingerprint`, `normalize`: PostgreSQL's own SQL parser, statically linked, with node types generated from its schema | | `pq` | `connect`, `exec`, `escape_literal`, `escape_identifier`, `identity`: a PostgreSQL client over the system libpq, the one dynamically linked library | | `pq/testdb` | a throwaway PostgreSQL server on a Unix socket, for tests | | `fuzz` | property fuzzing: an entropy `Source`, generators, format-agnostic `damage`, shrinking, a corpus, a per-case deadline | | `sqlite3/fuzz` | the `jm:sqlite3` suite for `jm:fuzz` | | `tar/fuzz` | the `jm:tar` suite for `jm:fuzz` | | `wasm/fuzz` | the `jm:wasm` suite for `jm:fuzz`, with a small Wasm encoder to build cases from | | `pg_query/fuzz` | the `jm:pg_query` suite for `jm:fuzz`, with a SQL generator to build cases from | | `pq/fuzz` | the `jm:pq` suite for `jm:fuzz`, against the `pq/testdb` server | | `ui/render/fuzz` | the `jm:ui/render` suite for `jm:fuzz`: composed frames checked against whole renders | `tools/odin-run` is the runner. Every package reads on its own; the doc comment at the top of each file is the reference. ## Conventions - Scripts never free. `prelude.init` puts a growing arena in `context.allocator`; the exit reclaims it. `ODIN_SCRIPT_DEBUG=1` swaps in the debug allocator, which reports overflow, double free and write after free at exit. - Anything that can fail returns `(value, ok)` or `(value, os.Error)` so `must(...)` wraps it. `die` writes the message and the call site to the script log, to stderr, and to one shared `deaths.log`. - `prelude` never writes to stdout. Stdout is the script's data channel. - Logs: `/odin//.log`, rotated to `.1` at 8 MiB, and `/odin/deaths.log`. The log dir is `~/.local/state` on Linux, `~/Library/Logs` on macOS and `%LOCALAPPDATA%` on Windows. `ODIN_LOG=debug` echoes everything to stderr; the default echoes warning and above. - Platform splits live in `_unix.odin` / `_windows.odin` files. `just check` type-checks every package for `linux_amd64`, `darwin_arm64` and `windows_amd64` from one machine. ## Runner ``` odin-run script.odin [args...] compile if stale, then run odin-run -clean drop the cache ``` Cache key: script bytes and path, every `.odin` in the collection, the odin binary, target and flags. `ODIN_RUN_FLAGS="-debug"` builds unoptimised; `ODIN_RUN_VERBOSE=1` prints the build line. On Windows the shebang does nothing; call `odin-run script.odin` or associate `.odin` with it. When `vendor/curl/lib/libcurl.dll` exists under the Odin root it is copied beside the cached binary. ## Recipes ``` just build debug odin-run just release optimised odin-run just test all package tests just check 3-target type-check just install odin-run -> ~/.local/bin (BINDIR overrides) just sqlite compile the vendored SQLite just wasm compile wasm3 just pg_query compile the vendored libpg_query just pg_query-gen regenerate pg_query/nodes.odin from the vendored schema just blend2d compile Blend2D into ui/blend2d/lib (BLEND2D_SRC overrides) just kitchen open the jm:ui demo just kitchen-dump its first frame as text just kitchen-png render it to build/kitchen.png just material-kitchen build and open the hot-reloaded M3 kitchen just material-png page=Chips render one M3 kitchen page to build/ just material-tokens regenerate ui/material/tokens from the m3e-kit (M3E_KIT) just material-shapes regenerate ui/material/shape_data.odin from the m3e-kit's morphs just bench-ui ms per frame for layout and the Blend2D executor just hot-counter-child build the subprocess `just test`'s own host/child test spawns just example run examples/hello.odin just fuzz 30s of fuzzing just clean drop build/ and the four archives ``` `just install` bakes this checkout's path into the runner as the `jm` collection root; `ODIN_RUN_COLLECTION` overrides it. ## SQLite `sqlite3/vendor/` holds the SQLite **3.53.4** amalgamation (`sqlite3.c` and `sqlite3.h`, source id `bf7c7f30031888f4e796e429ab3978879485813aaca6f641c7b33e4e09459bcc`), taken from sqlite.org and verified against the SHA3-256 that page publishes. SQLite is public domain, so vendoring it carries no licence obligation. `just sqlite` compiles it once into `sqlite3/lib/sqlite3.a`, which is gitignored and rebuilt when the amalgamation changes. `foreign import` resolves that archive relative to the package directory. `odin check` never opens a foreign import, so `just check` still type-checks all three targets on one machine with no archive built. The compile options are sqlite.org's recommended set, with three deliberate departures, all of them in the justfile: - `SQLITE_THREADSAFE=1`, not the recommended `0`. `jm:flow` exists, and a connection per worker has to be safe. - `SQLITE_OMIT_AUTOINIT` is **not** set, though it is recommended. With it, any call made before `sqlite3_initialize` is a segfault rather than an error. - `SQLITE_ENABLE_FTS5` is added, for a full-text index, and `SQLITE_OMIT_LOAD_EXTENSION` keeps the link from needing libdl. One trap is worth knowing even though the package handles it: the bytes behind a text or blob column are freed on the next `step`, and SQLite reuses its own pool rather than returning them to libc, so reading a stale pointer yields the *next* row's data instead of crashing. AddressSanitizer cannot see it. That is why `text` and `blob` clone into the allocator the query was given. ## WebAssembly `wasm/vendor/` holds **wasm3 0.9.1** (commit `c036c43`), the interpreter's `source/` tree minus the two files this build does not compile: the uvwasi backend, which needs libuv, and the meta-WASI one, which is for running wasm3 inside wasm3. wasm3 is MIT, and `wasm/vendor/LICENSE` is its copy. `just wasm` compiles it once into `wasm/lib/wasm3.a`, which is gitignored and rebuilt when any vendored source changes. As with SQLite, `foreign import` resolves that archive relative to the package directory, and `odin check` never opens it, so `just check` still type-checks all three targets on one machine with no archive built. The build takes wasm3's defaults, which already have bytecode validation and gas metering on, and adds one option: `d_m3HasWASI`, without which `run` has no `_start` to call. That is an interpreter, not a JIT — several times slower than Wasmtime on arithmetic, and in exchange it is 550 KB of portable C with nothing to install and no executable pages to allocate at runtime. One limit is worth knowing before reaching for `jm:flow`: wasm3 is not thread-safe, and not merely per runtime. Eight threads, each with its own environment, runtime and copy of a two-instruction module, still produce spurious traps — a stack overflow, an out of bounds access — roughly five times in sixteen hundred calls. That was reproduced in C against this archive, with no Odin involved, so it is wasm3's own state and not the binding's. `jm:wasm` is therefore a one-thread package; `interrupt` is the only call meant to cross a thread, because it does nothing but set a flag. `just test` runs this package's tests with the test runner on a single thread for the same reason. `wasm/fuzz` is the suite: six properties over generated modules, damaged ones and bytes that were never a module, with a small Wasm encoder so a case needs no toolchain. `just fuzz "wasm -for=1m"` runs it; the section below records what it found. ### Speed `just bench` times jm:wasm against the four workloads in `tools/wasm-bench/workloads`, compiled from the C beside them by `just bench-build`. Each exports `run(i32) -> i32`: the argument scales the work linearly and the result is a checksum, so the same module can be run through another engine and checked. The harness reports the load, the first call — which is where wasm3 compiles the body — an empty call, and the workload, and subtracts `run(2n) - run(n)` so that only what scales with the work is left. On this machine: ``` module load first call work fib 14.5µs 2.8µs 138ns 219µs per unit mandel 10.5µs 7.4µs 118ns 481µs memsum 10.8µs 5.3µs 106ns 2.3ms sortbench 9.9µs 7.9µs 102ns 9.5ms ``` A module is instantiated in about ten microseconds and a call into it costs around a hundred nanoseconds, which is what an embedded interpreter is for. The work is another matter. The same four modules, at a fixed size — `run(4096)`, `run(2048)`, `run(512)` and `run(128)` — through every engine that could be made to run them, each one a command that loads the module, calls `run` once and exits. Milliseconds of work, fastest of three, lowest first; *first run* is what the whole process costs at `run(1)`: | engine | fib | mandel | memsum | sort | first run | |---|---|---|---|---|---| | wasmtime 49.0.1, Cranelift JIT | 84 | 178 | 52 | 158 | 2.9ms | | Node 26.8.1, V8 JIT | 93 | 316 | 80 | 309 | 15.6ms | | wazero 1.12.0, Go compiler | 125 | 193 | 130 | 380 | 2.0ms | | WAMR 2.4.5, default (JIT) | 160 | 370 | 71 | 301 | 18.7ms | | **jm:wasm** (wasm3 0.9.1) | 820 | 897 | 1200 | 1140 | 1.0ms | | wasm3 0.9.1, its own CLI | 920 | 937 | 1367 | 1236 | 1.1ms | | WAMR 2.4.5, `--interp` | 2643 | 6528 | 2806 | 6437 | 20.2ms | Three things are worth taking from it. The bindings cost nothing: the same modules through wasm3's own CLI come out a few percent *slower*, so what is measured above is the interpreter, not the crossing. Against a JIT the interpreter is five to ten times slower on arithmetic and twenty times on memory traffic, which is the price of the 550 KB of portable C and the one millisecond to first execution — the fastest start of anything measured here, where a JIT pays two to twenty milliseconds before it runs a thing. And among interpreters wasm3 holds up: WAMR's classic interpreter, the other embedded standard, is three to seven times slower than it on the same modules. WAMR's default mode is not an interpreter at all, which is what makes it look fast in the fourth row. Every engine returned the same checksum for every workload, which is what makes the columns comparable. The figures are the fastest of three runs on an otherwise idle machine and repeat to within about fifteen percent, so the orders of magnitude are the point and the last digit is not. "The bindings cost nothing" is the same measurement against a C program driving `wasm/lib/wasm3.a` directly: 800ms against 923 on fib, 1166 against 1153 on memsum — the two are inside each other's noise. ## PostgreSQL `pg_query/vendor/` holds **libpg_query** at commit `6e764b79` of the `17-latest` branch: PostgreSQL's own `gram.y` and everything it needs, lifted out of the server source tree, carrying the **17.7** grammar. Vendored is the `src/` tree, `protobuf/pg_query.pb-c.{c,h}`, the two third-party directories under `vendor/` and `srcdata/`, which is the schema the node types are generated from; left out are upstream's tests, generator scripts and the optional C++ protobuf path. libpg_query is BSD-3-Clause, and `pg_query/vendor/LICENSE` is its copy. `just pg_query` compiles it once into `pg_query/lib/pg_query.a` — 86 translation units, about twenty seconds, 5 MB — which is gitignored and rebuilt when any vendored source changes. As with SQLite and wasm3, `foreign import` resolves that archive relative to the package directory and `odin check` never opens it, so `just check` still type-checks all three targets on one machine with no archive built. The flags are upstream's Makefile exactly, less its `-g` and at `-O2` rather than `-O3`, and they are in the justfile. Two of them are correctness rather than taste: the PostgreSQL sources are written against `-fno-strict-aliasing` and `-fwrapv` and miscompile without them. The protobuf objects are compiled although this binding only wants the JSON API, which is not for want of trying: `pg_query_parse.c` defines `pg_query_parse_protobuf` beside `pg_query_parse`, so the object that holds the one entry point we call also references the protobuf writer, and an archive without it fails to link. Measured, then written down in the recipe. ### Typed nodes A parse tree comes back as the JSON libpg_query wrote *and* as typed Odin nodes, and the nodes are generated rather than written by hand. `vendor/srcdata/` is libpg_query's own schema — the input it generates its Go, Ruby and Python bindings from — and `pg_query/gen` turns the four sections that describe parse nodes into `pg_query/nodes.odin`: **267 structs, 63 enumerated types** and a tag-dispatched decoder. `just pg_query-gen` regenerates it; the file is checked in, so nothing normally runs the generator. 267 rather than the 474 in `nodetypes.json`, and the difference is not a subset taken for convenience. `struct_defs.json` describes nodes in sixteen sections; four of them — `nodes/parsenodes`, `nodes/primnodes`, `nodes/value` and `nodes/pg_list` — are what a *parse* tree can contain, and they hold 267 structs between them. The rest describe planner and executor nodes, which exist only in a tree the server has already analysed and which `pg_query_parse` never emits. The fuzz suite is what says so rather than this paragraph: an unknown tag fails a parse, and a run that meets one fails. Typing them is the point rather than a convenience. Dropping the schema would not remove it, only make it implicit: a field PostgreSQL renames in its next major would stop decoding silently, and a caller asking "does this statement write?" would be told no because the field it looked for was absent rather than because the statement was harmless — a failure that fails *open*. Generated from the schema, the same rename fails to compile, and `schema_conforms` in the tests fails before that: it loads the vendored `struct_defs.json` at compile time and holds every node and field the Odin side names against it. The decoder dispatches on the tag rather than trying variants in order. Every node arrives as a single-key object — `{"UpdateStmt": {…}}`, `{"BoolExpr": {…}}` — so a union matched structurally would match whichever variant was declared first and hand back the wrong node, silently. A tag this build has no struct for fails the parse, for the same reason a missing field would: a gate must not be handed a tree with a hole in it. Fields are plain and left at their zero value when absent, because libpg_query omits anything false, zero or empty; `"inh":true` is written and `"inh":false` is not. Two shapes are written by hand in the C rather than generated from the schema — `A_Const`, whose value arrives under `ival`, `fval`, `boolval`, `sval` or `bsval`, and the bare `RawStmt` at the top level — and both are special-cased in `decode.odin` and named in the generator. Two traps, both handled, both worth knowing: - Every `char *` in a result dies at its `pg_query_free_*`, which releases a whole memory context. A pointer held past that reads memory the next parse reuses — the `sqlite3_column_text` trap in another dialect — so everything the package hands back is cloned first. - `pg_query_exit` is the one entry point deliberately left unwrapped. `pg_query_init` registers a pthread destructor over the same top memory context, so a thread that calls it and then ends frees that context twice and the process aborts in glibc. One worker thread, one parse, no concurrency needed; reproduced in C against this archive. Let the thread end and the destructor does it. Parsing itself *is* thread-safe, unlike wasm3: the memory contexts are `__thread`, and eight threads over the same statements agree on every tree through 144,000 parses in C and 9,600 in `pg_query_test.odin`. Statements must be valid UTF-8, and the package refuses one that is not with the offset of the first bad byte. That is not tidiness — see what the fuzzer found, below. `normalize`'s output is for showing a human and not for re-parsing, for a reason recorded there too. `pg_query/fuzz` is the suite: eight properties over generated SQL, damaged SQL and bytes that were never SQL, with a SQL generator so a case needs no fixtures. `just fuzz "pg_query -for=1m"` runs it. ## libpq `jm:pq` talks to a PostgreSQL server over **libpq**, and it is the one C library in the collection that is not vendored: it links the system's `libpq.so.5` dynamically, and a built script needs it at runtime. Every other binding here vendors its C because that C builds with a plain `cc` and nothing else. libpq does not — it is a slice of the PostgreSQL tree with its own configure step — and it brings OpenSSL and GSSAPI with it for TLS and Kerberos, which a machine should keep patched on its own schedule rather than have frozen into every script. What makes the exception safe is libpq's record: `libpq.so.5` has kept its ABI since 2006 and is on every machine with a PostgreSQL client, psql included. Built and tested against libpq 18.6; Windows links `libpq.lib` and is untested, like every Windows build here. So there is no `just` recipe for it and nothing under `pq/lib`. `foreign import "system:pq"` is resolved by the linker, and `odin check` never opens a foreign import, so `just check` type-checks all three targets on a machine without libpq installed. Only building and testing need it. **Tests need a server, and bring their own.** `jm:pq/testdb` runs `initdb` into a fresh directory under the temp location and `pg_ctl start` on it, once per process: a Unix socket in that directory, `listen_addresses=''` so nothing listens on TCP, `trust` authentication behind a socket only this user can reach. It clears every `PG*` variable — a shell exporting `PGHOSTADDR` or `PGSERVICE` for a real database would otherwise send a test there — and sets `PGHOST`, `PGPORT`, `PGUSER` and `PGDATABASE` to the throwaway, which is also what `pq.connect("")` reads. Bringing it up takes about half a second. It leaves a shell loop behind that waits for the test process to be gone, however it went, and then stops the server and deletes the directory; an `@(fini)` would miss an `os.exit`, a failed assertion and a sanitizer abort. No Docker, and no server needs to exist beforehand: on this machine `initdb` and `pg_ctl` come with the `postgresql` package. Where they are not on `PATH`, every test that needs a server logs why and skips, and `jm-fuzz pq` says so and runs no cases, so the other suites keep running without PostgreSQL. `pq/fuzz` is the suite: six properties against that server, each case on its own connection as an unprivileged role with a `statement_timeout`. `just fuzz "pq -for=1m"` runs it. ## Fuzzing `jm:fuzz` runs properties against generated input and tells you the smallest case that broke one. A property is handed a subject and a `Source`, draws whatever input it wants, and says whether the promise held. The package does the rest: seeding, budgets, an arena per case, shrinking, the corpus, and a deadline on a case that will not finish. ``` just fuzz 30 seconds of every suite just fuzz "tar -for=5m" one suite, longer just fuzz "sqlite3 -seed=12345" replay a reported seed exactly just fuzz "-corpus=DIR" keep failures somewhere else just fuzz-isolate five minutes, a child process per case just fuzz-asan the same under AddressSanitizer ``` Each suite keeps its regressions beside its source, in `/corpus`, and replays them before generating anything. `just test` runs a few hundred cases of each suite on fixed seeds and replays those corpora, so a bug found once by fuzzing stays found without anyone running the fuzzer again. A case's randomness is **a finite byte string**, and every generator draws from it. That one decision is what the rest rests on: a case is a pure function of its bytes, so it replays exactly, it can be written to disk as a regression, and it can be shrunk by simplifying the bytes and running it again. Generators are written so a zero byte asks for the simplest value they can give, which is what shrinking converges on. Writing a suite means naming a subject, how to make and unmake one, how to cancel work in flight, and the promises: ```odin properties := []fuzz.Property(Sandbox){{"no_escape", no_escape}} suite :: proc() -> fuzz.Suite(Sandbox) { return fuzz.Suite(Sandbox) { name = "tar", setup = open, teardown = shut, properties = properties, } } ``` Three things in it were each put there by something that went wrong: - **Shrinking**, because a failure arrived as an 80-byte blob when one byte was enough. A found case shrinks to its simplest form before it is reported. - **A corpus**, because a bug found once should be a test from then on. With `-corpus`, the case about to run is also written out before it runs, so a property that takes the process down — a panic, a failed bounds check, anything the harness cannot catch — leaves the input that did it on disk. That is how the `jm:tar` crash below was captured. - **A deadline**, because nothing in SQLite bounds how long a statement runs and a damaged recursive CTE returns rows for ever. In this process a suite has to supply `cancel` for that to work; `-isolate` lifts the restriction, below. ### Isolation `-isolate` runs each case in a child process. The child is the same binary, re-run with the case named in its environment, so a program that calls `fuzz.run` is its own child with no extra wiring. It costs a spawn per case. Measured with `just fuzz "tar -for=5s -no-corpus"` against the same run with `-isolate`, this machine does about 16,200 cases a second in process and 1,070 isolated — fifteen times slower — which is why it is off by default. What it buys: - **A crash is a result, not the end of the run.** A panic or a failed bounds check comes back as `Crashed`, carrying whatever the child wrote to stderr, and the next case starts from a clean process. - **The deadline works for any subject.** A child is killed whether or not it cooperates, so a suite over a parser with no interruption point — `jm:tar`, whose loop has nothing to check — gets a deadline for free. No `cancel` needed. - **Shrinking still applies**, so a crash shrinks to its smallest case like any other failure. Use it for unattended runs. In process is the right default for the quick pass that `just test` and a 30-second `just fuzz` do. One trap the sqlite3 suite exists to catch: its properties read every row before comparing any of them. Comparing inside the loop passes even when a column read hands back SQLite's memory instead of a copy, because the bytes have not been reused yet. Deleting the clone from `sqlite3.text` leaves the example-based tests green and fails one case in three here. ### What it found Wiring `jm:tar` up as the second suite turned up three ways to crash the parser on a malformed archive, all now fixed, all with regression tests in `tar/tar_test.odin`: - A size field in the base-256 form can name a number larger than an `int` holds. The shift wrapped, the size came back negative, and `read` sliced the archive backwards. - A pax record whose claimed length did not reach past its own length field made `read` slice backwards too. `"1 "` was enough. - Guarding the size field was not enough on its own: `read` then adds the header's offset to it, and a size of `max(int)` made *that* wrap. The check has to be a subtraction. This one is also `tar/fuzz/corpus`'s first entry: the shape needs a dozen specific bytes, so the corpus rather than the fuzzer is what keeps it tested. `jm:wasm` was the third suite, and its `bounds` property found the same bug in a different package on its first run. `wasm.bytes` is what stands between a guest's chosen pointer and the rest of the process; it checked the range by adding `ptr + size`, so a size near the top of `int` wrapped the sum back down into the memory, the check passed, and the slice that followed had a negative length. `wasm.bytes(mod, 8, max(int))` was enough. It subtracts now — what is left of the memory after `ptr`, which cannot wrap — and `wasm_test.odin` keeps the case. Two packages, written days apart, got the same arithmetic wrong in the same place; a property that draws pointers at the edges finds it in seconds, and no example-based test here had. `jm:pg_query` was the fourth suite, and it found two things in its first runs. `error_sane` bounded `cursorpos` by the length of the statement; a statement cut short faults at *one past* the end, which is how PostgreSQL says "at end of input". That one was the property being wrong, and it is written down in the package now rather than left to be rediscovered. The second was real. `split_covers` took the process down on raw bytes after ten thousand cases, inside `core:encoding/json`. libpg_query sets the scanner's encoding to UTF-8 but does not validate its input against it, so `SELECT '<0xff><0xfe>' FROM t` parses and those bytes are copied into the parse tree verbatim — making the tree JSON that is not UTF-8. Decoding that walks `unquote_string` off the end of its buffer, because an invalid byte decodes as one byte and re-encodes as the three of U+FFFD. `jm:pg_query` now refuses a statement that is not UTF-8 before the parser sees it, which is what a PostgreSQL server with a UTF-8 database does anyway. A third, under the sanitizer, is upstream's rather than ours and is written down rather than fixed: `normalize` substitutes a parameter over the literal's recorded extent without checking that a token boundary survives. A leading minus belongs to the constant, so `SELECT-1` comes back as `SELECT$1` — one identifier, not a statement. `normalize_reparses` skips that shape and names it; `pg_query_test.odin` keeps the case, and the package doc tells a caller not to re-parse what normalize writes. `jm:pq` was the fifth suite. Its first run failed `error_sane` on every COPY: the binding refused `COPY … TO STDOUT` with no SQLSTATE, the mark of a refusal that never reached the server — but by then the server had started the COPY. It now carries `0A000`, feature_not_supported, which is both true and something a caller branching on SQLSTATE can match. Nothing else turned up in 100,000 cases under the sanitizer. The sanitizer taught something about itself while the tests were written. `values_outlive_the_connection` closes the connection and then reads every string the binding handed back, so that a string still pointing into a freed `PGresult` traps. With the clone deliberately removed it passed anyway: the comparison runs in `base:runtime`, which is not instrumented, so ASan never saw the read. The test now walks every byte in its own code first, and with the clone removed it fails with a heap-use-after-free, as it should. ## UI `jm:ui` is an immediate-mode user interface whose frame is data. A ui proc records scene ops, `flatten` turns them into a draw list and a hit list, and `ui/render` executes the draw list on Blend2D while the router hit-tests the hit list. Every stage is an array with a text dump, so a frame can be asserted on, serialized (`encode`) for a renderer in another process, or driven by `ui.Probe` with no window at all: ``` build/debug/ui-kitchen -dump the scene as text build/debug/ui-kitchen -click Save -names click by tag, list what is on screen build/debug/ui-kitchen -click name -type Ada -png out.png ``` Widgets nest through containers with no per-child boilerplate: ```odin col := ui.column(gtx, gap = 8); defer ui.end(&col) ui.label(gtx, "Name") ui.text_field(gtx, &m.name) if ui.button(gtx, "Save") { save(m) } ``` A zero field in a style struct takes the theme's value. Clipping is exact for any shape under any affine: Blend2D clips only to rectangles, so a path or rotated clip renders through an A8 mask. The Blend2D binding is copied from `odin-blend2d`; `just blend2d` builds its archive from that checkout's source (`BLEND2D_SRC`), and anything linking it needs `-lstdc++`. `examples/ui-kitchen` is the demo, `just kitchen` opens it. That "serialized for a renderer in another process" is `ui/sdl.run_host`: a host owns the window and renders, a subprocess (`ui/child`) owns the Model, the ui proc, `Router` and `Layout`, and the two talk `ui/wire`'s Input/Reply over `ui/ipc`'s pipes. `Host_App.watch` names a pointer file a builder republishes on every successful build (`tools/hot-watch` is one); the host re-reads it and respawns the child on a change, never overwriting a running executable in place, which Windows refuses. `examples/hot-counter` (a two-binary click counter), `examples/hot-architecture` (a live-editable diagram of this very pipeline) and `examples/hot-button` (every M3 button kind — filled, tonal, outlined, text, elevated, icon, FAB and its sizes, extended FAB, segmented, split) and `examples/material-kitchen` (every `ui/material` component, a page each, in all its spec states) are the demos; `just hot-architecture` and `just hot-button` build their own host and print the two commands that run them, and `just material-kitchen` builds and opens its own. `tools/hot-watch` takes extra directories to watch after the pointer file, so a child rebuilds when the `ui` package it imports is edited too. Checking a change without a window: `-dump` prints the scene as text, free of vision tokens and enough for most bugs; `-png` renders it to a PNG when the question is actually about pixels; `tools/img-diff` (over `ui/render`'s `diff_files`) turns two PNGs into a list of changed regions as text, so confirming an edit changed what it should needs looking at neither.