//! 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; } }; }