1580 lines
79 KiB
Zig
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;
|
|
}
|
|
};
|
|
}
|