Files
2026-07-13 12:29:49 +08:00

1580 lines
79 KiB
Zig

//! The app-core runtime kernel ("rt"): the fixed, hand-written module the
//! transpiler's output links against. Two memory regions, no GC, no
//! free-lists, no headers:
//!
//! frame arena — bump allocator, holds every transient the update builds
//! (spread results, map/filter outputs, string builds).
//! Reset wholesale after each dispatch commits.
//! model heap — two-space bump allocator, holds the committed model
//! graph. Commit copies only frame-resident nodes in
//! (O(new nodes)); replaced nodes leak until a watermark
//! triggers a full copying compaction into the other space
//! (O(live model), deterministic, at a dispatch boundary).
//!
//! Address-range checks classify pointers: in-frame → must copy at commit;
//! in-heap or static/rodata → persists, share as-is.
//!
//! Capacities are comptime parameters: the emitted core instantiates
//! `Kernel(.{ .frame_cap = ..., .heap_cap = ... })` from the app's transpile
//! options (`--frame-cap` / `--heap-cap` on the CLI) and re-exports it as
//! `core.rt`. Both regions are FIXED at build time — v1 never grows them at
//! runtime, so binaries stay allocation-free and replay stays trivially
//! deterministic (a growth event could shift compaction cadence and address
//! ranges mid-run). Overflow is a loud teaching panic naming the knob.
//! Instantiations with equal options memoize to one shared kernel, so a
//! hand-written harness importing `core.rt` and the emitted core always see
//! the same arenas.
//!
//! The command builders at the bottom encode `Cmd` effect values (the
//! versioned wire format under `cmd_format_version`) into the frame arena;
//! hosts consume them after commit, before the frame resets.
const std = @import("std");
// ------------------------------------------------------------- byte text
//
// The everyday string-method surface on core text (`Uint8Array` bytes):
// toUpperCase/toLowerCase (Unicode SIMPLE case mapping — the tables below),
// repeat, startsWith/endsWith/includes, padStart/padEnd, trim family,
// indexOf/lastIndexOf, split, at. Byte-honest semantics throughout: lengths
// and offsets are BYTE lengths and offsets, search is byte-wise, and the
// node devhost polyfills the same methods from the same generated tables
// (src/text_polyfill.ts + src/text_tables.ts), so node and native produce
// identical bytes by construction.
//
// Case mapping is Unicode SIMPLE case mapping (UnicodeData.txt fields
// 12/13): code point -> code point, locale-independent, no special casing
// (ß stays ß, no Turkish dotless-i) — full/locale casing would need
// SpecialCasing tables and ambient locale state whose results differ
// across runtimes. UTF-8 in, UTF-8 out; bytes that are not well-formed
// UTF-8 pass through unchanged, one byte at a time.
/// One compressed run of the simple case-mapping table: `count` mapped code
/// points at lo, lo+stride, ...; each maps to itself + delta. stride2 marks
/// the alternating upper/lower pairs (U+0100 Ā / U+0101 ā). 8 bytes/range.
const CaseRange = packed struct(u64) {
lo: u21,
count: u16,
stride2: bool,
delta: i26,
};
// ---- BEGIN GENERATED CASE TABLES ----
// Unicode 17.0.0 simple case mappings (UnicodeData.txt fields 12/13).
// GENERATED by scripts/gen_case_tables.mjs — do not edit by hand; regenerate with:
// node scripts/gen_case_tables.mjs [path/to/UnicodeData.txt]
// 204 + 186 ranges, 8 bytes each = 3120 bytes of table data.
const simple_upper_ranges = [_]CaseRange{
.{ .lo = 0x61, .count = 26, .stride2 = false, .delta = -32 },
.{ .lo = 0xB5, .count = 1, .stride2 = false, .delta = 743 },
.{ .lo = 0xE0, .count = 23, .stride2 = false, .delta = -32 },
.{ .lo = 0xF8, .count = 7, .stride2 = false, .delta = -32 },
.{ .lo = 0xFF, .count = 1, .stride2 = false, .delta = 121 },
.{ .lo = 0x101, .count = 24, .stride2 = true, .delta = -1 },
.{ .lo = 0x131, .count = 1, .stride2 = false, .delta = -232 },
.{ .lo = 0x133, .count = 3, .stride2 = true, .delta = -1 },
.{ .lo = 0x13A, .count = 8, .stride2 = true, .delta = -1 },
.{ .lo = 0x14B, .count = 23, .stride2 = true, .delta = -1 },
.{ .lo = 0x17A, .count = 3, .stride2 = true, .delta = -1 },
.{ .lo = 0x17F, .count = 1, .stride2 = false, .delta = -300 },
.{ .lo = 0x180, .count = 1, .stride2 = false, .delta = 195 },
.{ .lo = 0x183, .count = 2, .stride2 = true, .delta = -1 },
.{ .lo = 0x188, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x18C, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x192, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x195, .count = 1, .stride2 = false, .delta = 97 },
.{ .lo = 0x199, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x19A, .count = 1, .stride2 = false, .delta = 163 },
.{ .lo = 0x19B, .count = 1, .stride2 = false, .delta = 42561 },
.{ .lo = 0x19E, .count = 1, .stride2 = false, .delta = 130 },
.{ .lo = 0x1A1, .count = 3, .stride2 = true, .delta = -1 },
.{ .lo = 0x1A8, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1AD, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1B0, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1B4, .count = 2, .stride2 = true, .delta = -1 },
.{ .lo = 0x1B9, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1BD, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1BF, .count = 1, .stride2 = false, .delta = 56 },
.{ .lo = 0x1C5, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1C6, .count = 1, .stride2 = false, .delta = -2 },
.{ .lo = 0x1C8, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1C9, .count = 1, .stride2 = false, .delta = -2 },
.{ .lo = 0x1CB, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1CC, .count = 1, .stride2 = false, .delta = -2 },
.{ .lo = 0x1CE, .count = 8, .stride2 = true, .delta = -1 },
.{ .lo = 0x1DD, .count = 1, .stride2 = false, .delta = -79 },
.{ .lo = 0x1DF, .count = 9, .stride2 = true, .delta = -1 },
.{ .lo = 0x1F2, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1F3, .count = 1, .stride2 = false, .delta = -2 },
.{ .lo = 0x1F5, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1F9, .count = 20, .stride2 = true, .delta = -1 },
.{ .lo = 0x223, .count = 9, .stride2 = true, .delta = -1 },
.{ .lo = 0x23C, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x23F, .count = 2, .stride2 = false, .delta = 10815 },
.{ .lo = 0x242, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x247, .count = 5, .stride2 = true, .delta = -1 },
.{ .lo = 0x250, .count = 1, .stride2 = false, .delta = 10783 },
.{ .lo = 0x251, .count = 1, .stride2 = false, .delta = 10780 },
.{ .lo = 0x252, .count = 1, .stride2 = false, .delta = 10782 },
.{ .lo = 0x253, .count = 1, .stride2 = false, .delta = -210 },
.{ .lo = 0x254, .count = 1, .stride2 = false, .delta = -206 },
.{ .lo = 0x256, .count = 2, .stride2 = false, .delta = -205 },
.{ .lo = 0x259, .count = 1, .stride2 = false, .delta = -202 },
.{ .lo = 0x25B, .count = 1, .stride2 = false, .delta = -203 },
.{ .lo = 0x25C, .count = 1, .stride2 = false, .delta = 42319 },
.{ .lo = 0x260, .count = 1, .stride2 = false, .delta = -205 },
.{ .lo = 0x261, .count = 1, .stride2 = false, .delta = 42315 },
.{ .lo = 0x263, .count = 1, .stride2 = false, .delta = -207 },
.{ .lo = 0x264, .count = 1, .stride2 = false, .delta = 42343 },
.{ .lo = 0x265, .count = 1, .stride2 = false, .delta = 42280 },
.{ .lo = 0x266, .count = 1, .stride2 = false, .delta = 42308 },
.{ .lo = 0x268, .count = 1, .stride2 = false, .delta = -209 },
.{ .lo = 0x269, .count = 1, .stride2 = false, .delta = -211 },
.{ .lo = 0x26A, .count = 1, .stride2 = false, .delta = 42308 },
.{ .lo = 0x26B, .count = 1, .stride2 = false, .delta = 10743 },
.{ .lo = 0x26C, .count = 1, .stride2 = false, .delta = 42305 },
.{ .lo = 0x26F, .count = 1, .stride2 = false, .delta = -211 },
.{ .lo = 0x271, .count = 1, .stride2 = false, .delta = 10749 },
.{ .lo = 0x272, .count = 1, .stride2 = false, .delta = -213 },
.{ .lo = 0x275, .count = 1, .stride2 = false, .delta = -214 },
.{ .lo = 0x27D, .count = 1, .stride2 = false, .delta = 10727 },
.{ .lo = 0x280, .count = 1, .stride2 = false, .delta = -218 },
.{ .lo = 0x282, .count = 1, .stride2 = false, .delta = 42307 },
.{ .lo = 0x283, .count = 1, .stride2 = false, .delta = -218 },
.{ .lo = 0x287, .count = 1, .stride2 = false, .delta = 42282 },
.{ .lo = 0x288, .count = 1, .stride2 = false, .delta = -218 },
.{ .lo = 0x289, .count = 1, .stride2 = false, .delta = -69 },
.{ .lo = 0x28A, .count = 2, .stride2 = false, .delta = -217 },
.{ .lo = 0x28C, .count = 1, .stride2 = false, .delta = -71 },
.{ .lo = 0x292, .count = 1, .stride2 = false, .delta = -219 },
.{ .lo = 0x29D, .count = 1, .stride2 = false, .delta = 42261 },
.{ .lo = 0x29E, .count = 1, .stride2 = false, .delta = 42258 },
.{ .lo = 0x345, .count = 1, .stride2 = false, .delta = 84 },
.{ .lo = 0x371, .count = 2, .stride2 = true, .delta = -1 },
.{ .lo = 0x377, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x37B, .count = 3, .stride2 = false, .delta = 130 },
.{ .lo = 0x3AC, .count = 1, .stride2 = false, .delta = -38 },
.{ .lo = 0x3AD, .count = 3, .stride2 = false, .delta = -37 },
.{ .lo = 0x3B1, .count = 17, .stride2 = false, .delta = -32 },
.{ .lo = 0x3C2, .count = 1, .stride2 = false, .delta = -31 },
.{ .lo = 0x3C3, .count = 9, .stride2 = false, .delta = -32 },
.{ .lo = 0x3CC, .count = 1, .stride2 = false, .delta = -64 },
.{ .lo = 0x3CD, .count = 2, .stride2 = false, .delta = -63 },
.{ .lo = 0x3D0, .count = 1, .stride2 = false, .delta = -62 },
.{ .lo = 0x3D1, .count = 1, .stride2 = false, .delta = -57 },
.{ .lo = 0x3D5, .count = 1, .stride2 = false, .delta = -47 },
.{ .lo = 0x3D6, .count = 1, .stride2 = false, .delta = -54 },
.{ .lo = 0x3D7, .count = 1, .stride2 = false, .delta = -8 },
.{ .lo = 0x3D9, .count = 12, .stride2 = true, .delta = -1 },
.{ .lo = 0x3F0, .count = 1, .stride2 = false, .delta = -86 },
.{ .lo = 0x3F1, .count = 1, .stride2 = false, .delta = -80 },
.{ .lo = 0x3F2, .count = 1, .stride2 = false, .delta = 7 },
.{ .lo = 0x3F3, .count = 1, .stride2 = false, .delta = -116 },
.{ .lo = 0x3F5, .count = 1, .stride2 = false, .delta = -96 },
.{ .lo = 0x3F8, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x3FB, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x430, .count = 32, .stride2 = false, .delta = -32 },
.{ .lo = 0x450, .count = 16, .stride2 = false, .delta = -80 },
.{ .lo = 0x461, .count = 17, .stride2 = true, .delta = -1 },
.{ .lo = 0x48B, .count = 27, .stride2 = true, .delta = -1 },
.{ .lo = 0x4C2, .count = 7, .stride2 = true, .delta = -1 },
.{ .lo = 0x4CF, .count = 1, .stride2 = false, .delta = -15 },
.{ .lo = 0x4D1, .count = 48, .stride2 = true, .delta = -1 },
.{ .lo = 0x561, .count = 38, .stride2 = false, .delta = -48 },
.{ .lo = 0x10D0, .count = 43, .stride2 = false, .delta = 3008 },
.{ .lo = 0x10FD, .count = 3, .stride2 = false, .delta = 3008 },
.{ .lo = 0x13F8, .count = 6, .stride2 = false, .delta = -8 },
.{ .lo = 0x1C80, .count = 1, .stride2 = false, .delta = -6254 },
.{ .lo = 0x1C81, .count = 1, .stride2 = false, .delta = -6253 },
.{ .lo = 0x1C82, .count = 1, .stride2 = false, .delta = -6244 },
.{ .lo = 0x1C83, .count = 2, .stride2 = false, .delta = -6242 },
.{ .lo = 0x1C85, .count = 1, .stride2 = false, .delta = -6243 },
.{ .lo = 0x1C86, .count = 1, .stride2 = false, .delta = -6236 },
.{ .lo = 0x1C87, .count = 1, .stride2 = false, .delta = -6181 },
.{ .lo = 0x1C88, .count = 1, .stride2 = false, .delta = 35266 },
.{ .lo = 0x1C8A, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x1D79, .count = 1, .stride2 = false, .delta = 35332 },
.{ .lo = 0x1D7D, .count = 1, .stride2 = false, .delta = 3814 },
.{ .lo = 0x1D8E, .count = 1, .stride2 = false, .delta = 35384 },
.{ .lo = 0x1E01, .count = 75, .stride2 = true, .delta = -1 },
.{ .lo = 0x1E9B, .count = 1, .stride2 = false, .delta = -59 },
.{ .lo = 0x1EA1, .count = 48, .stride2 = true, .delta = -1 },
.{ .lo = 0x1F00, .count = 8, .stride2 = false, .delta = 8 },
.{ .lo = 0x1F10, .count = 6, .stride2 = false, .delta = 8 },
.{ .lo = 0x1F20, .count = 8, .stride2 = false, .delta = 8 },
.{ .lo = 0x1F30, .count = 8, .stride2 = false, .delta = 8 },
.{ .lo = 0x1F40, .count = 6, .stride2 = false, .delta = 8 },
.{ .lo = 0x1F51, .count = 4, .stride2 = true, .delta = 8 },
.{ .lo = 0x1F60, .count = 8, .stride2 = false, .delta = 8 },
.{ .lo = 0x1F70, .count = 2, .stride2 = false, .delta = 74 },
.{ .lo = 0x1F72, .count = 4, .stride2 = false, .delta = 86 },
.{ .lo = 0x1F76, .count = 2, .stride2 = false, .delta = 100 },
.{ .lo = 0x1F78, .count = 2, .stride2 = false, .delta = 128 },
.{ .lo = 0x1F7A, .count = 2, .stride2 = false, .delta = 112 },
.{ .lo = 0x1F7C, .count = 2, .stride2 = false, .delta = 126 },
.{ .lo = 0x1F80, .count = 8, .stride2 = false, .delta = 8 },
.{ .lo = 0x1F90, .count = 8, .stride2 = false, .delta = 8 },
.{ .lo = 0x1FA0, .count = 8, .stride2 = false, .delta = 8 },
.{ .lo = 0x1FB0, .count = 2, .stride2 = false, .delta = 8 },
.{ .lo = 0x1FB3, .count = 1, .stride2 = false, .delta = 9 },
.{ .lo = 0x1FBE, .count = 1, .stride2 = false, .delta = -7205 },
.{ .lo = 0x1FC3, .count = 1, .stride2 = false, .delta = 9 },
.{ .lo = 0x1FD0, .count = 2, .stride2 = false, .delta = 8 },
.{ .lo = 0x1FE0, .count = 2, .stride2 = false, .delta = 8 },
.{ .lo = 0x1FE5, .count = 1, .stride2 = false, .delta = 7 },
.{ .lo = 0x1FF3, .count = 1, .stride2 = false, .delta = 9 },
.{ .lo = 0x214E, .count = 1, .stride2 = false, .delta = -28 },
.{ .lo = 0x2170, .count = 16, .stride2 = false, .delta = -16 },
.{ .lo = 0x2184, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x24D0, .count = 26, .stride2 = false, .delta = -26 },
.{ .lo = 0x2C30, .count = 48, .stride2 = false, .delta = -48 },
.{ .lo = 0x2C61, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x2C65, .count = 1, .stride2 = false, .delta = -10795 },
.{ .lo = 0x2C66, .count = 1, .stride2 = false, .delta = -10792 },
.{ .lo = 0x2C68, .count = 3, .stride2 = true, .delta = -1 },
.{ .lo = 0x2C73, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x2C76, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x2C81, .count = 50, .stride2 = true, .delta = -1 },
.{ .lo = 0x2CEC, .count = 2, .stride2 = true, .delta = -1 },
.{ .lo = 0x2CF3, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0x2D00, .count = 38, .stride2 = false, .delta = -7264 },
.{ .lo = 0x2D27, .count = 1, .stride2 = false, .delta = -7264 },
.{ .lo = 0x2D2D, .count = 1, .stride2 = false, .delta = -7264 },
.{ .lo = 0xA641, .count = 23, .stride2 = true, .delta = -1 },
.{ .lo = 0xA681, .count = 14, .stride2 = true, .delta = -1 },
.{ .lo = 0xA723, .count = 7, .stride2 = true, .delta = -1 },
.{ .lo = 0xA733, .count = 31, .stride2 = true, .delta = -1 },
.{ .lo = 0xA77A, .count = 2, .stride2 = true, .delta = -1 },
.{ .lo = 0xA77F, .count = 5, .stride2 = true, .delta = -1 },
.{ .lo = 0xA78C, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0xA791, .count = 2, .stride2 = true, .delta = -1 },
.{ .lo = 0xA794, .count = 1, .stride2 = false, .delta = 48 },
.{ .lo = 0xA797, .count = 10, .stride2 = true, .delta = -1 },
.{ .lo = 0xA7B5, .count = 8, .stride2 = true, .delta = -1 },
.{ .lo = 0xA7C8, .count = 2, .stride2 = true, .delta = -1 },
.{ .lo = 0xA7CD, .count = 8, .stride2 = true, .delta = -1 },
.{ .lo = 0xA7F6, .count = 1, .stride2 = false, .delta = -1 },
.{ .lo = 0xAB53, .count = 1, .stride2 = false, .delta = -928 },
.{ .lo = 0xAB70, .count = 80, .stride2 = false, .delta = -38864 },
.{ .lo = 0xFF41, .count = 26, .stride2 = false, .delta = -32 },
.{ .lo = 0x10428, .count = 40, .stride2 = false, .delta = -40 },
.{ .lo = 0x104D8, .count = 36, .stride2 = false, .delta = -40 },
.{ .lo = 0x10597, .count = 11, .stride2 = false, .delta = -39 },
.{ .lo = 0x105A3, .count = 15, .stride2 = false, .delta = -39 },
.{ .lo = 0x105B3, .count = 7, .stride2 = false, .delta = -39 },
.{ .lo = 0x105BB, .count = 2, .stride2 = false, .delta = -39 },
.{ .lo = 0x10CC0, .count = 51, .stride2 = false, .delta = -64 },
.{ .lo = 0x10D70, .count = 22, .stride2 = false, .delta = -32 },
.{ .lo = 0x118C0, .count = 32, .stride2 = false, .delta = -32 },
.{ .lo = 0x16E60, .count = 32, .stride2 = false, .delta = -32 },
.{ .lo = 0x16EBB, .count = 25, .stride2 = false, .delta = -27 },
.{ .lo = 0x1E922, .count = 34, .stride2 = false, .delta = -34 },
};
const simple_lower_ranges = [_]CaseRange{
.{ .lo = 0x41, .count = 26, .stride2 = false, .delta = 32 },
.{ .lo = 0xC0, .count = 23, .stride2 = false, .delta = 32 },
.{ .lo = 0xD8, .count = 7, .stride2 = false, .delta = 32 },
.{ .lo = 0x100, .count = 24, .stride2 = true, .delta = 1 },
.{ .lo = 0x130, .count = 1, .stride2 = false, .delta = -199 },
.{ .lo = 0x132, .count = 3, .stride2 = true, .delta = 1 },
.{ .lo = 0x139, .count = 8, .stride2 = true, .delta = 1 },
.{ .lo = 0x14A, .count = 23, .stride2 = true, .delta = 1 },
.{ .lo = 0x178, .count = 1, .stride2 = false, .delta = -121 },
.{ .lo = 0x179, .count = 3, .stride2 = true, .delta = 1 },
.{ .lo = 0x181, .count = 1, .stride2 = false, .delta = 210 },
.{ .lo = 0x182, .count = 2, .stride2 = true, .delta = 1 },
.{ .lo = 0x186, .count = 1, .stride2 = false, .delta = 206 },
.{ .lo = 0x187, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x189, .count = 2, .stride2 = false, .delta = 205 },
.{ .lo = 0x18B, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x18E, .count = 1, .stride2 = false, .delta = 79 },
.{ .lo = 0x18F, .count = 1, .stride2 = false, .delta = 202 },
.{ .lo = 0x190, .count = 1, .stride2 = false, .delta = 203 },
.{ .lo = 0x191, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x193, .count = 1, .stride2 = false, .delta = 205 },
.{ .lo = 0x194, .count = 1, .stride2 = false, .delta = 207 },
.{ .lo = 0x196, .count = 1, .stride2 = false, .delta = 211 },
.{ .lo = 0x197, .count = 1, .stride2 = false, .delta = 209 },
.{ .lo = 0x198, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x19C, .count = 1, .stride2 = false, .delta = 211 },
.{ .lo = 0x19D, .count = 1, .stride2 = false, .delta = 213 },
.{ .lo = 0x19F, .count = 1, .stride2 = false, .delta = 214 },
.{ .lo = 0x1A0, .count = 3, .stride2 = true, .delta = 1 },
.{ .lo = 0x1A6, .count = 1, .stride2 = false, .delta = 218 },
.{ .lo = 0x1A7, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x1A9, .count = 1, .stride2 = false, .delta = 218 },
.{ .lo = 0x1AC, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x1AE, .count = 1, .stride2 = false, .delta = 218 },
.{ .lo = 0x1AF, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x1B1, .count = 2, .stride2 = false, .delta = 217 },
.{ .lo = 0x1B3, .count = 2, .stride2 = true, .delta = 1 },
.{ .lo = 0x1B7, .count = 1, .stride2 = false, .delta = 219 },
.{ .lo = 0x1B8, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x1BC, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x1C4, .count = 1, .stride2 = false, .delta = 2 },
.{ .lo = 0x1C5, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x1C7, .count = 1, .stride2 = false, .delta = 2 },
.{ .lo = 0x1C8, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x1CA, .count = 1, .stride2 = false, .delta = 2 },
.{ .lo = 0x1CB, .count = 9, .stride2 = true, .delta = 1 },
.{ .lo = 0x1DE, .count = 9, .stride2 = true, .delta = 1 },
.{ .lo = 0x1F1, .count = 1, .stride2 = false, .delta = 2 },
.{ .lo = 0x1F2, .count = 2, .stride2 = true, .delta = 1 },
.{ .lo = 0x1F6, .count = 1, .stride2 = false, .delta = -97 },
.{ .lo = 0x1F7, .count = 1, .stride2 = false, .delta = -56 },
.{ .lo = 0x1F8, .count = 20, .stride2 = true, .delta = 1 },
.{ .lo = 0x220, .count = 1, .stride2 = false, .delta = -130 },
.{ .lo = 0x222, .count = 9, .stride2 = true, .delta = 1 },
.{ .lo = 0x23A, .count = 1, .stride2 = false, .delta = 10795 },
.{ .lo = 0x23B, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x23D, .count = 1, .stride2 = false, .delta = -163 },
.{ .lo = 0x23E, .count = 1, .stride2 = false, .delta = 10792 },
.{ .lo = 0x241, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x243, .count = 1, .stride2 = false, .delta = -195 },
.{ .lo = 0x244, .count = 1, .stride2 = false, .delta = 69 },
.{ .lo = 0x245, .count = 1, .stride2 = false, .delta = 71 },
.{ .lo = 0x246, .count = 5, .stride2 = true, .delta = 1 },
.{ .lo = 0x370, .count = 2, .stride2 = true, .delta = 1 },
.{ .lo = 0x376, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x37F, .count = 1, .stride2 = false, .delta = 116 },
.{ .lo = 0x386, .count = 1, .stride2 = false, .delta = 38 },
.{ .lo = 0x388, .count = 3, .stride2 = false, .delta = 37 },
.{ .lo = 0x38C, .count = 1, .stride2 = false, .delta = 64 },
.{ .lo = 0x38E, .count = 2, .stride2 = false, .delta = 63 },
.{ .lo = 0x391, .count = 17, .stride2 = false, .delta = 32 },
.{ .lo = 0x3A3, .count = 9, .stride2 = false, .delta = 32 },
.{ .lo = 0x3CF, .count = 1, .stride2 = false, .delta = 8 },
.{ .lo = 0x3D8, .count = 12, .stride2 = true, .delta = 1 },
.{ .lo = 0x3F4, .count = 1, .stride2 = false, .delta = -60 },
.{ .lo = 0x3F7, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x3F9, .count = 1, .stride2 = false, .delta = -7 },
.{ .lo = 0x3FA, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x3FD, .count = 3, .stride2 = false, .delta = -130 },
.{ .lo = 0x400, .count = 16, .stride2 = false, .delta = 80 },
.{ .lo = 0x410, .count = 32, .stride2 = false, .delta = 32 },
.{ .lo = 0x460, .count = 17, .stride2 = true, .delta = 1 },
.{ .lo = 0x48A, .count = 27, .stride2 = true, .delta = 1 },
.{ .lo = 0x4C0, .count = 1, .stride2 = false, .delta = 15 },
.{ .lo = 0x4C1, .count = 7, .stride2 = true, .delta = 1 },
.{ .lo = 0x4D0, .count = 48, .stride2 = true, .delta = 1 },
.{ .lo = 0x531, .count = 38, .stride2 = false, .delta = 48 },
.{ .lo = 0x10A0, .count = 38, .stride2 = false, .delta = 7264 },
.{ .lo = 0x10C7, .count = 1, .stride2 = false, .delta = 7264 },
.{ .lo = 0x10CD, .count = 1, .stride2 = false, .delta = 7264 },
.{ .lo = 0x13A0, .count = 80, .stride2 = false, .delta = 38864 },
.{ .lo = 0x13F0, .count = 6, .stride2 = false, .delta = 8 },
.{ .lo = 0x1C89, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x1C90, .count = 43, .stride2 = false, .delta = -3008 },
.{ .lo = 0x1CBD, .count = 3, .stride2 = false, .delta = -3008 },
.{ .lo = 0x1E00, .count = 75, .stride2 = true, .delta = 1 },
.{ .lo = 0x1E9E, .count = 1, .stride2 = false, .delta = -7615 },
.{ .lo = 0x1EA0, .count = 48, .stride2 = true, .delta = 1 },
.{ .lo = 0x1F08, .count = 8, .stride2 = false, .delta = -8 },
.{ .lo = 0x1F18, .count = 6, .stride2 = false, .delta = -8 },
.{ .lo = 0x1F28, .count = 8, .stride2 = false, .delta = -8 },
.{ .lo = 0x1F38, .count = 8, .stride2 = false, .delta = -8 },
.{ .lo = 0x1F48, .count = 6, .stride2 = false, .delta = -8 },
.{ .lo = 0x1F59, .count = 4, .stride2 = true, .delta = -8 },
.{ .lo = 0x1F68, .count = 8, .stride2 = false, .delta = -8 },
.{ .lo = 0x1F88, .count = 8, .stride2 = false, .delta = -8 },
.{ .lo = 0x1F98, .count = 8, .stride2 = false, .delta = -8 },
.{ .lo = 0x1FA8, .count = 8, .stride2 = false, .delta = -8 },
.{ .lo = 0x1FB8, .count = 2, .stride2 = false, .delta = -8 },
.{ .lo = 0x1FBA, .count = 2, .stride2 = false, .delta = -74 },
.{ .lo = 0x1FBC, .count = 1, .stride2 = false, .delta = -9 },
.{ .lo = 0x1FC8, .count = 4, .stride2 = false, .delta = -86 },
.{ .lo = 0x1FCC, .count = 1, .stride2 = false, .delta = -9 },
.{ .lo = 0x1FD8, .count = 2, .stride2 = false, .delta = -8 },
.{ .lo = 0x1FDA, .count = 2, .stride2 = false, .delta = -100 },
.{ .lo = 0x1FE8, .count = 2, .stride2 = false, .delta = -8 },
.{ .lo = 0x1FEA, .count = 2, .stride2 = false, .delta = -112 },
.{ .lo = 0x1FEC, .count = 1, .stride2 = false, .delta = -7 },
.{ .lo = 0x1FF8, .count = 2, .stride2 = false, .delta = -128 },
.{ .lo = 0x1FFA, .count = 2, .stride2 = false, .delta = -126 },
.{ .lo = 0x1FFC, .count = 1, .stride2 = false, .delta = -9 },
.{ .lo = 0x2126, .count = 1, .stride2 = false, .delta = -7517 },
.{ .lo = 0x212A, .count = 1, .stride2 = false, .delta = -8383 },
.{ .lo = 0x212B, .count = 1, .stride2 = false, .delta = -8262 },
.{ .lo = 0x2132, .count = 1, .stride2 = false, .delta = 28 },
.{ .lo = 0x2160, .count = 16, .stride2 = false, .delta = 16 },
.{ .lo = 0x2183, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x24B6, .count = 26, .stride2 = false, .delta = 26 },
.{ .lo = 0x2C00, .count = 48, .stride2 = false, .delta = 48 },
.{ .lo = 0x2C60, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x2C62, .count = 1, .stride2 = false, .delta = -10743 },
.{ .lo = 0x2C63, .count = 1, .stride2 = false, .delta = -3814 },
.{ .lo = 0x2C64, .count = 1, .stride2 = false, .delta = -10727 },
.{ .lo = 0x2C67, .count = 3, .stride2 = true, .delta = 1 },
.{ .lo = 0x2C6D, .count = 1, .stride2 = false, .delta = -10780 },
.{ .lo = 0x2C6E, .count = 1, .stride2 = false, .delta = -10749 },
.{ .lo = 0x2C6F, .count = 1, .stride2 = false, .delta = -10783 },
.{ .lo = 0x2C70, .count = 1, .stride2 = false, .delta = -10782 },
.{ .lo = 0x2C72, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x2C75, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0x2C7E, .count = 2, .stride2 = false, .delta = -10815 },
.{ .lo = 0x2C80, .count = 50, .stride2 = true, .delta = 1 },
.{ .lo = 0x2CEB, .count = 2, .stride2 = true, .delta = 1 },
.{ .lo = 0x2CF2, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0xA640, .count = 23, .stride2 = true, .delta = 1 },
.{ .lo = 0xA680, .count = 14, .stride2 = true, .delta = 1 },
.{ .lo = 0xA722, .count = 7, .stride2 = true, .delta = 1 },
.{ .lo = 0xA732, .count = 31, .stride2 = true, .delta = 1 },
.{ .lo = 0xA779, .count = 2, .stride2 = true, .delta = 1 },
.{ .lo = 0xA77D, .count = 1, .stride2 = false, .delta = -35332 },
.{ .lo = 0xA77E, .count = 5, .stride2 = true, .delta = 1 },
.{ .lo = 0xA78B, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0xA78D, .count = 1, .stride2 = false, .delta = -42280 },
.{ .lo = 0xA790, .count = 2, .stride2 = true, .delta = 1 },
.{ .lo = 0xA796, .count = 10, .stride2 = true, .delta = 1 },
.{ .lo = 0xA7AA, .count = 1, .stride2 = false, .delta = -42308 },
.{ .lo = 0xA7AB, .count = 1, .stride2 = false, .delta = -42319 },
.{ .lo = 0xA7AC, .count = 1, .stride2 = false, .delta = -42315 },
.{ .lo = 0xA7AD, .count = 1, .stride2 = false, .delta = -42305 },
.{ .lo = 0xA7AE, .count = 1, .stride2 = false, .delta = -42308 },
.{ .lo = 0xA7B0, .count = 1, .stride2 = false, .delta = -42258 },
.{ .lo = 0xA7B1, .count = 1, .stride2 = false, .delta = -42282 },
.{ .lo = 0xA7B2, .count = 1, .stride2 = false, .delta = -42261 },
.{ .lo = 0xA7B3, .count = 1, .stride2 = false, .delta = 928 },
.{ .lo = 0xA7B4, .count = 8, .stride2 = true, .delta = 1 },
.{ .lo = 0xA7C4, .count = 1, .stride2 = false, .delta = -48 },
.{ .lo = 0xA7C5, .count = 1, .stride2 = false, .delta = -42307 },
.{ .lo = 0xA7C6, .count = 1, .stride2 = false, .delta = -35384 },
.{ .lo = 0xA7C7, .count = 2, .stride2 = true, .delta = 1 },
.{ .lo = 0xA7CB, .count = 1, .stride2 = false, .delta = -42343 },
.{ .lo = 0xA7CC, .count = 8, .stride2 = true, .delta = 1 },
.{ .lo = 0xA7DC, .count = 1, .stride2 = false, .delta = -42561 },
.{ .lo = 0xA7F5, .count = 1, .stride2 = false, .delta = 1 },
.{ .lo = 0xFF21, .count = 26, .stride2 = false, .delta = 32 },
.{ .lo = 0x10400, .count = 40, .stride2 = false, .delta = 40 },
.{ .lo = 0x104B0, .count = 36, .stride2 = false, .delta = 40 },
.{ .lo = 0x10570, .count = 11, .stride2 = false, .delta = 39 },
.{ .lo = 0x1057C, .count = 15, .stride2 = false, .delta = 39 },
.{ .lo = 0x1058C, .count = 7, .stride2 = false, .delta = 39 },
.{ .lo = 0x10594, .count = 2, .stride2 = false, .delta = 39 },
.{ .lo = 0x10C80, .count = 51, .stride2 = false, .delta = 64 },
.{ .lo = 0x10D50, .count = 22, .stride2 = false, .delta = 32 },
.{ .lo = 0x118A0, .count = 32, .stride2 = false, .delta = 32 },
.{ .lo = 0x16E40, .count = 32, .stride2 = false, .delta = 32 },
.{ .lo = 0x16EA0, .count = 25, .stride2 = false, .delta = 27 },
.{ .lo = 0x1E900, .count = 34, .stride2 = false, .delta = 34 },
};
// ---- END GENERATED CASE TABLES ----
/// Simple case mapping of one code point through a range table (binary
/// search; unmapped code points map to themselves).
fn caseMapLookup(ranges: []const CaseRange, cp: u21) u21 {
var lo: usize = 0;
var hi: usize = ranges.len;
while (lo < hi) {
const mid = lo + (hi - lo) / 2;
const r = ranges[mid];
const stride: u32 = if (r.stride2) 2 else 1;
const last: u32 = @as(u32, r.lo) + (@as(u32, r.count) - 1) * stride;
if (cp < r.lo) {
hi = mid;
} else if (cp > last) {
lo = mid + 1;
} else {
if ((cp - r.lo) % stride == 0) {
return @intCast(@as(i32, cp) + r.delta);
}
return cp;
}
}
return cp;
}
/// One strict UTF-8 decode step (RFC 3629 well-formed: no overlongs, no
/// surrogates, nothing above U+10FFFF). `valid = false` means the byte at
/// `i` does not begin a well-formed sequence — the caller passes that ONE
/// byte through unchanged and re-tries at the next byte. The node polyfill
/// walks bytes with exactly this rule.
const Utf8Step = struct { cp: u21, len: u3, valid: bool };
fn utf8DecodeAt(bytes: []const u8, i: usize) Utf8Step {
const invalid: Utf8Step = .{ .cp = 0, .len = 1, .valid = false };
const b0 = bytes[i];
if (b0 < 0x80) return .{ .cp = b0, .len = 1, .valid = true };
if (b0 < 0xC2) return invalid; // continuation byte or overlong lead
if (b0 < 0xE0) {
if (i + 1 >= bytes.len) return invalid;
const b1 = bytes[i + 1];
if (b1 & 0xC0 != 0x80) return invalid;
return .{ .cp = (@as(u21, b0 & 0x1F) << 6) | (b1 & 0x3F), .len = 2, .valid = true };
}
if (b0 < 0xF0) {
if (i + 2 >= bytes.len) return invalid;
const b1 = bytes[i + 1];
const b2 = bytes[i + 2];
if (b1 & 0xC0 != 0x80 or b2 & 0xC0 != 0x80) return invalid;
if (b0 == 0xE0 and b1 < 0xA0) return invalid; // overlong
if (b0 == 0xED and b1 > 0x9F) return invalid; // surrogate
return .{ .cp = (@as(u21, b0 & 0x0F) << 12) | (@as(u21, b1 & 0x3F) << 6) | (b2 & 0x3F), .len = 3, .valid = true };
}
if (b0 < 0xF5) {
if (i + 3 >= bytes.len) return invalid;
const b1 = bytes[i + 1];
const b2 = bytes[i + 2];
const b3 = bytes[i + 3];
if (b1 & 0xC0 != 0x80 or b2 & 0xC0 != 0x80 or b3 & 0xC0 != 0x80) return invalid;
if (b0 == 0xF0 and b1 < 0x90) return invalid; // overlong
if (b0 == 0xF4 and b1 > 0x8F) return invalid; // above U+10FFFF
return .{
.cp = (@as(u21, b0 & 0x07) << 18) | (@as(u21, b1 & 0x3F) << 12) | (@as(u21, b2 & 0x3F) << 6) | (b3 & 0x3F),
.len = 4,
.valid = true,
};
}
return invalid;
}
/// Decode the well-formed sequence ENDING at `end` (exclusive), for the
/// backward walks (trimEnd). `valid = false` when the bytes before `end`
/// are not one whole well-formed sequence.
fn utf8DecodeBefore(bytes: []const u8, end: usize) Utf8Step {
const invalid: Utf8Step = .{ .cp = 0, .len = 1, .valid = false };
var back: usize = 1;
while (back <= 4 and back <= end) : (back += 1) {
const b = bytes[end - back];
if (b & 0xC0 != 0x80) {
const step = utf8DecodeAt(bytes, end - back);
if (step.valid and step.len == back) return step;
return invalid;
}
}
return invalid;
}
fn utf8EncodedLen(cp: u21) usize {
if (cp < 0x80) return 1;
if (cp < 0x800) return 2;
if (cp < 0x10000) return 3;
return 4;
}
fn utf8EncodeInto(out: []u8, at: usize, cp: u21) usize {
if (cp < 0x80) {
out[at] = @intCast(cp);
return 1;
}
if (cp < 0x800) {
out[at] = 0xC0 | @as(u8, @intCast(cp >> 6));
out[at + 1] = 0x80 | @as(u8, @intCast(cp & 0x3F));
return 2;
}
if (cp < 0x10000) {
out[at] = 0xE0 | @as(u8, @intCast(cp >> 12));
out[at + 1] = 0x80 | @as(u8, @intCast((cp >> 6) & 0x3F));
out[at + 2] = 0x80 | @as(u8, @intCast(cp & 0x3F));
return 3;
}
out[at] = 0xF0 | @as(u8, @intCast(cp >> 18));
out[at + 1] = 0x80 | @as(u8, @intCast((cp >> 12) & 0x3F));
out[at + 2] = 0x80 | @as(u8, @intCast((cp >> 6) & 0x3F));
out[at + 3] = 0x80 | @as(u8, @intCast(cp & 0x3F));
return 4;
}
/// The JS WhiteSpace + LineTerminator set — exactly the code points
/// `String.prototype.trim` strips (probed against node; ES2025 11.2/12.3).
fn isJsWhitespace(cp: u21) bool {
return switch (cp) {
0x09...0x0D, 0x20, 0xA0, 0x1680, 0x2000...0x200A, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000, 0xFEFF => true,
else => false,
};
}
/// App-configurable kernel capacities (bytes). The defaults carry a small
/// desktop app comfortably; raise them per app through the transpiler's
/// capacity options rather than editing this file.
pub const Options = struct {
/// Frame arena capacity: the transient budget of ONE dispatch.
frame_cap: usize = 1 << 20, // 1 MiB
/// Model heap capacity PER SPACE (two spaces exist): the committed
/// model graph plus not-yet-compacted garbage must fit in one space.
heap_cap: usize = 1 << 20, // 1 MiB
};
/// The default-capacity kernel. `Kernel(.{})` memoizes to this instance.
pub const default = Kernel(.{});
pub fn Kernel(comptime opts: Options) type {
return struct {
pub const frame_cap: usize = opts.frame_cap;
pub const heap_cap: usize = opts.heap_cap;
pub const heap_watermark: usize = heap_cap * 3 / 4;
var frame_buf: [frame_cap]u8 align(16) = undefined;
var frame_off: usize = 0;
var heap_bufs: [2][heap_cap]u8 align(16) = undefined;
var heap_cur: usize = 0;
var heap_off: usize = 0;
// ---------------------------------------------------------- stats
pub var stat_frame_last: usize = 0; // frame bytes used by the last dispatch
pub var stat_frame_peak: usize = 0; // max frame bytes over any dispatch
pub var stat_commit_last: usize = 0; // heap bytes committed by the last dispatch
pub var stat_compactions: u64 = 0;
// ---------------------------------------------------- frame arena
pub fn frameAlloc(comptime T: type, n: usize) []T {
const size = n *% @sizeOf(T);
const aligned = std.mem.alignForward(usize, frame_off, @alignOf(T));
const next = aligned + size;
if (next > frame_cap) {
@panic(std.fmt.comptimePrint(
"frame arena overflow: one dispatch built more than the {d}-byte frame capacity; raise it with the frameCap transpile option (--frame-cap, rt.Options.frame_cap)",
.{frame_cap},
));
}
frame_off = next;
const ptr: [*]T = @ptrCast(@alignCast(frame_buf[aligned..].ptr));
return ptr[0..n];
}
pub fn frameCreate(comptime T: type, value: T) *T {
const slot = frameAlloc(T, 1);
slot[0] = value;
return &slot[0];
}
/// Capacity growth for the push-builder lowering (`const out: T[] = []` +
/// `out.push(x)` in a loop): bump-allocate double the capacity and copy the
/// filled prefix. The abandoned block is frame-transient by construction —
/// the whole arena resets after the dispatch commits.
pub fn frameGrow(comptime T: type, old: []T) []T {
const cap = if (old.len == 0) 4 else old.len * 2;
const out = frameAlloc(T, cap);
@memcpy(out[0..old.len], old);
return out;
}
pub fn frameReset() void {
stat_frame_last = frame_off;
if (frame_off > stat_frame_peak) stat_frame_peak = frame_off;
frame_off = 0;
}
pub fn inFrame(addr: usize) bool {
const base = @intFromPtr(&frame_buf);
return addr >= base and addr < base + frame_cap;
}
// ----------------------------------------------------- model heap
pub fn heapAlloc(comptime T: type, n: usize) []T {
const size = n *% @sizeOf(T);
const aligned = std.mem.alignForward(usize, heap_off, @alignOf(T));
const next = aligned + size;
if (next > heap_cap) {
@panic(std.fmt.comptimePrint(
"model heap overflow: the committed model outgrew the {d}-byte per-space heap capacity; raise it with the heapCap transpile option (--heap-cap, rt.Options.heap_cap)",
.{heap_cap},
));
}
heap_off = next;
const ptr: [*]T = @ptrCast(@alignCast(heap_bufs[heap_cur][aligned..].ptr));
return ptr[0..n];
}
pub fn heapCreate(comptime T: type, value: T) *T {
const slot = heapAlloc(T, 1);
slot[0] = value;
return &slot[0];
}
pub fn heapUsed() usize {
return heap_off;
}
/// Flip to the other space (compaction target). The old space stays readable
/// until the copy completes; its bytes are dead afterwards.
pub fn heapFlip() void {
heap_cur ^= 1;
heap_off = 0;
}
pub fn currentHeapBase() usize {
return @intFromPtr(&heap_bufs[heap_cur]);
}
pub fn inCurrentHeap(addr: usize) bool {
const base = @intFromPtr(&heap_bufs[heap_cur]);
return addr >= base and addr < base + heap_cap;
}
// ------------------------------------------------------ number ops
//
// Float helpers for the sites where Zig's builtins differ from JS number
// semantics. @min/@max resolve a NaN operand to the other value and pick
// either zero when the magnitudes tie; JS Math.min/max propagate NaN and
// order -0 below +0. @round rounds half away from zero; JS Math.round is
// half toward positive infinity, decides the .5 boundary on the exact value
// (never on a lossy x + 0.5), and keeps the zero sign of negative inputs.
pub fn jsMin(a: f64, b: f64) f64 {
if (std.math.isNan(a) or std.math.isNan(b)) return std.math.nan(f64);
if (a == 0.0 and b == 0.0) return if (std.math.signbit(a)) a else b;
return if (a < b) a else b;
}
pub fn jsMax(a: f64, b: f64) f64 {
if (std.math.isNan(a) or std.math.isNan(b)) return std.math.nan(f64);
if (a == 0.0 and b == 0.0) return if (std.math.signbit(a)) b else a;
return if (a > b) a else b;
}
/// JS Math.sign on floats: NaN stays NaN and a zero keeps its sign (the
/// comparisons are false for NaN and both zeros, so `x` falls through).
pub fn jsSign(x: f64) f64 {
if (x > 0) return 1.0;
if (x < 0) return -1.0;
return x;
}
/// JS Number.isInteger on a float value: finite and integral (-0 included;
/// NaN and the infinities fail the comparisons/finiteness test).
pub fn jsIsInteger(x: f64) bool {
return std.math.isFinite(x) and @floor(x) == x;
}
pub fn jsRound(x: f64) f64 {
if (!std.math.isFinite(x)) return x;
const fl = @floor(x);
// Deciding on the fractional part (not on a lossy x + 0.5) keeps the .5
// boundary exact: x - fl is exact wherever the true fraction can sit near
// one half, and elsewhere its rounding cannot carry the comparison across
// the boundary.
const rounded = if (x - fl >= 0.5) fl + 1.0 else fl;
if (rounded == 0.0 and std.math.signbit(x)) return -0.0;
return rounded;
}
/// JS `Array.prototype.slice` index resolution: a negative index counts from
/// the end, then everything clamps into [0, len]. Emitted `.slice` sites
/// resolve both bounds through here so out-of-range arguments produce the
/// clamped copy JS produces, never an index trap.
pub fn sliceIndex(len: usize, v: i64) usize {
if (v < 0) {
const from_end = @as(i64, @intCast(len)) + v;
return if (from_end < 0) 0 else @intCast(from_end);
}
if (v > @as(i64, @intCast(len))) return len;
return @intCast(v);
}
/// JS `TypedArray.prototype.subarray` bound resolution: both bounds resolve
/// the slice way (negatives count from the end, everything clamps into
/// [0, len]) and a crossed range is empty. A view, never a copy. `to` is
/// null when the source omits the second argument (view to the end).
pub fn subarray(bytes: []const u8, from: i64, to: ?i64) []const u8 {
const lo = sliceIndex(bytes.len, from);
const hi = @max(lo, if (to) |t| sliceIndex(bytes.len, t) else bytes.len);
return bytes[lo..hi];
}
// -------------------------------------------------------- byte text
//
// The emitted lowerings of the everyday string methods on bytes
// (`s.toUpperCase()`, `s.repeat(3)`, ...). Semantics are pinned to
// the file-header contract: byte-honest measurements, Unicode
// SIMPLE case mapping over the generated tables, invalid UTF-8
// passed through unchanged, and results either fresh frame-arena
// bytes (case map, repeat, pad, split's array) or views of the
// receiver (trim family, at-length pads) — the node polyfill
// aliases identically.
/// Shared case-map walk: exact-size two-pass (measure, then fill).
fn textCaseMap(bytes: []const u8, ranges: []const CaseRange) []const u8 {
var out_len: usize = 0;
var i: usize = 0;
while (i < bytes.len) {
const step = utf8DecodeAt(bytes, i);
if (!step.valid) {
out_len += 1;
i += 1;
continue;
}
out_len += utf8EncodedLen(caseMapLookup(ranges, step.cp));
i += step.len;
}
const out = frameAlloc(u8, out_len);
var at: usize = 0;
i = 0;
while (i < bytes.len) {
const step = utf8DecodeAt(bytes, i);
if (!step.valid) {
out[at] = bytes[i];
at += 1;
i += 1;
continue;
}
at += utf8EncodeInto(out, at, caseMapLookup(ranges, step.cp));
i += step.len;
}
return out;
}
pub fn textUpper(bytes: []const u8) []const u8 {
return textCaseMap(bytes, &simple_upper_ranges);
}
pub fn textLower(bytes: []const u8) []const u8 {
return textCaseMap(bytes, &simple_lower_ranges);
}
/// `.repeat(n)`. A negative count is JS's RangeError site; builtins
/// never throw here, so the native form is the deterministic panic
/// an uncaught throw would be — guard the count first.
pub fn textRepeat(bytes: []const u8, n: i64) []const u8 {
if (n < 0) @panic("repeat count is negative: JS throws RangeError here — guard the count (n >= 0) before calling .repeat");
const times: usize = @intCast(n);
const out = frameAlloc(u8, bytes.len * times);
var off: usize = 0;
var k: usize = 0;
while (k < times) : (k += 1) {
@memcpy(out[off..][0..bytes.len], bytes);
off += bytes.len;
}
return out;
}
/// `.indexOf(needle)` with a bytes needle: first byte offset of the
/// substring, -1 when absent; the empty needle matches at 0 (JS).
pub fn textIndexOf(hay: []const u8, needle: []const u8) i64 {
if (needle.len > hay.len) return -1;
var s: usize = 0;
while (s + needle.len <= hay.len) : (s += 1) {
if (std.mem.eql(u8, hay[s..][0..needle.len], needle)) return @intCast(s);
}
return -1;
}
/// `.lastIndexOf(needle)` with a bytes needle: last byte offset,
/// -1 when absent; the empty needle matches at hay.len (JS).
pub fn textLastIndexOf(hay: []const u8, needle: []const u8) i64 {
if (needle.len > hay.len) return -1;
var s: usize = hay.len - needle.len;
while (true) : (s -= 1) {
if (std.mem.eql(u8, hay[s..][0..needle.len], needle)) return @intCast(s);
if (s == 0) return -1;
}
}
pub fn textIncludes(hay: []const u8, needle: []const u8) bool {
return textIndexOf(hay, needle) != -1;
}
pub fn textStartsWith(hay: []const u8, needle: []const u8) bool {
return needle.len <= hay.len and std.mem.eql(u8, hay[0..needle.len], needle);
}
pub fn textEndsWith(hay: []const u8, needle: []const u8) bool {
return needle.len <= hay.len and std.mem.eql(u8, hay[hay.len - needle.len ..], needle);
}
/// The number-needle forms (JS TypedArray element search,
/// SameValueZero over byte values: 66.5 matches nothing, -0
/// matches 0, NaN matches nothing — a u8 element is never NaN).
pub fn textIndexOfByte(hay: []const u8, x: f64) i64 {
for (hay, 0..) |b, i| {
if (@as(f64, @floatFromInt(b)) == x) return @intCast(i);
}
return -1;
}
pub fn textLastIndexOfByte(hay: []const u8, x: f64) i64 {
var i: usize = hay.len;
while (i > 0) {
i -= 1;
if (@as(f64, @floatFromInt(hay[i])) == x) return @intCast(i);
}
return -1;
}
pub fn textIncludesByte(hay: []const u8, x: f64) bool {
return textIndexOfByte(hay, x) != -1;
}
/// `.padStart(n, fill)` — n is the target BYTE length. At-or-under
/// target (or an empty fill) returns the receiver itself, exactly
/// the polyfill's aliasing; the last fill repetition truncates (JS).
pub fn textPadStart(bytes: []const u8, n: i64, fill: []const u8) []const u8 {
if (n <= 0) return bytes;
const target: usize = @intCast(n);
if (target <= bytes.len or fill.len == 0) return bytes;
const out = frameAlloc(u8, target);
const pad = target - bytes.len;
var off: usize = 0;
while (off < pad) {
const chunk = @min(fill.len, pad - off);
@memcpy(out[off..][0..chunk], fill[0..chunk]);
off += chunk;
}
@memcpy(out[pad..], bytes);
return out;
}
pub fn textPadEnd(bytes: []const u8, n: i64, fill: []const u8) []const u8 {
if (n <= 0) return bytes;
const target: usize = @intCast(n);
if (target <= bytes.len or fill.len == 0) return bytes;
const out = frameAlloc(u8, target);
@memcpy(out[0..bytes.len], bytes);
var off: usize = bytes.len;
while (off < target) {
const chunk = @min(fill.len, target - off);
@memcpy(out[off..][0..chunk], fill[0..chunk]);
off += chunk;
}
return out;
}
/// The trim family strips the JS whitespace set (WhiteSpace +
/// LineTerminator, decoded over UTF-8) and returns a VIEW — no
/// copy. A byte run that is not well-formed UTF-8 is not
/// whitespace, so trimming stops there.
pub fn textTrimStart(bytes: []const u8) []const u8 {
var i: usize = 0;
while (i < bytes.len) {
const step = utf8DecodeAt(bytes, i);
if (!step.valid or !isJsWhitespace(step.cp)) break;
i += step.len;
}
return bytes[i..];
}
pub fn textTrimEnd(bytes: []const u8) []const u8 {
var end: usize = bytes.len;
while (end > 0) {
const step = utf8DecodeBefore(bytes, end);
if (!step.valid or !isJsWhitespace(step.cp)) break;
end -= step.len;
}
return bytes[0..end];
}
pub fn textTrim(bytes: []const u8) []const u8 {
return textTrimEnd(textTrimStart(bytes));
}
/// `.split(sep)` with a bytes separator: non-overlapping, left to
/// right, String.split shapes exactly (leading/trailing/adjacent
/// separators produce empty elements; no match returns [whole]).
/// Elements are VIEWS into the receiver; the array itself is fresh
/// frame-arena memory (locally owned by the caller). An EMPTY
/// separator returns [whole] — per-code-point splitting is the
/// taught stop at compile time; this pins the dynamic case.
pub fn textSplit(hay: []const u8, sep: []const u8) [][]const u8 {
if (sep.len == 0) {
const whole = frameAlloc([]const u8, 1);
whole[0] = hay;
return whole;
}
var count: usize = 1;
var i: usize = 0;
while (i + sep.len <= hay.len) {
if (std.mem.eql(u8, hay[i..][0..sep.len], sep)) {
count += 1;
i += sep.len;
} else {
i += 1;
}
}
const out = frameAlloc([]const u8, count);
var part: usize = 0;
var start: usize = 0;
i = 0;
while (i + sep.len <= hay.len) {
if (std.mem.eql(u8, hay[i..][0..sep.len], sep)) {
out[part] = hay[start..i];
part += 1;
i += sep.len;
start = i;
} else {
i += 1;
}
}
out[part] = hay[start..];
return out;
}
/// `.at(i)` — byte access: the byte value at a (negative counts
/// from the end) byte index, or the empty (JS undefined) when out
/// of range.
pub fn textAt(bytes: []const u8, i: i64) ?i64 {
const idx = if (i < 0) i + @as(i64, @intCast(bytes.len)) else i;
if (idx < 0 or idx >= @as(i64, @intCast(bytes.len))) return null;
return @as(i64, bytes[@intCast(idx)]);
}
// -------------------------------------------------------- commands
//
// Cmd wire format v2 — the byte encoding of the command value an effectful
// `update` (or a pair-returning `initialModel`) returns alongside the next
// model (`UpdateResult.cmd` / `InitResult.cmd` in emitted cores). A command
// value is a flat sequence of op records; the empty slice means "no
// effects", and batching is plain concatenation:
//
// persist [op 0x01]
// now [op 0x02][msg_tag u8]
// host [op 0x03][name_len u8][name bytes][argc u8][arg f64 LE] * argc
// host_bytes [op 0x04][name_len u8][name bytes][payload_len u32 LE][payload bytes]
// request [op 0x05][name_len u8][name bytes][key_len u8][key bytes]
// [ok_tag u8][err_tag u8][payload_len u32 LE][payload bytes]
// cancel [op 0x06][key_len u8][key bytes]
// read_file [op 0x07][key_len u8][key bytes][ok_tag u8][err_tag u8]
// [path_len u32 LE][path bytes]
// write_file [op 0x08][key_len u8][key bytes][ok_tag u8][err_tag u8]
// [path_len u32 LE][path bytes][bytes_len u32 LE][bytes]
// fetch [op 0x09][key_len u8][key bytes][ok_tag u8][err_tag u8]
// [method u8][timeout_ms u32 LE][url_len u32 LE][url bytes]
// [header_count u8] then per header, in TS-field-name
// (code-unit) sort order: [name_len u8][name bytes]
// [value_len u32 LE][value bytes], then
// [body_len u32 LE][body bytes]. Header NAMES are
// compile-time text; header VALUES may be runtime
// bytes — the record is built at dispatch time, so a
// runtime value encodes exactly like a literal one
// (same length-prefixed field, no layout change).
// clip_write [op 0x0A][bytes_len u32 LE][bytes]
// clip_read [op 0x0B][key_len u8][key bytes][ok_tag u8][err_tag u8]
// delay [op 0x0C][key_len u8][key bytes][after_ms f64 LE][msg_tag u8]
// spawn [op 0x0D][key_len u8][key bytes][line_tag u8][exit_tag u8]
// [err_tag u8][mode u8][argc u8] then per argv element:
// [arg_len u32 LE][arg bytes], then [stdin_len u32 LE][stdin]
// audio_play [op 0x0E][key_len u8][key bytes][event_tag u8]
// [path_len u32 LE][path][url_len u32 LE][url]
// [cache_len u32 LE][cache_path][expected_bytes f64 LE]
// audio_ctl [op 0x0F][key_len u8][key bytes][verb u8][value f64 LE]
//
// v2 is additive over v1: the 0x01-0x03 records are byte-identical to v1,
// so a v1 effect log replays under a v2 reader unchanged. The named-op
// records 0x07-0x0C are additive within v2 the same way, and so are the
// streaming records 0x0D-0x0F: no existing record's bytes change, new
// opcodes only.
//
// `method` is the closed HTTP verb set in declaration order of `CmdFetchMethod`
// (GET 0, POST 1, PUT 2, DELETE 3, PATCH 4, HEAD 5). `timeout_ms` 0 means
// "the host engine's default fetch timeout" — the wire never bakes the
// default in, so re-tuning the engine re-tunes old cores too.
//
// `msg_tag` (and `ok_tag`/`err_tag`) is the declaration-order index of the
// app's Msg arm the runtime dispatches with the result payload; scalar args
// are f64 because every subset number is one. Enum/tag payloads ride
// declaration order across this whole channel (R5), so reordering Msg arms
// or CmdOp members is a format break: bump `cmd_format_version` when one
// moves, and for any record-layout change. Hosts that persist or transport
// a command log must store the version alongside the bytes; readers refuse
// other versions loudly rather than misreading yesterday's shape.
//
// Record semantics for the host:
// host_bytes fire-and-forget host command carrying one bytes payload
// (raw `Uint8Array` app data, or a host record — see the
// record encoding below).
// request a keyed, routed host command: the host performs `name`
// with `payload` and dispatches exactly one result Msg —
// the `ok_tag` arm with the result bytes as its single
// payload field on success, or the `err_tag` arm with the
// error bytes on failure. `key` (may be empty = unkeyed)
// names the in-flight effect for dedup/cancel: issuing a
// request whose key is already in flight replaces the old
// one (its result is dropped), and `cancel` with the same
// key drops it without dispatching either arm.
// cancel drop the in-flight keyed effect with this key, if
// any, SILENTLY: a cancelled request, named engine
// effect (read_file/write_file/fetch/clip_read), or
// delay dispatches neither arm — its result is simply
// dropped. The exception is a live spawn stream: cancel
// ends the child and its err arm delivers "cancelled"
// (killing a process is an observable event, kept
// loud). An open audio stream is not cancel's to end —
// close it with audio_ctl stop.
// read_file routed whole-file read: exactly one terminal — the ok
// arm with the file's bytes as its single bytes payload,
// or the err arm with the failure reason as bytes
// ("not_found", "io_failed", "truncated", "rejected").
// write_file routed whole-file write (parent directories created,
// existing file replaced whole): exactly one terminal —
// the ok arm, which carries NO payload (a successful
// write has nothing to report beyond success), or the
// err arm with the reason bytes ("io_failed",
// "rejected").
// fetch routed buffered HTTP(S) exchange: exactly one terminal
// — the ok arm, whose payload is a two-field record of
// one number field (the real HTTP status, non-2xx
// included) and one bytes field (the whole body), or the
// err arm with the reason bytes ("connect_failed",
// "tls_failed", "protocol_failed", "timed_out",
// "rejected", "truncated" — a body over the engine's
// buffered bound routes err rather than passing a
// silently cut body as ok).
// clip_write fire-and-forget clipboard write; an over-bound or
// refused write is dropped (documented, not silent-by-
// accident: there is no route to report on).
// clip_read routed clipboard read: the ok arm with the clipboard
// text as its single bytes payload, or the err arm with
// the reason bytes ("failed", "rejected").
// delay a keyed ONE-SHOT timer: after `after_ms` the host
// dispatches the `msg_tag` arm once, with the fire time
// (fractional ms) as its single number payload — the
// same arm shape `now` and Sub timer fires target.
// Re-issuing a live delay key re-arms it from now (the
// debounce discipline); `cancel` with the key drops it
// silently.
// spawn a STREAMING effect: run `argv` (each element its own
// bytes field; `mode` 0 streams stdout lines, 1 collects
// whole stdout), writing `stdin` to the child once when
// non-empty. Unlike every op above, one spawn dispatches
// MANY result Msgs across dispatches: in line mode each
// stdout line dispatches the `line_tag` arm with the
// line bytes (line_tag 0xFF = no line routing, lines are
// dropped), and exactly ONE terminal follows — a clean
// `.exited` end dispatches the `exit_tag` arm (line
// mode: the exit code as its single number payload;
// collect mode: a two-field code/output record, one
// number field and one bytes field matched by type),
// while every other end (signaled, cancelled, rejected,
// spawn_failed, or a collect stdout over the engine's
// 512 KiB bound, reason bytes "truncated") dispatches
// the `err_tag` arm with the reason bytes. The key is
// live for the whole stream: `cancel` mid-stream ends
// the child and the err arm runs with "cancelled" —
// killing a process is an observable event, kept loud —
// and a spawn whose key is already streaming REJECTS
// the new spawn (err arm "rejected"): a running
// subprocess is never killed implicitly; cancel it
// first.
// audio_play open (or replace — one player is the whole surface)
// the keyed audio EVENT STREAM: resolve the source
// cascade (local `path` first, then `url`, cached at
// `cache_path` and integrity-gated by `expected_bytes`;
// 0 = unknown size, empty fields = absent) and start
// playback. Every playback event — loaded, position
// ticks, completed, failed, rejected, spectrum — then
// dispatches the `event_tag` arm until `audio_ctl` stop
// (or a replacing audio_play) closes the stream. The
// arm is a six-field record built by name: `state` (an
// enum whose members are exactly loaded/position/
// completed/failed/rejected/spectrum, matched by member
// NAME so declaration order is the app's), position_ms
// and duration_ms (numbers), playing and buffering
// (booleans), and bands (bytes: the 32 spectrum band
// magnitudes, all zeros outside `.spectrum` events).
// Failure is never silent: an unplayable source is one
// `.failed` event, a refused command one `.rejected`.
// audio_ctl drive the open stream's playback in place, by verb
// (`CmdAudioVerb` declaration order): pause 0, resume
// 1, stop 2, seek 3 (`value` = position ms), volume 4
// (`value` clamped 0..1). `value` is 0 when the verb
// takes none. Control verbs are fire-and-forget — no
// result of their own; their consequences arrive on
// the keyed event stream — and a verb whose key names
// no open stream is a no-op (the playback it aimed at
// is already gone). `stop` closes the stream: no
// events for that key after it.
//
// Key discipline is ONE rule across request, the named engine ops
// (read_file/write_file/fetch/clip_read), and delay: issuing an
// effect whose key is already in flight REPLACES the old one (the
// superseded effect's result is dropped — no message), and `cancel`
// drops it silently. Empty keys are unkeyed (never cancellable, never
// colliding). The ONE exception is a live spawn key, which REJECTS
// the duplicate (err arm "rejected"): a running subprocess is never
// killed implicitly — cancel it first. audio_play also replaces on
// key reuse, per the engine underneath: one player is the whole
// surface.
//
// Sub vs streams, the design line: `Sub` descriptors are DECLARATIVE
// recurring effects derived from the model after every commit (the
// host reconciles them; the app never opens or closes one). spawn and
// audio_play are Cmd-INITIATED STREAMS: imperative opens with a keyed
// lifecycle the app drives (cancel / audio_ctl stop), delivering many
// results until their terminal (spawn) or close (audio) retires the
// key.
//
// Host record payload encoding (the `Cmd.host`/`Cmd.request` record form,
// produced identically by the SDK under node and by emitted Zig): fields
// sorted by TS field name (code-unit order), concatenated with no field
// headers — number -> f64 LE (8 bytes), boolean -> u8 0/1, Uint8Array ->
// [len u32 LE][bytes]. The decoding contract is the record's TS interface,
// which both ends derive from at build time.
//
// Lifetime: command bytes are frame-arena resident. The host consumes (or
// copies) them after the model commit and BEFORE frameReset — the same
// boundary the committed model crosses.
pub const cmd_format_version: u32 = 2;
/// An encoded command value: op records per the layout above.
pub const Cmd = []const u8;
pub const CmdOp = enum(u8) {
persist = 0x01,
now = 0x02,
host = 0x03,
host_bytes = 0x04,
request = 0x05,
cancel = 0x06,
read_file = 0x07,
write_file = 0x08,
fetch = 0x09,
clip_write = 0x0A,
clip_read = 0x0B,
delay = 0x0C,
spawn = 0x0D,
audio_play = 0x0E,
audio_ctl = 0x0F,
};
/// The spawn record's "no line routing" sentinel: a `line_tag` of
/// 0xFF means stdout lines dispatch nothing (collect spawns, and
/// line spawns that only care about the exit).
pub const spawn_no_line_tag: u8 = 0xFF;
/// The spawn record's output mode, wire value = declaration order.
pub const CmdSpawnMode = enum(u8) {
lines = 0,
collect = 1,
};
/// The audio_ctl verb set, wire value = declaration order.
pub const CmdAudioVerb = enum(u8) {
pause = 0,
@"resume" = 1,
stop = 2,
seek = 3,
volume = 4,
};
/// The closed HTTP verb set of the fetch record, wire value =
/// declaration order.
pub const CmdFetchMethod = enum(u8) {
GET = 0,
POST = 1,
PUT = 2,
DELETE = 3,
PATCH = 4,
HEAD = 5,
};
/// One fetch request header, encoded in TS-field-name sort order.
/// Names are compile-time string literals in the subset; values are
/// compile-time literals or runtime bytes — both ride the record's
/// length-prefixed value field, written when the command is built.
pub const CmdFetchHeader = struct {
name: []const u8,
value: []const u8,
};
pub const cmd_none: Cmd = &.{};
pub fn cmdPersist() Cmd {
const out = frameAlloc(u8, 1);
out[0] = @intFromEnum(CmdOp.persist);
return out;
}
pub fn cmdNow(msg_tag: u8) Cmd {
const out = frameAlloc(u8, 2);
out[0] = @intFromEnum(CmdOp.now);
out[1] = msg_tag;
return out;
}
pub fn cmdHost(name: []const u8, args: []const f64) Cmd {
std.debug.assert(name.len <= 255);
std.debug.assert(args.len <= 255);
const out = frameAlloc(u8, 3 + name.len + args.len * 8);
out[0] = @intFromEnum(CmdOp.host);
out[1] = @intCast(name.len);
@memcpy(out[2..][0..name.len], name);
out[2 + name.len] = @intCast(args.len);
for (args, 0..) |arg, i| {
std.mem.writeInt(u64, out[3 + name.len + i * 8 ..][0..8], @bitCast(arg), .little);
}
return out;
}
pub fn cmdHostBytes(name: []const u8, payload: []const u8) Cmd {
std.debug.assert(name.len <= 255);
std.debug.assert(payload.len <= std.math.maxInt(u32));
const out = frameAlloc(u8, 2 + name.len + 4 + payload.len);
out[0] = @intFromEnum(CmdOp.host_bytes);
out[1] = @intCast(name.len);
@memcpy(out[2..][0..name.len], name);
std.mem.writeInt(u32, out[2 + name.len ..][0..4], @intCast(payload.len), .little);
@memcpy(out[2 + name.len + 4 ..][0..payload.len], payload);
return out;
}
pub fn cmdRequest(name: []const u8, key: []const u8, ok_tag: u8, err_tag: u8, payload: []const u8) Cmd {
std.debug.assert(name.len <= 255);
std.debug.assert(key.len <= 255);
std.debug.assert(payload.len <= std.math.maxInt(u32));
const out = frameAlloc(u8, 2 + name.len + 1 + key.len + 2 + 4 + payload.len);
out[0] = @intFromEnum(CmdOp.request);
out[1] = @intCast(name.len);
@memcpy(out[2..][0..name.len], name);
var off: usize = 2 + name.len;
out[off] = @intCast(key.len);
@memcpy(out[off + 1 ..][0..key.len], key);
off += 1 + key.len;
out[off] = ok_tag;
out[off + 1] = err_tag;
std.mem.writeInt(u32, out[off + 2 ..][0..4], @intCast(payload.len), .little);
@memcpy(out[off + 6 ..][0..payload.len], payload);
return out;
}
pub fn cmdCancel(key: []const u8) Cmd {
std.debug.assert(key.len <= 255);
const out = frameAlloc(u8, 2 + key.len);
out[0] = @intFromEnum(CmdOp.cancel);
out[1] = @intCast(key.len);
@memcpy(out[2..][0..key.len], key);
return out;
}
/// The shared routed-op prefix: [op][key_len][key][ok_tag][err_tag].
fn writeRoutedHead(out: []u8, op: CmdOp, key: []const u8, ok_tag: u8, err_tag: u8) usize {
out[0] = @intFromEnum(op);
out[1] = @intCast(key.len);
@memcpy(out[2..][0..key.len], key);
out[2 + key.len] = ok_tag;
out[3 + key.len] = err_tag;
return 4 + key.len;
}
/// A u32-LE-length-prefixed field ([len u32 LE][bytes]).
fn writeLongBytes(out: []u8, at: usize, bytes: []const u8) usize {
std.mem.writeInt(u32, out[at..][0..4], @intCast(bytes.len), .little);
@memcpy(out[at + 4 ..][0..bytes.len], bytes);
return at + 4 + bytes.len;
}
pub fn cmdReadFile(key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8) Cmd {
std.debug.assert(key.len <= 255);
std.debug.assert(path.len <= std.math.maxInt(u32));
const out = frameAlloc(u8, 4 + key.len + 4 + path.len);
var off = writeRoutedHead(out, .read_file, key, ok_tag, err_tag);
off = writeLongBytes(out, off, path);
return out;
}
pub fn cmdWriteFile(key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8, bytes: []const u8) Cmd {
std.debug.assert(key.len <= 255);
std.debug.assert(path.len <= std.math.maxInt(u32));
std.debug.assert(bytes.len <= std.math.maxInt(u32));
const out = frameAlloc(u8, 4 + key.len + 4 + path.len + 4 + bytes.len);
var off = writeRoutedHead(out, .write_file, key, ok_tag, err_tag);
off = writeLongBytes(out, off, path);
off = writeLongBytes(out, off, bytes);
return out;
}
pub fn cmdFetch(
key: []const u8,
ok_tag: u8,
err_tag: u8,
method: CmdFetchMethod,
timeout_ms: u32,
url: []const u8,
headers: []const CmdFetchHeader,
body: []const u8,
) Cmd {
std.debug.assert(key.len <= 255);
std.debug.assert(url.len <= std.math.maxInt(u32));
std.debug.assert(headers.len <= 255);
std.debug.assert(body.len <= std.math.maxInt(u32));
var header_bytes: usize = 0;
for (headers) |h| {
std.debug.assert(h.name.len <= 255);
header_bytes += 1 + h.name.len + 4 + h.value.len;
}
const out = frameAlloc(u8, 4 + key.len + 1 + 4 + 4 + url.len + 1 + header_bytes + 4 + body.len);
var off = writeRoutedHead(out, .fetch, key, ok_tag, err_tag);
out[off] = @intFromEnum(method);
std.mem.writeInt(u32, out[off + 1 ..][0..4], timeout_ms, .little);
off += 5;
off = writeLongBytes(out, off, url);
out[off] = @intCast(headers.len);
off += 1;
for (headers) |h| {
out[off] = @intCast(h.name.len);
@memcpy(out[off + 1 ..][0..h.name.len], h.name);
off += 1 + h.name.len;
off = writeLongBytes(out, off, h.value);
}
off = writeLongBytes(out, off, body);
return out;
}
pub fn cmdClipWrite(bytes: []const u8) Cmd {
std.debug.assert(bytes.len <= std.math.maxInt(u32));
const out = frameAlloc(u8, 1 + 4 + bytes.len);
out[0] = @intFromEnum(CmdOp.clip_write);
_ = writeLongBytes(out, 1, bytes);
return out;
}
pub fn cmdClipRead(key: []const u8, ok_tag: u8, err_tag: u8) Cmd {
std.debug.assert(key.len <= 255);
const out = frameAlloc(u8, 4 + key.len);
_ = writeRoutedHead(out, .clip_read, key, ok_tag, err_tag);
return out;
}
pub fn cmdDelay(key: []const u8, after_ms: f64, msg_tag: u8) Cmd {
std.debug.assert(key.len <= 255);
const out = frameAlloc(u8, 2 + key.len + 8 + 1);
out[0] = @intFromEnum(CmdOp.delay);
out[1] = @intCast(key.len);
@memcpy(out[2..][0..key.len], key);
std.mem.writeInt(u64, out[2 + key.len ..][0..8], @bitCast(after_ms), .little);
out[2 + key.len + 8] = msg_tag;
return out;
}
pub fn cmdSpawn(
key: []const u8,
line_tag: u8,
exit_tag: u8,
err_tag: u8,
mode: CmdSpawnMode,
argv: []const []const u8,
stdin: []const u8,
) Cmd {
std.debug.assert(key.len <= 255);
std.debug.assert(argv.len >= 1 and argv.len <= 255);
std.debug.assert(stdin.len <= std.math.maxInt(u32));
var argv_bytes: usize = 0;
for (argv) |arg| {
std.debug.assert(arg.len <= std.math.maxInt(u32));
argv_bytes += 4 + arg.len;
}
const out = frameAlloc(u8, 2 + key.len + 3 + 1 + 1 + argv_bytes + 4 + stdin.len);
out[0] = @intFromEnum(CmdOp.spawn);
out[1] = @intCast(key.len);
@memcpy(out[2..][0..key.len], key);
var off: usize = 2 + key.len;
out[off] = line_tag;
out[off + 1] = exit_tag;
out[off + 2] = err_tag;
out[off + 3] = @intFromEnum(mode);
out[off + 4] = @intCast(argv.len);
off += 5;
for (argv) |arg| off = writeLongBytes(out, off, arg);
_ = writeLongBytes(out, off, stdin);
return out;
}
pub fn cmdAudioPlay(
key: []const u8,
event_tag: u8,
path: []const u8,
url: []const u8,
cache_path: []const u8,
expected_bytes: f64,
) Cmd {
std.debug.assert(key.len <= 255);
std.debug.assert(path.len <= std.math.maxInt(u32));
std.debug.assert(url.len <= std.math.maxInt(u32));
std.debug.assert(cache_path.len <= std.math.maxInt(u32));
const out = frameAlloc(u8, 2 + key.len + 1 + 4 + path.len + 4 + url.len + 4 + cache_path.len + 8);
out[0] = @intFromEnum(CmdOp.audio_play);
out[1] = @intCast(key.len);
@memcpy(out[2..][0..key.len], key);
var off: usize = 2 + key.len;
out[off] = event_tag;
off += 1;
off = writeLongBytes(out, off, path);
off = writeLongBytes(out, off, url);
off = writeLongBytes(out, off, cache_path);
std.mem.writeInt(u64, out[off..][0..8], @bitCast(expected_bytes), .little);
return out;
}
pub fn cmdAudioCtl(key: []const u8, verb: CmdAudioVerb, value: f64) Cmd {
std.debug.assert(key.len <= 255);
const out = frameAlloc(u8, 2 + key.len + 1 + 8);
out[0] = @intFromEnum(CmdOp.audio_ctl);
out[1] = @intCast(key.len);
@memcpy(out[2..][0..key.len], key);
out[2 + key.len] = @intFromEnum(verb);
std.mem.writeInt(u64, out[2 + key.len + 1 ..][0..8], @bitCast(value), .little);
return out;
}
pub fn cmdBatch(cmds: []const Cmd) Cmd {
var total: usize = 0;
for (cmds) |c| total += c.len;
if (total == 0) return cmd_none;
const out = frameAlloc(u8, total);
var off: usize = 0;
for (cmds) |c| {
@memcpy(out[off..][0..c.len], c);
off += c.len;
}
return out;
}
// --------------------------------------------------- subscriptions
//
// Sub wire format — the byte encoding of the descriptor value an app's
// `subscriptions(model)` returns (versioned together with the command
// channel under `cmd_format_version`). A subscription value is a flat
// sequence of records; the empty slice means "no subscriptions", and
// batching is plain concatenation:
//
// timer [op 0x01][key_len u8][key bytes][every_ms f64 LE][msg_tag u8]
//
// Subscriptions are DECLARATIVE: after every commit the host calls
// `subscriptions` on the committed model (before frameReset — the bytes
// are frame-arena resident) and reconciles the returned set against its
// active timers by key. A key not previously active arms a timer whose
// first fire is `every_ms` from now; a key whose `every_ms` changed
// re-arms the same way; an active key absent from the set is cancelled.
// Each fire dispatches the `msg_tag` arm with the current time (ms) as
// its single number payload field, then reconciles again against the
// new committed model.
/// An encoded subscription set: records per the layout above.
pub const Sub = []const u8;
pub const SubOp = enum(u8) { timer = 0x01 };
pub const sub_none: Sub = &.{};
pub fn subTimer(key: []const u8, every_ms: f64, msg_tag: u8) Sub {
std.debug.assert(key.len <= 255);
const out = frameAlloc(u8, 2 + key.len + 8 + 1);
out[0] = @intFromEnum(SubOp.timer);
out[1] = @intCast(key.len);
@memcpy(out[2..][0..key.len], key);
std.mem.writeInt(u64, out[2 + key.len ..][0..8], @bitCast(every_ms), .little);
out[2 + key.len + 8] = msg_tag;
return out;
}
pub fn subBatch(subs: []const Sub) Sub {
return cmdBatch(subs);
}
pub fn resetAll() void {
frame_off = 0;
heap_cur = 0;
heap_off = 0;
stat_frame_last = 0;
stat_frame_peak = 0;
stat_commit_last = 0;
stat_compactions = 0;
}
};
}