code wiki / (root) / nx_surface_boundary_expanded_t207.nx

nx_surface_boundary_expanded_t207.nx source

↩ module page · 3045 lines · 170610 B

1// nx_quicksort.nx -- Hoare quicksort (in-place, last-element pivot). 2// 3// Canonical: this is the substrate-wide canonical quicksort per 4// [[feedback-no-tool-proliferation-bit-level]]. Other primitives 5// needing quicksort MUST `import "nx_quicksort.nx"` and compose; 6// re-implementing inline is refused per the cardinal. nx_qsort.nx + 7// nx_quickselect.nx may carry distinct semantics (selection vs sort) 8// but MUST declare "Distinct from [[nx_quicksort.nx]] because: ..." 9// in their headers. 10// 11// genealogy_id: hoare_1961_quicksort 12// lineage_id: in_place_comparison_sort 13// references: Hoare 'Quicksort' CACM 4(7):321, 1961. 14// Sedgewick 1978 implementation analysis. 15// license: public_domain 16// complexity: avg O(n log n), worst O(n^2). 17// 18// Tier discipline (no bare i64 in API surface): 19// - elements: *nx_int (swappable via nx_tier.nx) 20// - indices: nx_idx (platform-pointer-width) 21// - status: nx_int (small value; not platform-mandated i64) 22// The only place `i64` appears in NishiLang source is the entrypoint 23// main() return, where the kernel exit syscall ABI mandates 64-bit. 24 25// nx_safety_envelope: 26// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 27// sil_target: SIL1 28// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 29// verdict: NOT_YET_EVALUATED 30 31// syscalls.nx -- thin __syscall wrappers used across modules. 32// 33// Sovereign path: no libc. Every memory allocation, file op, and 34// clock read in the rest of the runtime routes through one of these 35// helpers. Numbers match Linux RV64; NishiOS uses the same set. 36// 37// Extracted from runtime.nx and ir.nx's copy-pasted helpers so the 38// module-import build doesn't produce duplicate symbols. 39 40// Tier aliases (nx_size / nx_idx / nx_fd / ...) ride along with the 41// syscall shelf: 141 runtime files use `as nx_size` etc. and only 42// compiled historically because the old parser silently void-cast 43// unknown type names (T#nx-int-alias-size-0 closed that hole LOUDLY, 44// which exposed the missing import). nx_tier.nx is pure type 45// aliases (0 funcs); prepass_register_aliases skips duplicates, so 46// modules that also import it directly stay fine. 47// nx_tier.nx -- substrate-wide tier configuration. 48// 49// Single point of edit for scale-agnostic substrate. Per user 50// directive 2026-05-13: "with the i64 it looks hardcoded everywhere 51// if we really want this dynamic dont we want that to be a changeable 52// value everywhere so it can switch to i128 and i256 etc." 53// 54// Per cardinals: 55// - feedback-numeric-tier-ladder.md (N0..N9 swap) 56// - feedback-scale-agnostic-substrate.md (MCU..HPC swap) 57// - feedback-substrate-additive-not-restrictive.md (declare cost) 58// 59// SEMANTIC ALIASES (not all should swap simultaneously): 60// 61// nx_int -- DEFAULT ARITHMETIC integer. Swappable across the 62// numeric tier ladder. Swap this to i128 to make the 63// entire substrate compute in 128-bit integers. 64// 65// nx_size -- MEMORY-SIZE integer. Always platform-pointer-width. 66// Used for buffer sizes, mmap byte counts, struct 67// sizes. Does NOT swap with nx_int -- changing this 68// would break pointer arithmetic. Stays i64 on RV64. 69// 70// nx_idx -- ARRAY-INDEX integer. Same width as nx_size on 71// flat-memory targets. Distinct alias so future 72// GPU/distributed targets can change indexing without 73// touching arithmetic. 74// 75// nx_byte -- The byte type. Stays u8. Distinct alias so MCU 76// targets that emulate u16-byte memory could rebind. 77// 78// HARDWARE-TIER BUFFER SIZES (declare cost, don't restrict): 79// 80// NX_BUF_TINY -- 64 B (MCU-friendly; stack-safe) 81// NX_BUF_SMALL -- 256 B (MCU heap-friendly) 82// NX_BUF_MEDIUM -- 4096 B (page-size; workstation default) 83// NX_BUF_LARGE -- 64 KiB (server-friendly) 84// NX_BUF_HUGE -- 1 MiB (HPC; assumes virtual memory) 85// 86// Use these instead of `sys_mmap(4096)` etc. so the substrate 87// announces its memory footprint and tier-incompatible code can 88// be flagged by audit. 89// 90// HARDWARE TIER (informational; downstream code may branch): 91// 92// NX_TIER_MCU = 0 -- microcontroller, kilobytes RAM 93// NX_TIER_SOVEREIGN_CHIP = 1 -- custom silicon, ~MB RAM 94// NX_TIER_FAMILY_DEVICE = 2 -- phone/router, ~GB RAM 95// NX_TIER_WORKSTATION = 3 -- laptop/desktop, ~10-100 GB RAM 96// NX_TIER_SERVER = 4 -- server-class, ~TB RAM 97// NX_TIER_HPC = 5 -- cluster, distributed 98// 99// COMPILE-TIME SWAP for nx_int (uncomment exactly one line): 100 101// THIS FILE IS THE SINGLE DEFINITION SITE for substrate-wide types. 102// Per user directive 2026-05-13: only this file (and platform-ABI 103// definition files like nx_syscalls.nx) should declare bare i64. 104// Every other substrate module uses the aliases below. 105 106// ===== arithmetic-tier aliases (swappable per nx_int tier ladder) ===== 107 108type nx_int = i64 // N1 -- default; 9 quintillion, fits all physical scales 109// type nx_int = i32 // N0 -- MCU / embedded 110// type nx_int = i128 // N2 -- queued; needs nx_i128 backend ops 111// type nx_int = i256 // N3 -- shipped (nx_i256.nx); cosmology / crypto 112 113// ===== platform-width aliases (stay at pointer width) ================= 114 115type nx_size = i64 // memory-size / byte-count 116type nx_idx = i64 // array-index 117type nx_byte = u8 // single-byte unit 118 119// ===== POSIX/Linux platform-ABI aliases (mandated 64-bit on RV64) ==== 120// 121// Each is a 64-bit integer by Linux RV64 ABI. Renamed here so substrate 122// code never writes bare `i64` for these semantic types. 123 124type nx_fd = i64 // file descriptor (kernel-mandated width) 125type nx_exit = i64 // exit / status code (main() return) 126type nx_pid = i64 // process id 127type nx_uid = i64 // user id 128type nx_gid = i64 // group id 129type nx_syscall_num = i64 // Linux syscall number 130type nx_off = i64 // file offset (off_t) 131type nx_errno = i64 // errno (negative on syscall failure) 132 133// ===== SEMANTIC TYPE GENEALOGY (added 2026-05-20) ====================== 134// 135// Per cardinal [[feedback-type-genealogy-math-cardinal-not-script]] 136// AND its immediate refinement (same session): every alias collapsing 137// to i64 is "y2k incestuous" -- relabeling, not genealogy. Real 138// semantic types pick the APPROPRIATE underlying width based on 139// the physics of the values they represent: 140// 141// - Small sealed enums (15 outcomes, 18 probe kinds) -> u8 142// - Display pixel coords (~32M max realistic) -> i32 143// - Q10 / Q14 fixed-point (values * 1024 / 16384) -> i32 144// - 32-bit color packs (RGBA8888) -> u32 145// - Q20 fixed-point (values * 1048576) -> i64 146// - Wide color packs (RGBA16161616, PRESERVE_ALL) -> u64 147// - Timestamps (ns / us / ms / cycles) -> i64 (2038 Y2K38) 148// - 64-bit hash digests -> u64 149// - Cryptographic hashes (SHA-256, SHA-512) -> STRUCT (multi-word; queued) 150// - Virtual addresses on 64-bit ISA -> u64 151// 152// Each type is a child of its PHYSICALLY-APPROPRIATE parent 153// (i8/u8/i32/u32/i64/u64), not blanket-i64. This breaks the 154// y2k-incestuous trap where renaming i64 N ways pretends to be 155// type discipline while every value silently shares one width. 156 157// ----- TIME family (all i64; ns/us/ms/cycles legitimately need it) ----- 158// 2038 Y2K38 lurks for 32-bit time_t; i64 is the substrate-honest 159// choice. ms/us/ns + cycles all i64. s_q14 needs only i32 range 160// (val*16384 fits comfortably in i32 for typical second scales) but 161// we stay at i64 to compose cleanly with the i64 time arithmetic 162// across the substrate. 163type nx_ns = i64 // nanoseconds (since boot, monotonic) 164type nx_us = i64 // microseconds (since boot, monotonic) 165type nx_ms = i64 // milliseconds (since epoch, wall) 166type nx_s_q14 = i64 // seconds in Q14 fixed-point 167type nx_cycles = i64 // CPU cycle count 168 169// ----- HASH family (non-cryptographic 64-bit; crypto = STRUCT) ----- 170// FNV-1a / xxhash digest is u64 by spec. SHA-256 / SHA-512 / BLAKE 171// hashes are MULTI-WORD; they're declared as structs in 172// nx_sha256.nx / nx_sha512.nx / nx_blake2b.nx (each carries its own 173// fixed-size byte array; NOT i64). 174type nx_hash64 = u64 // FNV-1a / xxhash / truncated SHA -- 64-bit digest 175 176// ----- ETG family (sealed enums; small value space -> u8) ----- 177// nx_outcome_id sealed enum has 11 values; u8 fits 256 178// nx_probe_kind sealed enum has 18 values; u8 fits 256 179// nx_claim_source sealed enum has 13 values; u8 fits 256 180// nx_silicon_serial is a content-addressed identity HASH; u64. 181type nx_outcome_id = u8 // NX_ETG_OUTCOME_* (11 values; u8 fits) 182type nx_probe_kind = u8 // NX_ETG_PROBE_* (18 values; u8 fits) 183type nx_claim_source = u8 // NX_ETG_CLAIM_* (13 values; u8 fits) 184type nx_silicon_serial = u64 // per-die identity hash (cryptographic-strength width) 185 186// ----- PERF family (sealed enums) ----- 187type nx_pathology_id = u8 // NX_PERF_PATH_* (15 values; u8 fits) 188type nx_flow_state_id = u8 // NX_FLOW_STATE_* (6 values; u8 fits) 189 190// ----- FIXED-POINT family (width chosen by precision*range) ----- 191// Q10: value * 1024. Typical seed values are 0..255 so q10 max is 192// ~261K; i32 holds up to ~2.1B -> plenty of headroom. 193// Q14: value * 16384. Typical max around 16K of seed -> q14 ~ 2.6e8; 194// i32 holds up to 2.1e9 -> headroom for a few decimal seconds. 195// Q20: value * 1048576. Wider precision; needs i64 to avoid wrap. 196type nx_q10 = i32 // val * 1024; ~0.001 precision 197type nx_q14 = i32 // val * 16384; ~6e-5 precision 198type nx_q20 = i64 // val * 1048576; ~1e-6 precision 199 200// ----- GRAPHICS family (display coords + color packs at real widths) ----- 201// Modern displays are well within 32-bit pixel addressing. 202// 8K display = 7680x4320 pixels. i32 holds 2.1B -> plenty. 203// nx_color_rgba8 = 32-bit packed RGBA (the common case) 204// nx_color_rgba16 = 64-bit packed RGBA16161616 (HDR / wide gamut) 205type nx_pixel_x = i32 // screen X in pixels 206type nx_pixel_y = i32 // screen Y in pixels 207type nx_color_rgba8 = u32 // RGBA8888 packed 208type nx_color_rgba16 = u64 // RGBA16161616 packed (HDR / preserve-all) 209 210// ----- PERCEPTUAL family (sealed enum; small value space) ----- 211// nx_perceptual_profile has ~40 declared values up through 212// NX_PERCEPT_PRESERVE_ALL = 9999. Sentinel value 9999 needs i16, 213// not u8. i16 fits -32768..32767 with room for sentinels. 214type nx_perceptual_profile = i16 // NX_PERCEPT_* (~40 values + 9999 sentinel) 215 216// ----- ADDRESS family (virtual addresses on 64-bit ISA) ----- 217// Pointer-width is u64 on all our supported 64-bit targets 218// (RV64 / x86_64 / AArch64 / ppc64le / loongarch64 / mips64 / 219// s390x / RV32 uses u32 -- TODO: tier-conditional). 220type nx_addr = u64 // raw virtual address (caller casts to *u8) 221 222// nx_capability_manifest: 223// variant_class: tier_config 224// variant_id: tier_config_v1_global 225// requires_isa: [rv32i, rv32imac, rv64imac, rv64imacv, x86_64, aarch64, armv7a, cortex_m, avr, xtensa, wasm32] 226// requires_syscalls: [] 227// requires_ram_min_b: 0 // pure-const + typedef module, no runtime cost 228// tier_floor: NX_TIER_MCU 229// tier_ceiling: NX_TIER_HPC 230// cost_model: 231// flops_per_n: 0.0 232// bytes_per_n: 0.0 233// syscalls_per_n: 0.0 234// adversary_class: THREAT_OPPORTUNISTIC 235// 236// Note: This file is the substrate's TIER ENUM SOURCE OF TRUTH. It 237// has no variants by design (it IS the variant_class taxonomy that 238// other primitives' tier_floor / tier_ceiling reference). Manifest 239// declared for hygiene completeness; selector will skip it. 240 241// ---- buffer-size constants (use instead of bare numbers) ------- 242 243const NX_BUF_TINY: nx_size = 64 244const NX_BUF_SMALL: nx_size = 256 245const NX_BUF_MEDIUM: nx_size = 4096 246const NX_BUF_LARGE: nx_size = 65536 247const NX_BUF_HUGE: nx_size = 1048576 248 249// ---- hardware tier sentinels ----------------------------------- 250 251const NX_TIER_MCU: nx_int = 0 252const NX_TIER_SOVEREIGN_CHIP: nx_int = 1 253const NX_TIER_FAMILY_DEVICE: nx_int = 2 254const NX_TIER_WORKSTATION: nx_int = 3 255const NX_TIER_SERVER: nx_int = 4 256const NX_TIER_HPC: nx_int = 5 257 258// ---- numeric tier sentinels (informational) -------------------- 259 260const NX_NUM_N0_I32: nx_int = 0 261const NX_NUM_N1_I64: nx_int = 1 262const NX_NUM_N2_I128: nx_int = 2 263const NX_NUM_N3_I256: nx_int = 3 264const NX_NUM_N4_I512: nx_int = 4 265const NX_NUM_N5_BIGINT: nx_int = 5 266 267// ---- byte-width of substrate types (replace bare `8` / `4`) ---- 268// 269// Use these wherever you need the byte count of a substrate type -- 270// e.g., sys_mmap(N * NX_SIZEOF_NX_SIZE) to allocate N nx_size slots. 271// Swap nx_int's underlying type and ONLY this constant changes. 272 273const NX_SIZEOF_NX_INT: nx_size = 8 // nx_int currently i64 -> 8 bytes 274const NX_SIZEOF_NX_SIZE: nx_size = 8 // nx_size always pointer-width 275const NX_SIZEOF_NX_IDX: nx_size = 8 // nx_idx alias of nx_size 276 277// ---- POSIX stdio file descriptors (replace bare 0/1/2) --------- 278 279const NX_FD_STDIN: nx_fd = 0 280const NX_FD_STDOUT: nx_fd = 1 281const NX_FD_STDERR: nx_fd = 2 282 283const SYS_MAGIC_1024: i64 = 1024 284const SYS_MAGIC_1000000: i64 = 1000000 285const SYS_MAGIC_4294967296: i64 = 4294967296 286// first read window for a size-UNKNOWABLE file (lseek END <= 0); doubles while it fills -- see sys_read_file 287const SYS_READ_GROW_INIT: i64 = 65536 288const SYS_MAGIC_100000: i64 = 100000 289 290// ---- syscall numbers (per-target) ---- 291// 292// Cross-target via the macro processor (cardinal landed 2026-05-20: 293// feedback-hardware-agnostic-is-robustness -- the substrate must 294// compile + run on every silicon we point it at). Default path 295// (TARGET_X86_64 not defined) carries Linux RV64 numbers used by 296// qemu-RV64 + NishiOS. When nxc2 is invoked with --target x86_64 297// main.c pre-defines @macro TARGET_X86_64 1 so this file resolves 298// to x86_64 Linux ABI numbers. 299// 300// nx_syscalls_x86_64.nx remains the dedicated x86_64-only mirror 301// for files that want explicit single-target imports (e.g., bench 302// smokes built only for x86_64). This block makes nx_syscalls.nx 303// itself dual-target so substrate primitives compile portably. 304 305@ifdef TARGET_X86_64 306const SYS_READ: i64 = 0 307const SYS_WRITE: i64 = 1 308const SYS_CLOSE: i64 = 3 309const SYS_LSEEK: i64 = 8 310const SYS_OPENAT: i64 = 257 311const SYS_EXIT: i64 = 60 312const SYS_MMAP: i64 = 9 313const SYS_CLOCK_GETTIME: i64 = 228 314const SYS_IOCTL: i64 = 16 315const SYS_CLOCK_NANOSLEEP: i64 = 230 316// Namespace/container family, x86 branch (debt 1785528831). Moved here from 317// nx_syscalls_x86_64.nx so ONE module owns the wrapper set -- a TU reaching both 318// modules used to hold every wrapper TWICE, resolved silently by definition ORDER. 319const SYS_CHROOT: i64 = 161 320const SYS_MOUNT: i64 = 165 321const SYS_UNSHARE: i64 = 272 322const SYS_GETUID: i64 = 102 323const SYS_GETGID: i64 = 104 324const SYS_POLL: i64 = 7 325@endif 326 327@ifndef TARGET_X86_64 328const SYS_READ: i64 = 63 329const SYS_WRITE: i64 = 64 330const SYS_CLOSE: i64 = 57 331const SYS_LSEEK: i64 = 62 332const SYS_OPENAT: i64 = 56 333const SYS_EXIT: i64 = 93 334const SYS_MMAP: i64 = 222 335const SYS_CLOCK_GETTIME: i64 = 113 336const SYS_IOCTL: i64 = 29 337const SYS_CLOCK_NANOSLEEP: i64 = 115 338// Namespace/container family, RV64 branch (debt 1785528831). This is the branch actually 339// KEPT (TARGET_X86_64 is hard-pinned undefined), so these are the numbers the x86 backend 340// translates at emit: 51->161 chroot, 40->165 mount, 97->272 unshare, 174->102 getuid, 341// 176->104 getgid. The 40 and 51 rows were added to x86ctx_rv64_to_x86_64_syscall and 342// shipped FIRST -- without them both would pass through to the WRONG x86 syscall 343// (sendfile / getsockname), silently, because that translator's default is `return num`. 344const SYS_CHROOT: i64 = 51 345const SYS_MOUNT: i64 = 40 346const SYS_UNSHARE: i64 = 97 347const SYS_GETUID: i64 = 174 348const SYS_GETGID: i64 = 176 349const SYS_POLL: i64 = 73 350@endif 351 352func sys_ioctl(fd: i64, request: i64, arg: i64) -> i64 { 353 return __syscall(SYS_IOCTL, fd, request, arg, 0, 0, 0) 354} 355 356// poll(2): wait for events on fds. fds points to an array of `nfds` 357// struct pollfd { i32 fd; i16 events; i16 revents } (8 bytes each). 358// timeout_ms < 0 = block forever, 0 = return immediately. Returns the 359// count of ready fds (>0), 0 on timeout, or -errno. Used by the 360// substrate's own network diagnostics (bounded non-blocking connect) 361// instead of reaching for external tools. (rv64 const = ppoll; this 362// wrapper only runs on the x86_64 target.) 363func sys_poll(fds: *u8, nfds: i64, timeout_ms: i64) -> i64 { 364 return __syscall(SYS_POLL, fds, nfds, timeout_ms, 0, 0, 0) 365} 366 367// ---- core wrappers ---- 368 369func sys_write(fd: i64, buf: *u8, count: i64) -> i64 { 370 return __syscall(SYS_WRITE, fd, buf, count, 0, 0, 0) 371} 372 373func sys_read(fd: i64, buf: *u8, count: i64) -> i64 { 374 return __syscall(SYS_READ, fd, buf, count, 0, 0, 0) 375} 376 377func sys_close(fd: i64) -> i64 { 378 return __syscall(SYS_CLOSE, fd, 0, 0, 0, 0, 0) 379} 380 381// chdir. The compiler only rv64->x86 translates CONSTANT syscall numbers (x86ctx_emit_syscall: 382// VK_CONST_INT); chdir is absent from that table, so a constant 49 falls through to x86_64 bind and a 383// constant 80 is mapped to fstat -- BOTH gave EBADF (PROBE-PROVEN by test_chdir). The documented escape 384// (nx_x86_64_ctx.nx:1004 "Runtime-computed syscall number -- load as-is") is to make op0 RUNTIME: a memory 385// load can't be folded to VK_CONST_INT, so the raw x86_64 number 80 passes through untranslated = real 386// chdir. Used by the supervisor to set a spawned daemon's CWD before execve. 0 on success, -errno on fail. 387func sys_chdir(path: *u8) -> i64 { 388 let nbox: *i64 = sys_mmap(16) as *i64 389 nbox[0] = 80 // x86_64 chdir, forced runtime so the rv64->x86 xlate is skipped 390 return __syscall(nbox[0], path as i64, 0, 0, 0, 0, 0) 391} 392 393// getcwd -- SAME runtime-number escape as sys_chdir directly above, for the same documented reason: the 394// rv64->x86 translator only rewrites CONSTANT syscall numbers, and getcwd is absent from that table, so a 395// constant would be mangled exactly as chdir's was. A memory load cannot be folded to VK_CONST_INT, so the 396// raw x86_64 number passes through untranslated. 397// WHY THIS EXISTS (2026-08-14): the shim had sys_chdir but NOTHING to ask where we are. Every organ that 398// resolves a path against the CWD could therefore only print a RELATIVE path -- a claim whose truth depends 399// on invisible state. Three separate working-directory faults in one session stayed invisible until they 400// bit, and in each the reader could not tell "the file is missing" from "I am standing somewhere else". 401// ★★★AN ORGAN THAT CANNOT REPORT WHERE IT IS CANNOT WRITE AN HONEST PATH. 402// Returns the byte length written INCLUDING the terminator, or -errno (notably -ERANGE if cap is short). 403// SYS_PATH_MAX is exported so a caller never hand-writes the size: the FIRST consumer of sys_getcwd (this 404// author, minutes after adding it) wrote `sys_mmap(4096)` and `sys_getcwd(buf, 4096)` on consecutive 405// lines -- a bare literal AND a duplicate-authored pair, the exact shape being removed elsewhere the same 406// day. ★★A NEW PRIMITIVE THAT DOES NOT EXPORT ITS OWN SIZE INVITES EVERY CALLER TO INVENT ONE. 407const SYS_PATH_MAX: i64 = 4096 // Linux PATH_MAX; getcwd returns -ERANGE below it 408// The DIRECTORY sibling of MODE_0644, added on the same evidence: `0x1ed` appears at 569 sites in 409// buildroot/runtime (nx_shelltool, corpus_complete=1), i.e. the estate scatters TWO file-mode constants, 410// not one. Named here so the pair lives together and a reader meets both at the same place. 411const MODE_0755: i64 = 0x1ed // rwxr-xr-x : default mode for a created directory 412func sys_getcwd(buf: *u8, cap: i64) -> i64 { 413 let nbox: *i64 = sys_mmap(16) as *i64 414 nbox[0] = 79 // x86_64 getcwd, forced runtime so the rv64->x86 xlate is skipped 415 return __syscall(nbox[0], buf as i64, cap, 0, 0, 0, 0) 416} 417 418// ⚠AT_FDCWD MOVED UP 2026-07-20 -- IT WAS A LIVE MISCOMPILE. This const was declared ~60 lines BELOW 419// (in the openat block) while sys_unlinkat and sys_fchmodat immediately below REFERENCE it. A module 420// const referenced ABOVE its declaration does not resolve, and nx_cc silently substituted CONSTANT 0 421// -- so both wrappers passed dirfd=0 (stdin) instead of -100. Absolute paths survive that (openat 422// ignores dirfd when the path is absolute), RELATIVE paths do not, which is exactly why unlinkat was 423// long recorded as flaky and "passing only by luck". Surfaced by the new unknown-identifier 424// diagnostic, which turned a silent 0 into a compile error. LAW (already banked, now enforced): 425// module-wide consts/statics go ABOVE every possible reader. 426const AT_FDCWD: i64 = -100 427 428// unlinkat(AT_FDCWD, path, 0) -- delete a file. x86_64 263 is a PROVEN pass-through (not an rv64 key), 429// but this is THE canonical home: 5+ organs hand-rolled `__syscall(263,...)` before this landed (DRY, 430// 2026-07-20). 0 on success, -errno on fail. 431func sys_unlinkat(path: *u8) -> i64 { 432 return __syscall(263, AT_FDCWD, path as i64, 0, 0, 0, 0) 433} 434 435// fchmodat(AT_FDCWD, path, mode) -- chmod by path. ⚠a CONSTANT 268 gets rv64->x86 TRANSLATED to the 436// wrong syscall (silent no-op chmod -- cost a vacuous-permission-test debug cycle, 2026-07-20), so the 437// number is forced RUNTIME via the sys_chdir nbox pattern. 0 on success, -errno on fail. 438func sys_fchmodat(path: *u8, mode: i64) -> i64 { 439 let nbox: *i64 = sys_mmap(16) as *i64 440 nbox[0] = 268 // x86_64 fchmodat, forced runtime so the xlate is skipped 441 return __syscall(nbox[0], AT_FDCWD, path as i64, mode, 0, 0, 0) 442} 443 444// exit_group(2) -- terminate ALL tasks in the thread group. Raw x86_64 231 445// (231 is NOT an rv64 key in the compiler's swap table, so it passes through 446// untranslated -- the munmap-11 precedent). THE explicit program-exit call 447// once a process holds live nx_thread_pool workers: CLONE_VM tasks are 448// separate PIDs, so plain sys_exit (93 -> x86 60, single task) leaves them 449// running, holding stdout open and wedging any pipeline that waits for EOF 450// (found 2026-07-07: the shared-pool matmul dispatcher hung the build lane 451// this way). Return-from-main already exit_groups via the _start trampoline; 452// use THIS for explicit early program exit. Per-THREAD exit stays sys_exit 453// (see nx_thread_exit). 454func sys_exit_group(code: i64) -> i64 { 455 return __syscall(231, code, 0, 0, 0, 0, 0) 456} 457 458// setpriority(PRIO_PROCESS=0, who=0 -> SELF, prio) -- x86_64 syscall 141. 459// Lower priority = larger nice value; 19 is the maximum yield. 460// WHY A WRAPPER AND NOT AN OPERATOR STEP (measured 2026-07-30): a bulk media 461// migration walk saturated the NAS; every forked organ queued behind its I/O so 462// EVERY agent MCP call 503'd for minutes -- the control plane went blind while a 463// background job did exactly what it was told. `renice 19` on the running pid 464// restored interactive service at once. 465// LAW: a long-running BULK job must yield to the interactive control plane BY 466// CONSTRUCTION at its own launch, not when an operator notices. Bind it to the 467// one act every bulk job performs (its startup) and nothing has to remember it. 468// WARN: `ionice` does NOT exist on the Synology busybox, so the I/O-class lever 469// is unavailable; CPU nice sufficed because the walk is SHA-256-bound over 470// cached reads (state R, not D, once niced). 471func sys_setpriority(prio: i64) -> i64 { 472 return __syscall(141, 0, 0, prio, 0, 0, 0) 473} 474 475// ADDITIVE TWIN 2026-08-04 (nx_resgov): re-nice ANOTHER process by pid. The incumbent above pins 476// who=0 = "me", so it cannot deprioritise a runaway -- and a governor that can only slow ITSELF has 477// no graceful rung between "observe" and "kill". PRIO_PROCESS=0, who=pid. Existing callers untouched 478// (rule 19: add the new entry point, never re-shape the one in service). 479func sys_setpriority_of(pid: i64, prio: i64) -> i64 { 480 return __syscall(141, 0, pid, prio, 0, 0, 0) 481} 482 483// munmap -- free a region from sys_mmap. x86_64 munmap = 11; 11 is NOT an rv64 number in the compiler's 484// swap table, so the literal passes through untranslated = real munmap (unlike chdir, where rv64 80=fstat 485// intercepted it). CRITICAL for long-running loops: the supervisor's per-poll proc_* scans mmap 64KB+ each; 486// unfreed, the leak hits DSM's RLIMIT_AS -> mmap returns -12 -> the code writes through it -> SEGFAULT 487// (dmesg-proven: nx_hostctl segfault at 0xfffffffffffffff4). Free scan buffers to keep the supervisor alive. 488// ===== SMALL-ALLOCATION BUMP ARENA (2026-08-06, debt 1785516350 / 1786055008) ===================== 489// MEASURED FIRST, THEN BUILT. nx_arena_probe: 20,000 x sys_mmap(32) -> VmSize 80,172 kB, 490// VmRSS 80,024 kB. 640 KB of requested data cost 78 MB of RESIDENT memory -- 4096 bytes per 32-byte 491// request, exactly one page and one kernel VMA each. Across the corpus nx_mmapbal deep counts 17,157 492// functions / 43,498 sites that allocate and never return, so this multiplier is the actual shape of 493// the leak: the call sites are not individually wrong so much as individually EXPENSIVE. 494// 495// One VMA per call is also a HARD CORRECTNESS CEILING, not just a memory cost: vm.max_map_count 496// defaults to 65530, after which mmap returns -ENOMEM and callers write through the failed pointer. 497// That is precisely the dmesg-proven nx_hostctl SEGFAULT at 0xfffffffffffffff4 described below. 498// 499// SO: requests <= NXA_SMALL_MAX are bump-allocated out of a 256 KiB chunk (one VMA per ~5,400 small 500// allocations instead of one per allocation). Larger requests take the ORIGINAL path untouched -- 501// they are the ones plausibly relying on page alignment, and they are not where the leak lives. 502// 503// THE ZEROING CONTRACT IS LOAD-BEARING AND IS PRESERVED BY NEVER RECYCLING. Callers rely on mmap 504// returning zeroed memory (nx_mmapbal: "mmap zeroes, so an untouched slot reads empty with no init 505// loop"). Bytes handed out here come from a freshly mmapped chunk and are NEVER handed out twice, so 506// every region is zero-filled exactly as before. LIFO give-back on munmap was deliberately REJECTED: 507// it would recover memory but hand back dirty bytes, silently breaking every caller that trusts the 508// zero -- a correctness regression traded for a memory win, which is the wrong trade. 509// 510// KNOWN TRADE-OFF, stated rather than hidden: small allocations are now ADJACENT within a chunk 511// instead of isolated in their own pages. An overrun that today walks off the end of a page and 512// SIGSEGVs loudly may instead corrupt a neighbouring allocation quietly. NXA_GAP puts slack between 513// allocations and NXA_SMALL_MAX is kept deliberately low to bound the exposure, but the risk is real 514// and is the reason this starts at 256 rather than a page. 515// ---- MEMORY ORDERING, THE ONE DEFINITION ------------------------------------------------------- 516// Moved here from nx_atom.nx on 2026-08-25 and DELETED from its two other copies 517// (nx_atomic_intrinsic_test, nx_simd_i32x8_test). Measured before the move, corpus_complete=1: 518// THREE files each declared NX_MO_SEQ_CST = 5 independently. A constant written in three places is 519// three rulers that agree until one of them does not. 520// 521// They live at THIS layer because the arena allocator below needs an ordering value for its own 522// lock, and this file cannot import nx_atom.nx -- nx_atom imports THIS file, so that direction is a 523// cycle. Everything that had these constants still has them: nx_atom.nx imports this file, and so 524// does every consumer of nx_atom. 525// 526// The __atomic_* forms these feed are COMPILER INTRINSICS, not library calls, so this file can use 527// them with no import at all. Verified in nx_x86_64_ctx rather than assumed: __atomic_cas_i64 emits 528// `lock cmpxchgq`, __atomic_faa_i64 emits `lock xaddq`, __atomic_fence emits `mfence`. On x86-64 the 529// ordering operand is not consulted by the emitter because those instructions are full barriers 530// regardless; it is carried for the RV64A backend, where it selects the aq/rl bits. 531const NX_MO_RELAXED: i64 = 0 532const NX_MO_CONSUME: i64 = 1 533const NX_MO_ACQUIRE: i64 = 2 534const NX_MO_RELEASE: i64 = 3 535const NX_MO_ACQ_REL: i64 = 4 536const NX_MO_SEQ_CST: i64 = 5 537 538const NXA_SMALL_MAX: i64 = 256 539const NXA_CHUNK: i64 = 262144 540const NXA_ALIGN: i64 = 16 541const NXA_GAP: i64 = 16 542const NXA_STATE: i64 = 4096 543// RING CANARY (temporary diagnostic): the single-slot canary checked only the immediately 544// previous allocation and reported ZERO overruns -- but the bisection proved the write is 545// DELAYED, landing after later allocations have been served. Track the last NXA_RING 546// allocations and re-verify every one of them on each call. Lives at i64 slot NXA_RBASE in 547// the state page; the reporter borrows bytes 64/128, so 512 is clear of it. 548const NXA_RING: i64 = 128 549const NXA_RBASE: i64 = 64 550// ---- ARENA MARK/RESET (2026-08-12, additive; the durable fix for bump-without-reset). The arena 551// abandons a full chunk on rollover, so a long-running accept loop accumulates chunks into one giant 552// coalesced VMA (hub_gw MEASURED 3.4GB over 64k requests). A daemon marks the arena AFTER startup and 553// resets at its accept-loop's quiescent point; reset munmaps every chunk allocated since the mark and 554// zeroes the marked chunk's reclaimed tail, so per-request small allocations reuse a bounded slab. 555// State slots (state page is 512 i64): [3]=chunk_count [4]=mark_valid [5]=mark_bump [6]=mark_chunk_end 556// [7]=mark_chunk_count; the chunk-base list lives at slots NXA_CHUNKBASE..+NXA_CHUNKMAX (clear of the 557// ring at 64..320 and the reporter scratch below 64). CONTRACT: the caller guarantees NO arena 558// allocation made after the mark is still referenced at reset (the accept-loop top, where the previous 559// request's frames have all returned -- the same quiescent point ss_cache_reap already uses). LARGE 560// (>NXA_SMALL_MAX) allocations take their own VMA and are NOT tracked here; a per-request large mmap 561// still needs its own munmap. Untracked-overflow (>NXA_CHUNKMAX chunks between resets) degrades to the 562// old leak for the excess, never corrupts. 563// ---- ARENA MUTUAL EXCLUSION (2026-08-25) ------------------------------------------------------- 564// THE DEFECT: the bump-pointer advance below was a plain read-modify-write -- 565// let p: i64 = nxa_st[0] 566// nxa_st[0] = p + need 567// -- so two threads that read nxa_st[0] before either wrote it BOTH RECEIVE THE SAME POINTER and 568// then write over each other. The chunk refill, the ring-canary scan and the nxa_st[2] counter have 569// the same shape. MEASURED while shipping structured concurrency: eight pool workers calling a 570// helper that allocates a 16-byte timespec raced this cursor and produced ARENA-OVERRUN 571// prev_alloc_size=16 followed by SIGSEGV. It generalises to EVERY small allocation from more than 572// one thread, which is why the scoped-spawn child body was written to allocate nothing at all. 573// 574// WHY A LOCK AND NOT A LOCK-FREE BUMP. A fetch-and-add on the cursor fixes only the fast path; two 575// threads can still both observe the chunk exhausted and both refill, and the canary ring and the 576// counter would still race. One lock over the whole mutable region is correct by inspection, which 577// on the allocator that every organ in the estate calls is worth more than a clever fast path. 578// THE COST IS NOT THE DOMINANT COST HERE: this function ALREADY walks all NXA_RING canary slots on 579// every allocation, so one uncontended `lock cmpxchgq` is far below the noise of work already done. 580// 581// SLOT 4 IS FREE BY THE LAYOUT ABOVE: [0] cursor, [1] limit, [2] ring counter, [3] chunk count, and 582// the ring starts at NXA_RBASE=64. It is also clear of the byte-64 and byte-128 scratch that 583// nxa_report_overrun formats digits into (slots 8 and 16), which slot 4 (bytes 32-39) does not touch. 584const NXA_LOCK: i64 = 4 585// A BOUND ON AN UNKNOWABLE WAIT, DERIVED RATHER THAN PICKED, AND ITS EXHAUSTION ANNOUNCES. The 586// longest thing the critical section can do is the NXA_RING canary scan plus one mmap, so a spin far 587// beyond that is not contention -- it is a holder that is never coming back. Eight times the ring 588// gives an order of magnitude of headroom over the longest legitimate hold; on reaching it the 589// allocator SAYS SO on stderr once and keeps waiting, because hanging visibly is recoverable and 590// corrupting silently is not, and dying inside the allocator would take down a process that may be 591// merely slow. 592const NXA_LOCK_WARN: i64 = NXA_RING * 8 593// Slot 5: "the contention hint has already been printed by this process". Also free by the layout 594// above and clear of every scratch region. It is a FLAG, not a counter, and it is set through a CAS 595// so the once-ness is itself race-free rather than depending on the lock it reports about. 596const NXA_LOCK_WARNED: i64 = 5 597 598const NXA_CHUNKBASE: i64 = 320 599const NXA_CHUNKMAX: i64 = 192 600 601// [0] = next free byte, [1] = one past the end of the current chunk. A static POINTER to a real 602// mmapped page rather than scalar statics, matching the idiom the corpus already proves; the state 603// page is taken through __syscall directly so this can never recurse into itself. 604static nxa_st: *i64 605 606// munmap -- free a region from sys_mmap. x86_64 munmap = 11; 11 is NOT an rv64 number in the compiler's 607// swap table, so the literal passes through untranslated = real munmap (unlike chdir, where rv64 80=fstat 608// intercepted it). CRITICAL for long-running loops: the supervisor's per-poll proc_* scans mmap 64KB+ each; 609// unfreed, the leak hits DSM's RLIMIT_AS -> mmap returns -12 -> the code writes through it -> SEGFAULT 610// (dmesg-proven: nx_hostctl segfault at 0xfffffffffffffff4). Free scan buffers to keep the supervisor alive. 611// 612// A small len means the region came from the bump arena above, because sys_mmap routes by the SAME 613// threshold. Unmapping an interior pointer would tear a hole in a chunk still holding other callers' 614// live allocations, so it is a no-op here. Balanced small callers therefore no longer return memory -- 615// but they now cost ~48 bytes instead of 4096, so the arena wins by two orders of magnitude even 616// against code that was already correct. 617// Matching release for sys_mmap_try and other whole kernel mappings. 618// Never pass an arena allocation from sys_mmap: its small pointers may be interior. 619// Preserve the requested mapping length; the kernel applies its page rounding. 620const NXA_MAP_INVALID:i64=0-22 // Linux EINVAL, a protocol value rather than a resource budget. 621func sys_munmap_direct(addr:*u8,len:i64)->i64{ 622 if (addr as i64)<=0||len<=0{return NXA_MAP_INVALID} 623 return __syscall(11,addr as i64,len,0,0,0,0) 624} 625 626func sys_munmap(addr: *u8, len: i64) -> i64 { 627 if len <= NXA_SMALL_MAX { return 0 } 628 return __syscall(11, addr as i64, len, 0, 0, 0, 0) 629} 630 631// Seek within a file. whence: 0=SEEK_SET, 1=SEEK_CUR, 2=SEEK_END. 632// Returns new file offset on success, -errno on failure. 633func sys_lseek(fd: i64, offset: i64, whence: i64) -> i64 { 634 return __syscall(SYS_LSEEK, fd, offset, whence, 0, 0, 0) 635} 636 637// ---- FILESYSTEM SPACE: THE AXIS THE ESTATE DID NOT HAVE (2026-08-28) ----------------------------- 638// WHY THIS IS HERE AND NOT LEFT WHERE IT WAS. On 2026-08-28 a 100%-FULL DISK truncated a sibling seat's 639// MEMORY.md to 0 bytes -- open(path,"w") truncates before it writes, so a full volume does not refuse a 640// write, it DESTROYS the file. Nothing in the estate saw it coming: nx_resmon is "the resource axis 641// nx_health lacks" for MEMORY and SWAP, and a search for the disk primitive returned matches=0 for BOTH 642// sys_statfs and statvfs with corpus_complete=1. nx_res_census records the same absence in its own header. 643// The capability was not missing, it was DARK: nx_system_triage.tr_free_gb has read filesystem space since 644// 2026-06-10, in an _hdl_build organ that is NOT REGISTERED (nx_job_run refuses it as "not an unpinned 645// GREEN tool"), so the one instrument that could have warned was unreachable by any caller. 646// A CAPABILITY THAT EXISTS IN ONE UNREACHABLE ORGAN IS INDISTINGUISHABLE FROM ONE NOBODY BUILT. 647// 648// WHY THE RAW 137 AND NOT A SYS_ CONST. This file's dual-arch blocks are gated on TARGET_X86_64, which is 649// HARD-PINNED UNDEFINED, so the RV64 branch is what compiles and the x86 backend translates each number at 650// emit through x86ctx_rv64_to_x86_64_syscall -- whose default is `return num`. There is NO row for RV64 43 651// (statfs), so a SYS_STATFS=43 const would pass through unmapped to x86_64 43 = ACCEPT: a different 652// syscall, silently, on a path pointer. That is not a hypothesis -- nx_system_triage PROBE-PROVED it on 653// 2026-06-10: "rv64 43 returns -9 through the translation table; 137 raw matches df exactly." So 137 is 654// the MEASURED-CORRECT number for the target we actually emit, and it is named here ONCE instead of 655// sitting as a bare literal at each call site. 656// ⚠NAMED FOLLOW-UP, conflict-checked and deliberately NOT taken here: adding `if num == 43 { return 137 }` 657// to x86ctx_rv64_to_x86_64_syscall would make the arch-correct const work too. Nothing passes 43 as an x86 658// number (43 appears only as a translation TARGET, from RV64 202 accept), so the row is safe -- but it is a 659// COMPILER change that activates only on the next nx_cc self-host rebuild, and the working path needs none. 660// 661// struct statfs (x86_64) as i64 slots: 0 f_type, 1 f_bsize, 2 f_blocks, 3 f_bfree, 4 f_bavail, 5 f_files. 662// f_bavail (not f_bfree) is the honest number for "will my write succeed": it excludes the root reserve, so 663// it reports FULLER than root would see. Wrong in the safe direction, and said out loud rather than implied. 664// ⚠THE IMPRECISION, MEASURED AND NAMED SO NOBODY LATER "FIXES" IT INTO AGREEING WITH df: this permil is 665// NOT df's Use%. df computes Used/(Used+Available), which EXCLUDES the root-reserved blocks from its 666// denominator; this computes (blocks-bavail)/blocks, which counts the reserve as used. VERIFIED against df 667// on 2026-08-28: avail_bytes came back 958449582080, which is EXACTLY df's Available of 935985920 KiB, while 668// the same volume read 113 permil here and 7% there -- both correct, measuring different things. Both reach 669// their maximum at the SAME event (bavail = 0), so a threshold calibrated against THIS metric alarms at the 670// same moment a writer actually hits the wall; it simply sits higher below that. Calibrate thresholds to 671// this definition, and do not import a df-derived number as if it were the same quantity. 672const SYS_STATFS_X86_MEASURED: i64 = 137 673const STATFS_BUF_BYTES: i64 = 144 674const STATFS_I_BSIZE: i64 = 1 675const STATFS_I_BLOCKS: i64 = 2 676const STATFS_I_BAVAIL: i64 = 4 677const STATFS_PERMIL: i64 = 1000 678const STATFS_ERR: i64 = 0 - 1 679 680// raw statfs into a caller-supplied 144-byte buffer. 0 = ok, non-zero = the kernel's negative errno. 681func sys_statfs(path: *u8, buf: *i64) -> i64 { 682 return __syscall(SYS_STATFS_X86_MEASURED, path, buf, 0, 0, 0, 0) 683} 684 685// bytes available to a non-root writer on the filesystem holding `path`; STATFS_ERR if statfs failed. 686func sys_fs_avail_bytes(path: *u8) -> i64 { 687 let buf: *i64 = sys_mmap(STATFS_BUF_BYTES) as *i64 688 if sys_statfs(path, buf) != 0 { return STATFS_ERR } 689 return buf[STATFS_I_BSIZE] * buf[STATFS_I_BAVAIL] 690} 691 692// USED per-mille of the filesystem holding `path`, counted against what a non-root writer can reach: 693// (blocks - bavail) * 1000 / blocks. STATFS_ERR if statfs failed or the volume reports zero blocks -- 694// an UNMEASURABLE volume must never read as 0 permil used, which is the most flattering possible lie. 695func sys_fs_used_permil(path: *u8) -> i64 { 696 let buf: *i64 = sys_mmap(STATFS_BUF_BYTES) as *i64 697 if sys_statfs(path, buf) != 0 { return STATFS_ERR } 698 let blocks: i64 = buf[STATFS_I_BLOCKS] 699 if blocks <= 0 { return STATFS_ERR } 700 let avail: i64 = buf[STATFS_I_BAVAIL] 701 return ((blocks - avail) * STATFS_PERMIL) / blocks 702} 703 704func sys_exit(code: i64) -> i64 { 705 return __syscall(SYS_EXIT, code, 0, 0, 0, 0, 0) 706} 707 708// mmap anonymous R/W memory; returns raw bytes. Fixed flags: 709// PROT_READ|PROT_WRITE = 3, MAP_PRIVATE|MAP_ANONYMOUS = 0x22, fd=-1. 710// FAIL-CLOSED ON A REFUSED MAPPING (2026-08-07). MEASURED: the corpus has 90,817 sys_mmap call sites 711// and SIX of them check the result -- all six in test probes whose response is sys_exit anyway. So 712// 90,811 sites take whatever this returns and write through it. When the kernel refuses, that value is 713// -errno, and the write lands at 0xfffffffffffffff4 (-12, ENOMEM). That is not a hypothetical: dmesg 714// on this host recorded it hourly in nx_web_shard_compact, and 18 times in nx_web_crawl_step. 715// Returning a poisoned pointer to 90,811 unguarded callers is the defect. Dying here is strictly safer 716// than dying there: the process ends either way, but this way there is no memory corruption first and 717// the failure is NAMED instead of arriving as a bare segfault address an operator has to decode. 718// This is the never-brick shape -- fail-safe BY CONSTRUCTION, not by every caller remembering. 719// KNOWN COST, stated: nx_mmap_probe / test_munmap deliberately provoke a refusal to observe it. They 720// now exit here with code 12 rather than printing their own verdict. Six probes lose a diagnostic; 721// 90,811 sites stop corrupting memory. 722// ===== TEMPORARY DIAGNOSTIC -- ARENA OVERRUN CANARY (2026-08-07) ===================================== 723// ⛔DO NOT BLESS A COMPILER BUILT WITH THIS. The canary writes 0xC7 into the NXA_GAP slack that a 724// caller could otherwise legitimately read as zeros, so it changes observable behaviour for any code 725// that reads past its declared size -- which is precisely the code being hunted. 726// PURPOSE: at NXA_SMALL_MAX=256 the compiler produces 14 SPURIOUS type diagnostics (it reports 727// `arg 2 is an INTEGER but the parameter is a POINTER` against a parameter DECLARED `j: *u8`), i.e. 728// something writes past its allocation and corrupts the parser's type table. At threshold 64 the same 729// requests each get a 4096-byte page whose slack absorbs it. Reading the source found nothing: the 730// two obvious suspects (nx_ir.nx:70 sys_mmap(104), nx_parse.nx:868 sys_mmap(256)) are both correctly 731// sized and bounded. So stop reading and MEASURE: stamp each small allocation's gap, verify the 732// PREVIOUS one on the next call, and print the size of whichever allocation was overrun. 733// Writes to fd 2 without allocating -- it borrows scratch inside the arena state page, because a 734// reporter that called sys_mmap would recurse into the thing it is instrumenting. 735// Dump n bytes at src to fd 2, unprintables as '.', using scratch at state+256 (the ring starts at 736// state+512 and the decimal scratch sits at +64/+128, so this cannot collide with either). n is 737// capped by callers at 48 so the buffer stays clear of the ring. 738func nxa_dump_printable(src: i64, n: i64) -> i64 { 739 let o: *u8 = ((nxa_st as i64) + 256) as *u8 740 var i: i64 = 0 741 while i < n { 742 let sp: *u8 = (src + i) as *u8 743 var c: i64 = sp[0] as i64 744 if c < 32 { c = 46 } 745 if c > 126 { c = 46 } 746 o[i] = c as u8 747 i = i + 1 748 } 749 o[n] = 10 as u8 750 sys_write(2, o, n + 1) 751 return 0 752} 753 754// FINGERPRINT (2026-08-12): the size alone + all-zeros byte dump never named the site. The ring already 755// records each allocation's REQUESTED size in counter order, so the recent size SEQUENCE fingerprints the 756// code path that was running when the overrun landed (a distinctive run of sizes is near-unique to a 757// function). Writes to fd 2 borrowing state-page scratch at bytes 320/340 (clear of the ring at byte 512, 758// the reporter decimals at 64/128, and the byte-dump at 256). No allocation -- must not recurse into sys_mmap. 759func nxa_dump_sizes() -> i64 { 760 sys_write(2, " ring_sizes(old->recent): " as *u8, 27) 761 let scr: *u8 = ((nxa_st as i64) + 320) as *u8 762 let out2: *u8 = ((nxa_st as i64) + 340) as *u8 763 let cnt: i64 = nxa_st[2] 764 var start: i64 = cnt - 32 765 if start < 0 { start = 0 } 766 var idx: i64 = start 767 while idx < cnt { 768 let slot: i64 = idx % NXA_RING 769 let szv: i64 = nxa_st[NXA_RBASE + slot * 2 + 1] 770 var m: i64 = szv 771 var k: i64 = 0 772 if m == 0 { scr[0] = 48 as u8; k = 1 } 773 while m > 0 { scr[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 774 var j: i64 = 0 775 while j < k { out2[j] = scr[k - 1 - j]; j = j + 1 } 776 out2[k] = 44 as u8 777 sys_write(2, out2, k + 1) 778 idx = idx + 1 779 } 780 sys_write(2, "\n" as *u8, 1) 781 return 0 782} 783 784func nxa_report_overrun(sz: i64, gs: i64) -> i64 { 785 let msg: *u8 = "ARENA-OVERRUN prev_alloc_size=" as *u8 786 var n: i64 = 0 787 while msg[n] != (0 as u8) { n = n + 1 } 788 sys_write(2, msg, n) 789 let b: *u8 = ((nxa_st as i64) + 64) as *u8 790 let o: *u8 = ((nxa_st as i64) + 128) as *u8 791 var m: i64 = sz 792 var k: i64 = 0 793 if m == 0 { b[0] = 48 as u8; k = 1 } 794 while m > 0 { b[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 795 var i: i64 = 0 796 while i < k { o[i] = b[k - 1 - i]; i = i + 1 } 797 o[k] = 10 as u8 798 sys_write(2, o, k + 1) 799 // The SIZE alone did not name the site (four 80-byte victims, and the two unbounded 80-byte 800 // buffers in nx_parse.nx were sized from their inputs with no effect). So show the DATA: the 801 // victim's own bytes identify the buffer, and the bytes written past its end identify the WRITER. 802 let algn: i64 = (sz + NXA_ALIGN - 1) / NXA_ALIGN * NXA_ALIGN 803 let base: i64 = gs - algn 804 var dn: i64 = sz 805 if dn > 48 { dn = 48 } 806 sys_write(2, " own : " as *u8, 8) 807 nxa_dump_printable(base, dn) 808 sys_write(2, " over: " as *u8, 8) 809 nxa_dump_printable(gs, 16) 810 nxa_dump_sizes() 811 return 0 812} 813 814func nxa_die(msg: *u8) -> i64 { 815 var n: i64 = 0 816 while msg[n] != (0 as u8) { n = n + 1 } 817 sys_write(2, msg, n) 818 sys_exit(12) 819 return 0 820} 821 822// Address of the arena lock word. Valid only once nxa_st exists; every caller below has already 823// ensured that, and the state-page creation itself is discussed at the take site. 824func nxa_lock_addr() -> *i64 { 825 return ((nxa_st as i64) + NXA_LOCK * 8) as *i64 826} 827 828// __atomic_cas_i64 returns 1 when it wrote and 0 when it did not, so the spin condition is == 0. 829// It is a COMPILER INTRINSIC, not a call into nx_atom -- that module imports THIS file, so importing 830// it back would be a cycle. Verified in nx_x86_64_ctx rather than assumed: it lowers to a genuine 831// `lock cmpxchgq` followed by sete, which is a full barrier on x86-64 whatever ordering is passed. 832func nxa_lock_take() -> i64 { 833 var spins: i64 = 0 834 while __atomic_cas_i64(nxa_lock_addr(), 0, 1, NX_MO_ACQUIRE) == 0 { 835 spins = spins + 1 836 // Fires EXACTLY ONCE, on equality rather than on exceeding, so a genuinely long wait reports 837 // itself without turning the allocator into a log generator. 838 if spins == NXA_LOCK_WARN { 839 // ONCE PER PROCESS, not once per acquisition. MEASURED 2026-08-25 and this is a 840 // correction to the first cut of this very function: it fired on equality per CALL, and 841 // eight workers contending LEGITIMATELY produced hundreds of identical lines in a single 842 // gate run. A DIAGNOSTIC THAT FIRES CONSTANTLY IS ONE EVERY READER LEARNS TO IGNORE, and 843 // this one writes to the stderr of every organ in the estate. 844 // The threshold was derived from the longest the critical section can run, which bounds 845 // ONE hold and says nothing about QUEUE DEPTH: with N threads waiting, a legitimate wait 846 // is N holds and can exceed any per-section derivation. So this is a NOISE FLOOR for a 847 // hint, never a correctness bound -- it never fails, never delays, and never repeats. 848 // The flag is set through a CAS so the once-ness cannot itself race. 849 let wflag: *i64 = ((nxa_st as i64) + NXA_LOCK_WARNED * 8) as *i64 850 if __atomic_cas_i64(wflag, 0, 1, NX_MO_ACQ_REL) == 1 { 851 let m: *u8 = "ARENA-LOCK: sustained allocator contention seen (reported once per process; a hint, not an error -- allocation proceeds normally).\n" as *u8 852 var mn: i64 = 0 853 while m[mn] != (0 as u8) { mn = mn + 1 } 854 sys_write(2, m, mn) 855 } 856 } 857 } 858 return 0 859} 860 861func nxa_lock_give() -> i64 { 862 // nx_cc refuses a bare intrinsic statement ("computes a value and never uses it") and an atomic 863 // store has no result worth using, so it is bound and discarded -- the same shape nx_atom uses 864 // for exactly this reason. The contract is unchanged: this returns 0 either way. 865 let discarded: i64 = __atomic_store_i64(nxa_lock_addr(), 0, NX_MO_RELEASE) 866 if discarded != 0 { return 0 } 867 return 0 868} 869 870// Optional mapping for request boundaries that must report allocation refusal. 871// Unlike sys_mmap, this never aborts the process and never consumes arena storage. 872// Release successful mappings with sys_munmap_direct, not the arena-aware sys_munmap. 873// A successful reservation can still fail on later physical-memory pressure; callers 874// must not describe virtual address admission as guaranteed resident RAM. 875func sys_mmap_try(size:i64)->*u8 { 876 if size<=0 { return 0 as *u8 } 877 let mapped:i64=__syscall(SYS_MMAP,0,size,3,0x22,-1,0) 878 if mapped<=0 { return 0 as *u8 } 879 return mapped as *u8 880} 881 882func sys_mmap(size: i64) -> *u8 { 883 // Large requests keep the EXACT original behaviour, byte for byte: page-aligned, own VMA. Any 884 // caller that depends on page alignment is allocating at least a page, so the arena cannot reach 885 // it. Every failure path below also falls back to this same call, so an exhausted arena degrades 886 // to the old allocator rather than returning a bad pointer. 887 if size > NXA_SMALL_MAX { 888 let big: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 889 if big <= 0 { nxa_die("FATAL sys_mmap: kernel refused a large mapping (ENOMEM). Refusing to return a poisoned pointer -- a write through it would corrupt memory.\n" as *u8) } 890 return big as *u8 891 } 892 if (nxa_st as i64) == 0 { 893 let s: i64 = __syscall(SYS_MMAP, 0, NXA_STATE, 3, 0x22, -1, 0) 894 if s <= 0 { 895 // arena state page refused -- degrade to the plain allocator, and only die if THAT fails too 896 let f1: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 897 if f1 <= 0 { nxa_die("FATAL sys_mmap: kernel refused the arena state page AND the fallback mapping (ENOMEM).\n" as *u8) } 898 return f1 as *u8 899 } 900 nxa_st = s as *i64 901 } 902 // EVERYTHING FROM HERE TO THE RETURN TOUCHES SHARED STATE: the cursor, the limit, the chunk 903 // table, the canary ring and the ring counter. It is ONE critical section because the refill 904 // decision and the bump that depends on it cannot be separated without reintroducing the race. 905 // The state page itself is created ABOVE this point, unlocked: two threads arriving there 906 // together would each map a page and one would win the static, leaking the other's 4 KiB but 907 // corrupting nothing, and in practice the arena is warm long before any thread is spawned 908 // because spawning one allocates. That residual is NAMED here rather than papered over. 909 nxa_lock_take() 910 var need: i64 = size 911 if need <= 0 { need = 1 } 912 need = (need + NXA_ALIGN - 1) / NXA_ALIGN * NXA_ALIGN + NXA_GAP 913 if nxa_st[0] + need > nxa_st[1] { 914 let c: i64 = __syscall(SYS_MMAP, 0, NXA_CHUNK, 3, 0x22, -1, 0) 915 if c <= 0 { 916 // chunk refused -- degrade to the plain allocator, and only die if THAT fails too. 917 // RELEASE FIRST: this is the one path that leaves the critical section early, and a lock 918 // held across a degraded return would wedge every other allocator in the process. 919 nxa_lock_give() 920 let f2: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 921 if f2 <= 0 { nxa_die("FATAL sys_mmap: kernel refused an arena chunk AND the fallback mapping (ENOMEM).\n" as *u8) } 922 return f2 as *u8 923 } 924 nxa_st[0] = c 925 nxa_st[1] = c + NXA_CHUNK 926 // track the chunk base so arena_reset can munmap post-mark chunks (additive; guarded at cap). 927 if nxa_st[3] < NXA_CHUNKMAX { nxa_st[NXA_CHUNKBASE + nxa_st[3]] = c; nxa_st[3] = nxa_st[3] + 1 } 928 } 929 // ---- RING CANARY (temporary diagnostic) ---- 930 var rk: i64 = 0 931 while rk < NXA_RING { 932 let gs0: i64 = nxa_st[NXA_RBASE + rk * 2] 933 if gs0 != 0 { 934 var bi: i64 = 0 935 var bad: i64 = 0 936 while bi < 8 { 937 let bp: *u8 = (gs0 + bi) as *u8 938 if bp[0] != (199 as u8) { bad = 1; bi = 8 } else { bi = bi + 1 } 939 } 940 if bad == 1 { 941 nxa_report_overrun(nxa_st[NXA_RBASE + rk * 2 + 1], gs0) 942 nxa_st[NXA_RBASE + rk * 2] = 0 943 } 944 } 945 rk = rk + 1 946 } 947 let p: i64 = nxa_st[0] 948 nxa_st[0] = p + need 949 let gs: i64 = p + need - NXA_GAP 950 var gj: i64 = 0 951 while gj < NXA_GAP { let q: *u8 = (gs + gj) as *u8; q[0] = 199 as u8; gj = gj + 1 } 952 let slot: i64 = nxa_st[2] % NXA_RING 953 nxa_st[NXA_RBASE + slot * 2] = gs 954 nxa_st[NXA_RBASE + slot * 2 + 1] = size 955 nxa_st[2] = nxa_st[2] + 1 956 // The ONLY other exit from the critical section is the degraded chunk-refill path above, which 957 // releases before it returns. Every shared write is now behind this pair. 958 nxa_lock_give() 959 return p as *u8 960} 961 962// arena_mark: force the arena warm (so a first chunk + state page exist), then record the current 963// position as the reset barrier. Returns 1. A daemon calls this ONCE after startup, before its loop. 964func sys_arena_mark() -> i64 { 965 let warm: *u8 = sys_mmap(1) // ensures nxa_st + chunk[0] exist; the 1 byte is itself arena scratch 966 if (warm as i64) == 0 { return 0 } 967 nxa_st[4] = 1 968 nxa_st[5] = nxa_st[0] 969 nxa_st[6] = nxa_st[1] 970 nxa_st[7] = nxa_st[3] 971 return 1 972} 973 974// arena_reset: reclaim everything allocated since the mark. munmap post-mark chunks, restore the bump 975// to the mark, ZERO the marked chunk's reclaimed tail (preserves the mmap-returns-zeroed contract for 976// recycled bytes), and CLEAR the ring canary (its stamps may point into a just-munmap'd chunk, and a 977// stale deref on the next alloc would SEGV). Returns 1 on reset, 0 if no mark was set. 978func sys_arena_reset() -> i64 { 979 if (nxa_st as i64) == 0 { return 0 } 980 if nxa_st[4] != 1 { return 0 } 981 var i: i64 = nxa_st[7] 982 while i < nxa_st[3] { 983 let cb: i64 = nxa_st[NXA_CHUNKBASE + i] 984 if cb != 0 { __syscall(11, cb, NXA_CHUNK, 0, 0, 0, 0); nxa_st[NXA_CHUNKBASE + i] = 0 } 985 i = i + 1 986 } 987 nxa_st[3] = nxa_st[7] 988 nxa_st[0] = nxa_st[5] 989 nxa_st[1] = nxa_st[6] 990 var z: i64 = nxa_st[0] 991 while z < nxa_st[1] { let q: *u8 = z as *u8; q[0] = 0 as u8; z = z + 1 } 992 var r: i64 = 0 993 while r < NXA_RING * 2 { nxa_st[NXA_RBASE + r] = 0; r = r + 1 } 994 nxa_st[2] = 0 995 return 1 996} 997 998// mmap anonymous SHARED R/W memory -- ONE region that survives fork() so all 999// children see each other's writes (MAP_SHARED|MAP_ANONYMOUS = 0x21). Allocate 1000// in the PARENT before fork. Foundation for the fork-per-connection video relay 1001// (peers in separate children share the per-room frame table). 1002func sys_mmap_shared(size: i64) -> *u8 { 1003 let r: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x21, -1, 0) 1004 return r as *u8 1005} 1006 1007// madvise(2) -- prefetch/advice hints for mapped ranges. MADV_WILLNEED=3 batches page-ins so a 1008// serial fault loop over a cold file-backed mmap becomes parallel disk readahead (the dp-web-pub 1009// stage-2 p95 fix, 2026-08-12). RAW x86_64 NUMBER 28 ON PURPOSE (sys_exit_group's raw-231 pattern): 1010// the portable rv64/asm-generic number is 233 and x86ctx_rv64_to_x86_64_syscall has no 233 row in 1011// the DEPLOYED compiler, so a portable const would emit x86_64 233 = epoll_ctl (the wrong-syscall- 1012// not-an-error class; see the setpgid/flock rows). The 233->28 row is staged in nx_x86_64_ctx.nx and 1013// activates on the next nx_cc self-host rebuild; flip this to the portable const AFTER that lands. 1014// Signature bite-proven by nx_madvise_probe (0 / -12 ENOMEM / -22 EINVAL). Advisory contract: callers 1015// may ignore the return value -- a failed hint costs nothing but the cold-read behaviour it hints away. 1016func sys_madvise(addr: *u8, len: i64, advice: i64) -> i64 { 1017 return __syscall(28, addr, len, advice, 0, 0, 0) 1018} 1019 1020// openat flavors used by the compiler driver. AT_FDCWD = -100 (declared ABOVE, next to its first 1021// reader -- see the miscompile note there; do NOT move it back down). 1022// O_RDONLY = 0; O_CREAT|O_WRONLY|O_TRUNC = 0x241 on Linux RV64. 1023const O_RDONLY: i64 = 0 1024const O_WRONLY_CT: i64 = 0x241 // O_CREAT | O_WRONLY | O_TRUNC 1025const O_WRONLY_CA: i64 = 0x441 // O_CREAT | O_WRONLY | O_APPEND 1026 1027func sys_openat_rd(path: *u8) -> i64 { 1028 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDONLY, 0, 0, 0) 1029} 1030 1031// O_RDWR|O_CREAT (NO truncate) -- for offset-addressed persistent files like the metrics ring TSDB 1032// (create if missing, then lseek+read/write records in place, never truncating existing history). 1033const O_RDWR_CREATE: i64 = 0x42 1034func sys_openat_rdwr(path: *u8, mode: i64) -> i64 { 1035 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDWR_CREATE, mode, 0, 0) 1036} 1037 1038// ★★★THE FILE MODE IS THE HALF OF THIS INTERFACE THAT WAS NEVER NAMED. The O_ flags above are named 1039// consts in hex WITH a decoding comment; the mode passed beside them is a bare literal at every call 1040// site. MEASURED 2026-08-14 (coverage_complete=1 corpus_complete=1 over 23,053 files): 1041// - 29 organs passed the mode as a bare DECIMAL literal, which no reader decodes as rw-r--r-- 1042// without stopping to convert it. ⚠THE FIRST COUNT PUBLISHED HERE WAS 26: the scan was scoped to 1043// runtime/_hdl_build/ and the SUBDIRECTORY's count was published as the estate figure -- three 1044// more (nx_forge_rag, nx_gpu_export, nx_bvhfk) sat one level up in runtime/. 1045// ★A COUNT INHERITS THE SCOPE OF ITS SCAN, AND THE SCOPE IS THE PART NOBODY PRINTS BESIDE IT. 1046// ⚠The offending call is deliberately NOT spelled out literally in this comment: prose is source 1047// bytes, so writing the pattern here would make every future grep for it match this note; 1048// - 10 MORE each define their OWN private 0644 const (IP_ VR_ VP_ LIVE_ FD_ FP_ WL_ PUB_ REG_ HFF_), 1049// nine written 0x1a4 and one written 420 -- THE SAME CONSTANT IN TWO DIFFERENT BASES. 1050// Ten seats each solved this privately and none put the answer where the next one would look. That is 1051// the duplicate-ruler defect precisely: changing the estate's default artifact mode today means finding 1052// 39 sites in two notations and hoping none was missed. One name, in the shim every organ already 1053// imports, is the entire fix -- and it belongs HERE, beside the flags, not in a 40th private copy. 1054const MODE_0644: i64 = 0x1a4 // rw-r--r-- : default mode for a generated artifact 1055// rwxr-xr-x : default mode for a created DIRECTORY. A directory without the execute bit cannot be 1056// traversed, so MODE_0644 is not merely stricter here -- it is wrong, and the failure surfaces later 1057// as an unopenable path rather than as a refused mkdir. Named beside its sibling so the choice is a 1058// lookup rather than a recollection; the estate otherwise spells this as a raw 0x1ed at every site. 1059const MODE_0755: i64 = 0x1ed 1060// Seconds of ZERO PROGRESS on one socket operation before an accepted connection is abandoned. 1061// A single-threaded accept-loop daemon that loop-reads to Content-Length can be starved FOREVER by one 1062// peer that declares a body it never finishes sending -- a one-request DoS, hostile OR merely buggy. 1063// nx_dos_timeout_scan supervises the class and named 16 daemons carrying no timeout at all; the cure is 1064// sys_set_socket_timeout(cfd, ACCEPT_TMO_S) folded in right after accept. 1065// WHY 30 AND NOT THE 5 THE LOGIN DAEMONS USE: this bound must be wrong in the direction of SERVING, not 1066// of dropping. The attack is an UNBOUNDED wait, so ANY finite bound closes it; a short one additionally 1067// risks aborting a legitimate slow client. 30s of zero progress on a single recv/send is pathological 1068// for every daemon in the class -- including the streaming ones, where data is flowing and the timer 1069// never approaches its bound -- while still converting an infinite starvation into a bounded one. 1070// It is the calibration nx_galx_bridge already uses for an accepted cfd; named here rather than copied 1071// into a 16th private literal, exactly as MODE_0644 above. 1072const ACCEPT_TMO_S: i64 = 30 1073func sys_openat_wr(path: *u8, mode: i64) -> i64 { 1074 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CT, mode, 0, 0) 1075} 1076 1077// Linux O_WRONLY | O_CREAT | O_EXCL. An existing final component, including 1078// a symlink, is a conflict; callers acquire ownership only on success. 1079const O_WRONLY_CREATE_EXCLUSIVE: i64 = 0x1 | 0x40 | 0x80 1080func sys_openat_exclusive(path: *u8, mode: i64) -> i64 { 1081 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CREATE_EXCLUSIVE, mode, 0, 0) 1082} 1083 1084// Linux O_DIRECTORY: require a directory, rather than merely an openable node. 1085const O_DIRECTORY: i64 = 0x10000 1086func sys_openat_directory(path: *u8) -> i64 { 1087 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDONLY | O_DIRECTORY, 0, 0, 0) 1088} 1089 1090// Open path for append (create if missing). Used by append-only 1091// journals such as .race_telemetry.tsv. RV64 syscall numbers; the 1092// x86_64 mirror lives in nx_syscalls_x86_64.nx. 1093func sys_openat_append(path: *u8, mode: i64) -> i64 { 1094 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CA, mode, 0, 0) 1095} 1096 1097// Linux open ABI flags: acquire close-on-exec atomically and refuse a final 1098// symlink. Nonblocking also prevents an unexpected FIFO from stalling admission. 1099const O_CLOEXEC: i64 = 0x80000 1100const O_NOFOLLOW: i64 = 0x20000 1101const O_NONBLOCK: i64 = 0x800 1102const MODE_0600: i64 = 0x180 1103func sys_openat_lock(path: *u8) -> i64 { 1104 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CA | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK, MODE_0600, 0, 0) 1105} 1106 1107// symlinkat(target, AT_FDCWD, linkpath) -- raw x86_64 266 forced RUNTIME (the chdir escape, same as 1108// readlinkat below). THE atomic-repoint primitive for release management: create releases/current.new -> 1109// sys_renameat over releases/current = an atomic symlink swap (golive/rollback are instant + crash-safe). 1110// 0 on success, -errno (notably -EEXIST=-17 if linkpath exists -- create the .new name, then rename). 1111func sys_symlinkat(target: *u8, linkpath: *u8) -> i64 { 1112 let nbox: *i64 = sys_mmap(16) as *i64 1113 nbox[0] = 266 1114 let r: i64 = __syscall(nbox[0], target as i64, AT_FDCWD, linkpath as i64, 0, 0, 0) 1115 sys_munmap(nbox as *u8, 16) 1116 return r 1117} 1118 1119// readlinkat(AT_FDCWD, path, buf, cap) -- raw x86_64 267 forced RUNTIME (the chdir escape: keep the 1120// number out of the rv64->x86 constant-translate path). Returns link length (NO NUL appended), -errno 1121// on fail. nbox is munmap'd before return: the daemon supervisor calls this hundreds of times PER CYCLE 1122// (exe-identity sweeps), and a leaked page per call is exactly the VSZ-balloon class that broke fork. 1123func sys_readlinkat(path: *u8, buf: *u8, cap: i64) -> i64 { 1124 let nbox: *i64 = sys_mmap(16) as *i64 1125 nbox[0] = 267 1126 let r: i64 = __syscall(nbox[0], AT_FDCWD, path as i64, buf as i64, cap, 0, 0) 1127 sys_munmap(nbox as *u8, 16) 1128 return r 1129} 1130 1131// Atomically replace newpath with oldpath (rename(2) on one filesystem: a concurrent reader sees the 1132// whole old file or the whole new file, never a torn read). The S-class content-publish primitive: 1133// write the new page to a temp file, then sys_renameat(tmp, live) -> hot-swap, NO rm+ln race. 1134// renameat2: rv64=276, x86_64=316, flags=0. The known-good compiler translates most rv64 syscall 1135// numbers to the x86_64 target but its table MISSES 276 -- verified 2026-06-14 via nx_rename_probe: 1136// raw 276 -> -EINVAL (lands on x86_64 `tee`), raw 316 -> renames OK. That silently broke every 1137// cst_write_atomic publish (page.html.new written, never swapped in). Try the x86_64 number first 1138// (works on every x86_64 build incl. known-good); fall back to the rv64 number for native-rv64 or 1139// translating compilers that do map it. flags=0 so renameat2 == renameat semantics. 1140func sys_renameat(oldpath: *u8, newpath: *u8) -> i64 { 1141 let r: i64 = __syscall(316, AT_FDCWD, oldpath, AT_FDCWD, newpath, 0, 0) 1142 if r == 0 { return 0 } 1143 return __syscall(276, AT_FDCWD, oldpath, AT_FDCWD, newpath, 0, 0) 1144} 1145 1146// fsync(2): flush file (or directory) data+metadata to stable storage. 1147// PROBE-PROVEN 2026-06-10 (_fsync_probe): rv64 82 is NOT in the compiler's 1148// translation table (lands on x86 rename -> -EFAULT both ways); direct 1149// x86_64 74 passes through raw (the unlinkat-263 precedent) and behaves as 1150// fsync (0 on a valid fd, -9 EBADF on a bad one). Storage commit points 1151// fsync the data files AND their directory around rename(2) so a committed 1152// segment survives power loss, not just process death. 1153func sys_fsync(fd: i64) -> i64 { 1154 return __syscall(74, fd, 0, 0, 0, 0, 0) 1155} 1156 1157// flock(2): BSD-style whole-file ADVISORY lock. rv64 32 -> x86_64 73 via the compiler's 1158// x86ctx_rv64_to_x86_64_syscall table (nx_x86_64_ctx.nx:961, PROVEN LIVE in flock_deploy.log). 1159// op: SYS_LOCK_SH=1 / SYS_LOCK_EX=2 / SYS_LOCK_NB=4 (OR) / SYS_LOCK_UN=8. Returns 0 on success, 1160// -errno on failure. Used by the framed-append durability floor to serialize the write-until- 1161// complete loop so a partial/short write under contention can NEVER misalign a concurrent appender 1162// (O_APPEND single-write atomicity is necessary but not sufficient on every fs -- the lock makes 1163// the whole framed record write atomic against other lockers). Additive: no existing caller in 1164// this file changes. NOTE: nx_flock.nx is a separate organ importing the LEGACY "syscalls.nx" 1165// name; this wrapper lives HERE so organs already on nx_syscalls.nx (e.g. nx_framed_append) get 1166// flock without a second import (double-import rc=6 trap). 1167const SYS_LOCK_SH: i64 = 1 1168const SYS_LOCK_EX: i64 = 2 1169const SYS_LOCK_NB: i64 = 4 1170const SYS_LOCK_UN: i64 = 8 1171func sys_flock(fd: i64, op: i64) -> i64 { 1172 return __syscall(32, fd, op, 0, 0, 0, 0) 1173} 1174 1175// newfstatat(2): stat `path` into a 144-byte x86-64 struct stat at `statbuf`. x86_64 nr 262 is passed 1176// DIRECTLY (the unlinkat-263 / fsync-74 precedent: stat-family rv64 numbers aren't in the compiler's 1177// translation table, so a raw x86_64 number passes through untranslated). Returns 0 on success, <0 1178// (e.g. -2 ENOENT) on error. st_mtim.tv_sec @ offset 88, st_mtim.tv_nsec @ 96 (the freshness channel). 1179func sys_fstatat(path: *u8, statbuf: *u8) -> i64 { 1180 return __syscall(262, AT_FDCWD, path, statbuf, 0, 0, 0) 1181} 1182 1183// utimensat(2): set `path` atime+mtime from `times` (a struct timespec[2] = [atime.sec,atime.nsec, 1184// mtime.sec,mtime.nsec]). x86_64 nr 280 passed DIRECTLY. A sovereign `touch`; also makes freshness 1185// tests deterministic. Returns 0 on success, <0 on error. 1186func sys_utimensat(path: *u8, times: *i64) -> i64 { 1187 return __syscall(280, AT_FDCWD, path, times as i64, 0, 0, 0) 1188} 1189 1190// ---- sovereign host control-plane syscalls (x86_64; single unconditional consts, 1191// per the known-good-compiler @ifdef finding). The Nishi supervisor uses these to 1192// manage the daemon lifecycle WITHOUT any shell (no pkill / mkdir / chmod glue). ---- 1193 1194// COMPILER NOTE: the known-good compiler BAKES whole function bodies by NAME for some syscalls 1195// (proven via emitted .s: a function literally named sys_kill emits number 8, sys_chmod emits 155 1196// -- both wrong, regardless of the const referenced). So these wrappers use NON-baked names 1197// (nx_kill / nx_chmod). sys_mkdir / sys_renameat are not baked, so those keep the sys_ name. 1198 1199// DESIGN: __syscall takes the RV64/generic number; the compiler's x86ctx_rv64_to_x86_64_syscall table 1200// (nx_x86_64_ctx.nx) translates it to the build target. So pass the RV64 number. These four were added 1201// to that sovereign table 2026-06-06 (kill 129->62, mkdirat 34->258, fchmodat 53->268, renameat2 1202// 276->316); x86 kill(62) had collided with rv64 lseek(62), x86 fchmodat(268) with rv64 pivot_root(268). 1203 1204// kill(pid, sig) -- rv64 129 -> x86_64 62. SIGTERM=15 / SIGKILL=9. Host control plane. 1205func nx_kill(pid: i64, sig: i64) -> i64 { return __syscall(129, pid, sig, 0, 0, 0, 0) } 1206 1207// setpgid(pid, pgid) -- put a process in its own PROCESS GROUP so a killer can reach its whole 1208// subtree. nx_kill(0 - pgid, sig) signals every member, not just the one process you forked. 1209// A BOUND THAT ONLY REACHES THE PROCESS YOU FORKED IS NOT A BOUND ON THE WORK IT STARTED. 1210// Per-target const, NOT a bare generic number: x86ctx_rv64_to_x86_64_syscall translates only the 1211// numbers it knows and FALLS THROUGH for the rest. MEASURED on the laptop lane 2026-08-10: a bare 1212// generic 154 reached x86_64 as 154 and returned -38 (ENOSYS), silently -- and a fix built on it 1213// reproduced the original bug exactly. Callers must treat setpgid as BEST-EFFORT. 1214@ifdef TARGET_X86_64 1215const SYS_SETPGID: i64 = 109 1216@endif 1217@ifndef TARGET_X86_64 1218const SYS_SETPGID: i64 = 154 1219@endif 1220func sys_setpgid(pid: i64, pgid: i64) -> i64 { return __syscall(SYS_SETPGID, pid, pgid, 0, 0, 0, 0) } 1221 1222// prlimit64(pid, resource, new_limit, old_limit) -- the Linux RESOURCE-LIMIT primitive = 1223// the Job-Object ActiveProcessLimit / memory-limit analog for the sovereign supervisor (M5). 1224// x86_64 prlimit64 = 302 (PASSED DIRECTLY, the unlinkat-263 / fsync-74 / fstatat-262 1225// precedent: a raw x86_64 number not in the compiler's rv64->x86 swap table passes through 1226// untranslated). NOTE: rv64 prlimit64 IS 261 but x86_64 261 = futimesat -- so the naive 1227// "261 is the same on both" is WRONG (PROBE-PROVEN: 261 returned EFAULT/EINVAL because it 1228// hit futimesat); the build target here is x86_64, so we emit 302 directly. pid=0 => the 1229// calling process (a forked child caps ITSELF before running its payload). new_limit / 1230// old_limit each point at a struct rlimit64 { rlim_cur: i64, rlim_max: i64 } (16 bytes); 1231// pass 0 for old_limit to skip read-back. Returns 0 on success, -errno (e.g. -1 EPERM if 1232// raising a hard limit unprivileged) on failure. NON-baked name (the compiler bakes some 1233// sys_* bodies by name; the nx_ prefix avoids that trap). 1234func nx_prlimit(pid: i64, resource: i64, new_limit: *u8, old_limit: *u8) -> i64 { 1235 return __syscall(302, pid, resource, new_limit as i64, old_limit as i64, 0, 0) 1236} 1237 1238// RLIMIT resource ids (Linux generic; identical rv64/x86_64). RLIMIT_AS = address-space 1239// (virtual memory) cap -- the cleanest userspace-settable "memory budget" for a supervised 1240// job. RLIMIT_CPU = CPU-seconds cap. WNOHANG=1 = wait4 non-blocking liveness poll option. 1241const RLIMIT_CPU: i64 = 0 1242const RLIMIT_AS: i64 = 9 1243const WNOHANG: i64 = 1 1244 1245// mkdirat -- rv64 34 -> x86_64 258. Create a doc-root directory. mode e.g. 0x1ed (0755). 1246func sys_mkdir(path: *u8, mode: i64) -> i64 { return __syscall(34, AT_FDCWD, path, mode, 0, 0, 0) } 1247 1248// fchmodat -- rv64 53 -> x86_64 268. +x a freshly-deployed daemon binary (mode 0x1ed). flags=0. 1249func nx_chmod(path: *u8, mode: i64) -> i64 { return __syscall(53, AT_FDCWD, path, mode, 0, 0, 0) } 1250 1251// setsid -- x86_64 = 112 (not in the rv64->x86 table, so the literal passes through). Detach a forked 1252// process into a NEW session so it survives the SSH/parent close -- sovereign daemonization (no shell setsid). 1253func nx_setsid() -> i64 { return __syscall(112, 0, 0, 0, 0, 0, 0) } 1254 1255// CLOCK_MONOTONIC = 1. ts is 16 bytes {sec: i64, nsec: i64}. 1256// Returns 0 / -errno. 1257func sys_clock_gettime_mono(ts: *i64) -> i64 { 1258 return __syscall(SYS_CLOCK_GETTIME, 1, ts, 0, 0, 0, 0) 1259} 1260 1261// CLOCK_REALTIME = 0 -- wall-clock seconds since the Unix epoch. Use 1262// this (NOT monotonic) for anything that must match calendar time: 1263// X.509 notBefore/notAfter, logs, TLS timestamps. Monotonic returns 1264// time-since-boot, which encodes as ~1970 when (mis)used as an epoch. 1265func sys_clock_gettime_real(ts: *i64) -> i64 { 1266 return __syscall(SYS_CLOCK_GETTIME, 0, ts, 0, 0, 0, 0) 1267} 1268 1269// Wall-clock seconds since the Unix epoch. 1270func sys_now_realtime_sec() -> i64 { 1271 let ts: *i64 = sys_mmap(16) as *i64 1272 sys_clock_gettime_real(ts) 1273 return ts[0] 1274} 1275 1276// Wall-clock milliseconds since the Unix epoch. 1277func sys_now_realtime_ms() -> i64 { 1278 let ts: *i64 = sys_mmap(16) as *i64 1279 sys_clock_gettime_real(ts) 1280 return ts[0] * 1000 + ts[1] / SYS_MAGIC_1000000 1281} 1282 1283// Wall-clock MICROSECONDS since the Unix epoch -- the CROSS-MACHINE stamp. 1284// ★ Use this, never sys_now_us(), for any value one machine writes and ANOTHER machine judges 1285// (fleet beats, lease expiry, telemetry rows). Monotonic counts from each machine's OWN boot, so 1286// subtracting one node's monotonic stamp from another's monotonic now yields the difference of two 1287// unrelated boot epochs -- the remote row then reads as ancient (or future-forged) and a freshness 1288// guard rejects every honest remote node while looking like it is working. 1289func sys_now_realtime_us() -> i64 { 1290 let ts: *i64 = sys_mmap(16) as *i64 1291 sys_clock_gettime_real(ts) 1292 return ts[0] * SYS_MAGIC_1000000 + ts[1] / 1000 1293} 1294 1295// Convenience: monotonic time in milliseconds. Caller does not own 1296// the timespec buffer -- it is mmap'd once per call (cheap; the 1297// underlying syscall already costs more than the page fault). 1298func sys_now_ms() -> i64 { 1299 let ts: *i64 = sys_mmap(16) as *i64 1300 sys_clock_gettime_mono(ts) 1301 let sec_part: i64 = ts[0] * 1000 1302 let nsec_part: i64 = ts[1] / SYS_MAGIC_1000000 1303 return sec_part + nsec_part 1304} 1305 1306// Convenience: monotonic time in microseconds. Used by per-request 1307// elapsed-time tracking in search engines + benches where ms is too 1308// coarse. Same caller-ownership rules as sys_now_ms. 1309func sys_now_us() -> i64 { 1310 let ts: *i64 = sys_mmap(16) as *i64 1311 sys_clock_gettime_mono(ts) 1312 let sec_part: i64 = ts[0] * SYS_MAGIC_1000000 1313 let nsec_part: i64 = ts[1] / 1000 1314 return sec_part + nsec_part 1315} 1316 1317// Alias used by nx_search_onsite_engine etc. Matches `_us` naming 1318// convention. Substrate-canonical name is sys_now_us; this alias 1319// preserves existing call sites without churn. 1320func sys_clock_now_us() -> i64 { 1321 return sys_now_us() 1322} 1323 1324// Read the entire file at `path` into a fresh mmap'd buffer. Returns 1325// a null-terminated *u8 plus writes the byte count to *out_len. On 1326// error (open failure, oversize) returns null and leaves out_len = 0. 1327// Uses a fixed 1 MiB buffer for the first pass; larger sources need a 1328// growth loop. 1329// ---- process control (Linux RV64) ---------------------------- 1330// 1331// Lets NishiLang programs spawn other processes -- prerequisite 1332// for replacing shell scripts (f6_gate.sh) with .nx equivalents. 1333// NishiOS will expose a different process model (capability-based); 1334// these wrappers are the Linux-host compatibility layer. 1335 1336@ifdef TARGET_X86_64 1337const SYS_CLONE: i64 = 56 1338const SYS_EXECVE: i64 = 59 1339const SYS_WAIT4: i64 = 61 1340const SYS_PIPE2: i64 = 293 1341const SYS_DUP3: i64 = 292 1342@endif 1343 1344@ifndef TARGET_X86_64 1345const SYS_CLONE: i64 = 220 1346const SYS_EXECVE: i64 = 221 1347const SYS_WAIT4: i64 = 260 1348const SYS_PIPE2: i64 = 59 1349const SYS_DUP3: i64 = 24 1350@endif 1351 1352// Clone flags (subset). CLONE_VFORK blocks parent until child 1353// exec's or exits, matching fork() semantics closely enough for 1354// our spawn-then-wait patterns. 1355const CLONE_VM: i64 = 0x00000100 1356const CLONE_VFORK: i64 = 0x00004000 1357const SIGCHLD: i64 = 17 1358 1359// Create a child process via Linux clone(). Returns: 1360// > 0 in the parent: child PID 1361// == 0 in the child: child should exec or exit 1362// < 0 on error: -errno 1363// Uses SIGCHLD as the signal that parent receives on child exit 1364// (the libc fork() default); no shared memory or thread flags. 1365// ---- namespace / container family (debt 1785528831) ---------------- 1366// Moved here from nx_syscalls_x86_64.nx so ONE module owns the wrapper set. Their 1367// absence here is why nx_container.nx had to import that module as a SECOND syscall 1368// layer, which put every wrapper in the TU twice and let definition ORDER pick the 1369// winner, silently, until the duplicate-definition guard made it fail closed. 1370func sys_unshare(flags: i64) -> i64 { 1371 return __syscall(SYS_UNSHARE, flags, 0, 0, 0, 0, 0) 1372} 1373func sys_mount(source: *u8, target: *u8, fs_type: *u8, mountflags: i64, data: *u8) -> i64 { 1374 return __syscall(SYS_MOUNT, source, target, fs_type, mountflags, data, 0) 1375} 1376func sys_chroot(path: *u8) -> i64 { 1377 return __syscall(SYS_CHROOT, path, 0, 0, 0, 0, 0) 1378} 1379func sys_getuid() -> i64 { 1380 return __syscall(SYS_GETUID, 0, 0, 0, 0, 0, 0) 1381} 1382func sys_getgid() -> i64 { 1383 return __syscall(SYS_GETGID, 0, 0, 0, 0, 0, 0) 1384} 1385 1386func sys_fork() -> i64 { 1387 return __syscall(SYS_CLONE, SIGCHLD, 0, 0, 0, 0, 0) 1388} 1389 1390// Replace the current process image. `path` is the executable 1391// (absolute or in $PATH if the child first does a fresh clone). 1392// `argv` is a null-terminated array of *u8 (already-marshalled). 1393// `envp` same shape, or null for "inherit parent's env". 1394// Only returns on failure (-errno). 1395// EXEC WITH A CLEAN FD TABLE (seq1785451144). A child inherits every fd its parent held, INCLUDING 1396// listen sockets, across fork AND execve. That is how nx_opaque_login came to hold mgmt s :18098 1397// alongside mgmt itself -- two listeners on one port, connections split between them, a VALID route 1398// answering 404 on some requests. There is no error anywhere in that state, which is why it was 1399// filed as a transport flake for months. 1400// ADDITIVE ON PURPOSE: sys_execve is left byte-identical (910 call sites across 719 files -- a 1401// global change there is unverifiable in one session). Spawners opt in by calling THIS instead. 1402// AUDIT THAT MAKES IT SAFE: zero call sites in the tree dup3 to a target fd above 2, so no exec d 1403// child is deliberately handed a high fd; 0/1/2 are preserved untouched. 1404// Linux child lifetime binding: call in the freshly forked child, before exec. 1405// The expected parent PID is captured before fork, closing the pre-arm death race. 1406// Kernel semantics bind to the creating thread; privileged exec can clear this. 1407const NX_SYS_PRCTL: i64 = 167 1408const NX_PR_SET_PDEATHSIG: i64 = 1 1409const NX_PR_SET_CHILD_SUBREAPER: i64 = 36 1410func sys_prctl(option: i64, arg: i64) -> i64 { 1411 return __syscall(NX_SYS_PRCTL,option,arg,0,0,0,0) 1412} 1413func sys_bind_parent_lifetime(expected_parent: i64, signal: i64) -> i64 { 1414 if expected_parent <= 0 || signal <= 0 { return 0-22 } 1415 let armed: i64=sys_prctl(NX_PR_SET_PDEATHSIG,signal) 1416 if armed < 0 { return armed } 1417 let parent: i64=__syscall(173,0,0,0,0,0,0) 1418 if parent != expected_parent { return 0-10 } 1419 return 0 1420} 1421 1422// Linux waitid observes termination without releasing the child's PID when WNOWAIT is set. 1423// Portable syscall 95 requires the matching x86 backend translation to 247. 1424const SYS_WAITID_PORTABLE: i64 = 95 1425const NX_WAIT_P_PID: i64 = 1 1426const NX_WAIT_EXITED: i64 = 4 1427const NX_WAIT_NOWAIT: i64 = 0x01000000 1428const NX_WAIT_SIGINFO_BYTES: i64 = 128 1429func sys_waitid(idtype: i64, id: i64, info: *u8, options: i64) -> i64 { 1430 return __syscall(SYS_WAITID_PORTABLE,idtype,id,info as i64,options,0,0) 1431} 1432 1433// Post-fork only: the child owns its descriptor table. The buffer bounds a 1434// getdents batch, never the descriptor numbers or number of open handles. 1435const NX_FD_DENT_BUFFER: i64 = 4096 1436const NX_SYS_CLOSE_RANGE: i64 = 436 // Linux x86_64 and asm-generic ABI 1437const NX_FD_UINT_MAX: i64 = 4294967295 1438func sys_close_inherited_proc(first: i64) -> i64 { 1439 let directory: i64=sys_openat_rd("/proc/self/fd") 1440 if directory < 0 { return directory } 1441 let buf: *u8=sys_mmap(NX_FD_DENT_BUFFER) 1442 var result: i64=0 1443 var running: i64=1 1444 while running == 1 { 1445 let n: i64=sys_getdents64(directory,buf,NX_FD_DENT_BUFFER) 1446 if n == (0-4) { continue } 1447 if n <= 0 { result=n; break } 1448 var off: i64=0 1449 while off < n { 1450 if n-off < 20 { result=0-5; running=0; break } 1451 let rec: *u8=buf+off 1452 let size: i64=dirent_reclen(rec) 1453 if size < 20 || size > n-off { result=0-5; running=0; break } 1454 var i: i64=19 1455 var fd: i64=0 1456 var valid: i64=1 1457 while i < size { 1458 let c: i64=rec[i] as i64 1459 if c == 0 { break } 1460 if c < 48 || c > 57 { valid=0; break } 1461 if fd > (2147483647-(c-48))/10 { valid=0; break } 1462 fd=fd*10+c-48; i=i+1 1463 } 1464 if i == 19 || i == size { valid=0 } 1465 if valid == 1 && fd >= first && fd != directory { 1466 // Linux releases the descriptor even when close reports a late 1467 // I/O error; never retry close and risk a reused descriptor. 1468 let closed: i64=sys_close(fd) 1469 if closed < 0 && closed != (0-9) { result=closed; running=0; break } 1470 } 1471 off=off+size 1472 } 1473 } 1474 let closedir: i64=sys_close(directory) 1475 sys_munmap(buf,NX_FD_DENT_BUFFER) 1476 if result == 0 && closedir < 0 { result=closedir } 1477 return result 1478} 1479func sys_close_inherited(first: i64) -> i64 { 1480 if first < 0 { return 0-22 } 1481 let rc: i64=__syscall(NX_SYS_CLOSE_RANGE,first,NX_FD_UINT_MAX,0,0,0,0) 1482 if rc == (0-38) { return sys_close_inherited_proc(first) } 1483 return rc 1484} 1485func sys_execve_clean(path: *u8, argv: *i64, envp: *i64) -> i64 { 1486 let rc: i64=sys_close_inherited(3) 1487 if rc < 0 { return rc } 1488 return sys_execve(path,argv,envp) 1489} 1490 1491func sys_execve(path: *u8, argv: *i64, envp: *i64) -> i64 { 1492 return __syscall(SYS_EXECVE, path, argv, envp, 0, 0, 0) 1493} 1494 1495// Wait for a child to exit. `pid` = -1 waits for ANY child, 1496// otherwise waits for that specific PID. `status` is a caller- 1497// mmapped i64 slot: on exit the low 16 bits carry Linux's w* status 1498// flags (WIFEXITED / WEXITSTATUS). Returns the reaped child's PID 1499// or -errno. 1500func sys_wait4(pid: i64, status: *i64, options: i64) -> i64 { 1501 return __syscall(SYS_WAIT4, pid, status, options, 0, 0, 0) 1502} 1503 1504// Extract exit code from a wait4 status word. Matches the glibc 1505// WEXITSTATUS macro: bits 8-15 of the low 16. 1506func wait_exit_code(status: i64) -> i64 { 1507 return (status >> 8) & 0xFF 1508} 1509 1510// Terminating signal from a wait4 status (0 when the child exited normally). Sibling of 1511// wait_exit_code; RESTORED 2026-07-30 after a stale whole-tree push erased both it and 1512// sys_ignore_sigpipe below, while three files still CALLED them (nx_http_server, nx_sigpipe_gate, 1513// nx_tools_api_serve) -- so the tree could not build until they came back. 1514func wait_term_signal(status: i64) -> i64 { 1515 return status & 0x7f 1516} 1517 1518// THE ONE RULER for "what result code did this process actually produce". Use this, not 1519// wait_exit_code, anywhere the answer becomes a VERDICT. 1520// 1521// WHY IT EXISTS, MEASURED 2026-08-25. wait_exit_code is WEXITSTATUS and is correctly named: 1522// bits 8-15 of the status word. But a child KILLED BY A SIGNAL has no exit status at all, and 1523// those bits are ZERO -- so a SEGFAULTING process is indistinguishable from a clean exit 0 to 1524// every caller that reads only wait_exit_code. Measured live: a gate that SIGSEGV'd mid-run was 1525// served by /api/gate_run as exit_code 0, verdict GREEN. A CRASHED GATE WORE A PASS. 1526// 1527// This is not a new discovery in this estate -- and that is the point. nx_gatekit_lib's 1528// gk_wait_code already carried exactly this rule, with its own measurement recorded (two gates 1529// the 60 s watchdog KILLED journaled `GREEN exit=0 ms=60443`). It was fixed THERE in August and 1530// left unfixed in nx_tool_run, which is the shared exec primitive sitting behind /api/gate_run, 1531// /api/build and 51 other consumers. A LAW APPLIED IN ONE ORGAN AND NOT ITS SIBLING IS HALF A 1532// LAW, AND THE HALF LEFT UNDONE IS THE ONE ON THE PRODUCTION PATH. So the rule now lives HERE, 1533// beside the two accessors it is composed of, and gk_wait_code delegates to it: one ruler. 1534// 1535// Shell convention 128+signal (137 SIGKILL, 139 SIGSEGV) is deliberate: it makes the death both 1536// VISIBLE and NON-ZERO, so every existing caller that branches on rc != 0 sees it with no change. 1537// wait_exit_code is left EXACTLY as it was -- 85 call sites across the corpus (corpus_complete=1) 1538// read it, and silently redefining WEXITSTATUS under them would be the cure being worse. 1539func wait_status_rc(status: i64) -> i64 { 1540 let sig: i64 = wait_term_signal(status) 1541 if sig != 0 { return 128 + sig } 1542 return wait_exit_code(status) 1543} 1544 1545// Ignore SIGPIPE process-wide, so writing to a socket the peer already closed returns -EPIPE 1546// instead of KILLING the process. SIGPIPE default action is TERMINATE, which for a daemon means 1547// every client that walks away mid-response is an outage -- this one call at the listen primitive 1548// is inherited by all 52 consumers of nx_http_server_listen. 1549// rt_sigaction(SIGPIPE, {handler=SIG_IGN}, NULL, 8): syscall 13 on x86-64, which happens to equal 1550// the signal number. SA_RESTORER is deliberately NOT set -- the kernel consults it only when it 1551// DELIVERS a handler frame, and SIG_IGN never delivers one. 1552// PROVEN, not asserted: nx_sigpipe_gate forks a child that writes to a closed pipe and demands 1553// death-by-signal-13 WITHOUT this call and a clean -EPIPE WITH it. 1554// Restore a signal to its DEFAULT disposition. THE INVERSE OF sys_ignore_sigpipe, and it exists 1555// because SIG_IGN is inherited across BOTH fork and execve: a daemon that ignores SIGPIPE hands 1556// that ignore to every child it spawns, FOREVER. That silently corrupted verification -- the 1557// sigpipe gate reported 4/5 RED under /api/gate_run and 5/5 GREEN under a shell, same binary, 1558// same minute, because its DISEASE control (writing to a closed peer must KILL) could not be 1559// observed inside an environment where the kill was already disabled (seq1463). A harness must 1560// not change the state it is verifying; where it must, it has to hand back a clean slate. 1561// ⚠the same inheritance can also produce a FALSE GREEN, which is the far more dangerous half. 1562func sys_default_signal(sig: i64) -> i64 { 1563 let act: *i64 = sys_mmap(64) as *i64 1564 act[0] = 0 1565 act[1] = 0 1566 act[2] = 0 1567 act[3] = 0 1568 return __syscall(13, sig, act as i64, 0, 8, 0, 0) 1569} 1570 1571func sys_ignore_sigpipe() -> i64 { 1572 let act: *i64 = sys_mmap(64) as *i64 1573 act[0] = 1 1574 act[1] = 0 1575 act[2] = 0 1576 act[3] = 0 1577 return __syscall(13, 13, act as i64, 0, 8, 0, 0) 1578} 1579 1580// Create a pipe. `fds` must point at 8+ writable bytes; the kernel 1581// packs BOTH int32 fds into fds[0]: read end = low 32 bits, write end 1582// = HIGH 32 bits (fds[1] is never written -- the old comment claiming 1583// fds[1]=write-end caused a false-pass KAT + a hung gate, 2026-07-16). 1584// Extract: rfd = fds[0] & 0xffffffff; wfd = (fds[0] / 4294967296) & 1585// 0xffffffff. Returns 0 on success, -errno on failure. 1586func sys_pipe2(fds: *i64, flags: i64) -> i64 { 1587 return __syscall(SYS_PIPE2, fds, flags, 0, 0, 0, 0) 1588} 1589 1590// Duplicate `oldfd` onto `newfd`, closing `newfd` first if open. 1591// Used to wire child stdout to a pipe: dup3(pipe_write_end, 1). 1592func sys_dup3(oldfd: i64, newfd: i64, flags: i64) -> i64 { 1593 return __syscall(SYS_DUP3, oldfd, newfd, flags, 0, 0, 0) 1594} 1595 1596// ---- directory listing (Linux RV64 getdents64) --------------- 1597// 1598// Foundation for ls / glob / dir-walk helpers. Linux returns 1599// linux_dirent64 records: 1600// u64 d_ino (inode, ignored here) 1601// s64 d_off (next-record offset) 1602// u16 d_reclen (this record's byte length) 1603// u8 d_type (file type; DT_DIR=4, DT_REG=8, DT_LNK=10) 1604// char d_name[] (null-terminated name, padded so d_reclen 1605// carries us to the next record boundary) 1606// Total struct header: 19 bytes, then name up to d_reclen - 19. 1607 1608@ifdef TARGET_X86_64 1609const SYS_GETDENTS64: i64 = 217 1610@endif 1611@ifndef TARGET_X86_64 1612const SYS_GETDENTS64: i64 = 61 1613@endif 1614 1615const DT_UNKNOWN: i64 = 0 1616const DT_FIFO: i64 = 1 1617const DT_CHR: i64 = 2 1618const DT_DIR: i64 = 4 1619const DT_BLK: i64 = 6 1620const DT_REG: i64 = 8 1621const DT_LNK: i64 = 10 1622const DT_SOCK: i64 = 12 1623 1624// Raw syscall. Returns bytes written on success (0 = end-of-dir), 1625// or -errno on failure. 1626func sys_getdents64(fd: i64, buf: *u8, buf_len: i64) -> i64 { 1627 return __syscall(SYS_GETDENTS64, fd, buf, buf_len, 0, 0, 0) 1628} 1629 1630// Extract fields from a linux_dirent64 record. `rec` points at 1631// the start of the record; fields are at fixed offsets. 1632func dirent_reclen(rec: *u8) -> i64 { 1633 // d_reclen is u16 at offset 16. Read as two bytes little-endian. 1634 let lo: i64 = rec[16] 1635 let hi: i64 = rec[17] 1636 return lo | (hi << 8) 1637} 1638 1639func dirent_type(rec: *u8) -> i64 { 1640 return rec[18] 1641} 1642 1643// Pointer to the null-terminated name inside the record. 1644func dirent_name(rec: *u8) -> *u8 { 1645 let base: i64 = rec as i64 1646 return (base + 19) as *u8 1647} 1648 1649// ---- content-addressed file reader --------------------------- 1650 1651func sys_read_file(path: *u8, out_len: *i64) -> *u8 { 1652 let fd: i64 = sys_openat_rd(path) 1653 if fd < 0 { 1654 *out_len = 0 1655 return 0 as *u8 1656 } 1657 // DEBT-EATEN 2026-07-15: the old fixed 4 GiB cap SILENTLY TRUNCATED bigger files (a 9 GB gguf would 1658 // short-read into plausible-garbage tensors -- the worst failure class). Now the buffer is sized from 1659 // the file itself (lseek END), so ANY size reads fully. Physical pages still allocate on-demand. For 1660 // zero-copy any-size READ-ONLY access prefer sys_map_file (below). 1661 // DEBT-EATEN 2026-08-19 (1787076780): when the size is UNKNOWABLE (lseek END <= 0: /proc files, pipes 1662 // -- AND every empty regular file, which reports 0 just the same) this used to reserve 1663 // SYS_MAGIC_4294967296 of address space per call. Untouched pages were never resident, but the 1664 // mapping WAS: a daemon that read an empty registry every sweep ballooned its VmSize by 4 GiB per 1665 // read (measured: smoke instances at a 4.2 GB base), the leak screens flagged it, and sys_free_file 1666 // could only release what was read. The size-unknowable path now GROWS: start at SYS_READ_GROW_INIT, 1667 // double while the window fills, and hand back an EXACT mapping (total + 16) so sys_free_file 1668 // releases all of it. An empty file costs one small read and a 16-byte arena cell; /proc/stat fits 1669 // the first window; a pipe of any length still reads whole. The known-size path is unchanged. 1670 let fsz: i64 = sys_lseek(fd, 0, 2) 1671 sys_lseek(fd, 0, 0) 1672 var cap: i64 = SYS_READ_GROW_INIT 1673 var grow: i64 = 1 1674 if fsz > 0 { cap = fsz; grow = 0 } 1675 var buf: *u8 = sys_mmap(cap + 16) 1676 var total: i64 = 0 1677 var go: i64 = 1 1678 while go == 1 { 1679 let base: i64 = buf as i64 1680 let tail: *u8 = (base + total) as *u8 1681 let n: i64 = sys_read(fd, tail, cap - total) 1682 if n <= 0 { go = 0 } 1683 if n > 0 { total = total + n } 1684 if total >= cap { 1685 if grow == 0 { go = 0 } else { 1686 // the window filled and the size is unknown: double it, copy, release the old mapping 1687 let ncap: i64 = cap * 2 1688 let nb: *u8 = sys_mmap(ncap + 16) 1689 var ci: i64 = 0 1690 let obase: i64 = buf as i64 1691 let nbase: i64 = nb as i64 1692 while ci < total { let src: *u8 = (obase + ci) as *u8; let dst: *u8 = (nbase + ci) as *u8; dst[0] = src[0]; ci = ci + 1 } 1693 sys_munmap(buf, cap + 16) 1694 buf = nb 1695 cap = ncap 1696 } 1697 } 1698 } 1699 sys_close(fd) 1700 if grow == 1 { 1701 // hand back an EXACT mapping so the paired free releases everything (the doubled window would 1702 // otherwise leave its slack mapped forever -- the address-space leak this change exists to end) 1703 let xb: *u8 = sys_mmap(total + 16) 1704 var xi: i64 = 0 1705 let gbase: i64 = buf as i64 1706 let xbase: i64 = xb as i64 1707 while xi < total { let gsrc: *u8 = (gbase + xi) as *u8; let xdst: *u8 = (xbase + xi) as *u8; xdst[0] = gsrc[0]; xi = xi + 1 } 1708 sys_munmap(buf, cap + 16) 1709 buf = xb 1710 } 1711 // Null-terminate for the lexer. 1712 let bbase: i64 = buf as i64 1713 let term: *u8 = (bbase + total) as *u8 1714 term[0] = 0 1715 *out_len = total 1716 return buf 1717} 1718 1719// PAIRED FREE FOR sys_read_file (2026-08-17). sys_read_file mmaps `cap + 16` where cap is the FILE SIZE 1720// and returns only the pointer -- so any caller that frees it must know the padding, and a caller that 1721// unmaps `len` alone leaks the tail page whenever the file size sits just under a page boundary. 1722// ★A CALLER FORCED TO KNOW ITS ALLOCATOR'S PADDING IS A COUPLING THAT WILL DRIFT -- so the +16 lives 1723// HERE, beside the +16 it mirrors, instead of being retyped at every call site. 1724// Pass the length sys_read_file reported through out_len; this re-derives the mapping from it. 1725// Null-safe by construction: sys_read_file returns 0 on failure, so callers need no extra guard -- 1726// ★A FREE THAT REFUSES NULL IS A FREE NOBODY HAS TO WRAP IN AN IF. 1727// EXACT for every path since 2026-08-19: the size-unknowable fallback (lseek <= 0: /proc, pipes, empty 1728// regular files) now returns a mapping of exactly total + 16, so this releases ALL of it. (It used to 1729// map SYS_MAGIC_4294967296 of address space and release only what was read -- stated then, ended now.) 1730// WHY IT EXISTS: nx_sites_daemon serves /wiki/roadmap by calling sys_read_file PER REQUEST inside a loop 1731// that runs up to NX_SD_MAX_REQ_PER_CONN (64) times per connection and never released it -- an 8,408 B 1732// file became 3 fresh pages and a fresh kernel VMA on every hit, held until the child exited. 1733func sys_free_file(buf: *u8, len: i64) -> i64 { 1734 if (buf as i64) == 0 { return 0 } 1735 if len < 0 { return 0 } 1736 return sys_munmap(buf, len + 16) 1737} 1738 1739// Read-only FILE-BACKED map of the whole file (PROT_READ=1, MAP_PRIVATE=2): any size, zero-copy -- only 1740// touched pages become resident (the lazy-MoE shape: a 9 GB model serves in ~active-set RSS, and load 1741// time is ~0 because nothing is copied). NO NUL pad (a file mapping cannot be extended) -- BINARY 1742// consumers only; text/lexer callers keep sys_read_file. Returns 0 on failure; *out_len = file size. 1743// Read-only by construction (PROT_READ; writes fault -- Rule 26-friendly). 1744func sys_map_file(path: *u8, out_len: *i64) -> *u8 { 1745 *out_len = 0 1746 let fd: i64 = sys_openat_rd(path) 1747 if fd < 0 { return 0 as *u8 } 1748 let fsz: i64 = sys_lseek(fd, 0, 2) 1749 if fsz <= 0 { sys_close(fd); return 0 as *u8 } 1750 let r: i64 = __syscall(SYS_MMAP, 0, fsz, 1, 2, fd, 0) 1751 sys_close(fd) 1752 if r <= 0 { return 0 as *u8 } 1753 *out_len = fsz 1754 return r as *u8 1755} 1756 1757// Sleep for `ms` milliseconds against CLOCK_MONOTONIC (relative). 1758// Returns 0 on success, negative errno on failure. Caller-supplied 1759// budget: ms <= 0 is a no-op; very large values are accepted as-is 1760// (the kernel will saturate to its own clamp). Defined at the bottom 1761// of this file so sys_mmap is in scope (single-pass parser). 1762func sys_sleep_ms(ms: i64) -> i64 { 1763 if ms <= 0 { return 0 } 1764 // struct timespec { sec: i64, nsec: i64 } -- 16 bytes RV64. 1765 let req: *u8 = sys_mmap(16) 1766 let rem: *u8 = sys_mmap(16) 1767 let secs: i64 = ms / 1000 1768 let nsec: i64 = (ms - secs * 1000) * SYS_MAGIC_1000000 // remainder ms -> ns 1769 let req_sec: *i64 = req as *i64 1770 let req_nsec: *i64 = ((req as i64) + 8) as *i64 1771 req_sec[0] = secs 1772 req_nsec[0] = nsec 1773 // clock_nanosleep(CLOCK_MONOTONIC=1, flags=0, req, rem). On EINTR (-4) a signal (e.g. SIGCHLD from a 1774 // reaped child) cut the sleep short and wrote the leftover into rem -- RESUME it, otherwise a caller 1775 // that uses the sleep as a timer (the torrent pool's 2s tick) gets spun into a busy loop by child 1776 // deaths and any tick-based budget collapses to milliseconds. A sleep must sleep its full duration. 1777 var r: i64 = __syscall(SYS_CLOCK_NANOSLEEP, 1, 0, req as i64, rem as i64, 0, 0) 1778 var guard: i64 = 0 1779 while r == (0 - 4) { 1780 if guard > SYS_MAGIC_100000 { r = 0 } else { 1781 let rs: *i64 = rem as *i64 1782 let rn: *i64 = ((rem as i64) + 8) as *i64 1783 req_sec[0] = rs[0] 1784 req_nsec[0] = rn[0] 1785 r = __syscall(SYS_CLOCK_NANOSLEEP, 1, 0, req as i64, rem as i64, 0, 0) 1786 guard = guard + 1 1787 } 1788 } 1789 sys_munmap(req, 16); sys_munmap(rem, 16) // FREE the timespec pages -- every call mmap'd 2 pages; in a 1790 // long-running poll loop (the supervisor's 15s tick) that leaked ~8KB/iter until mmap -> -12 -> SEGFAULT. 1791 return r 1792} 1793 1794// ---- sockets (RV64 generic syscall numbers) ---------------------- 1795// 1796// Source uses RV64 numbers; the x86_64 backend's 1797// x86ctx_rv64_to_x86_64_syscall table translates at codegen time. 1798// Numbers from arch/arm64/include/asm/unistd.h (RV64 inherits the 1799// generic ABI). 1800 1801// Socket-family syscall numbers via @ifdef macro -- mirrors the 1802// pattern already used for SYS_READ/WRITE/MMAP/etc. above. Without 1803// this gate, --target x86_64 compiled the RV64 numbers as literals 1804// into the `syscall` instruction (e.g. 198 = sched_setaffinity on 1805// x86_64, not socket) and any daemon using sys_socket() died with 1806// ENOSYS before printing its banner -- caught by the nx_signaling 1807// stone S2 deploy on 2026-05-20 (see [[project-cross-isa-syscall- 1808// unification-gap-2026-05-20]]). 1809@ifdef TARGET_X86_64 1810const SYS_SOCKET: i64 = 41 1811const SYS_BIND: i64 = 49 1812const SYS_LISTEN: i64 = 50 1813const SYS_ACCEPT: i64 = 43 1814const SYS_CONNECT: i64 = 42 1815const SYS_SETSOCKOPT: i64 = 54 1816const SYS_SENDTO: i64 = 44 1817const SYS_RECVFROM: i64 = 45 1818const SYS_SHUTDOWN: i64 = 48 1819@endif 1820 1821@ifndef TARGET_X86_64 1822const SYS_SOCKET: i64 = 198 1823const SYS_BIND: i64 = 200 1824const SYS_LISTEN: i64 = 201 1825const SYS_ACCEPT: i64 = 202 1826const SYS_CONNECT: i64 = 203 1827const SYS_SETSOCKOPT: i64 = 208 1828const SYS_SENDTO: i64 = 206 1829const SYS_RECVFROM: i64 = 207 1830const SYS_SHUTDOWN: i64 = 210 1831@endif 1832 1833// Socket-option constants used by nx_http_server / nx_https_server. 1834const SOL_SOCKET: i64 = 1 1835const SO_REUSEADDR: i64 = 2 1836// Receive/send timeouts (Linux x86_64). optval is a struct timeval 1837// {tv_sec: i64, tv_usec: i64} (16 bytes). Essential on PUBLIC sockets: 1838// without them, a single silent/slow client hangs a blocking read 1839// forever -> trivial DoS on a single-threaded accept loop. 1840const SO_SNDTIMEO: i64 = 21 1841const SO_RCVTIMEO: i64 = 20 1842 1843// setsockopt(2) -- set a socket option. Defined BEFORE its first caller 1844// (sys_set_socket_timeout, below): NishiLang forbids forward references, 1845// so the definition must precede every use. 1846func sys_setsockopt(fd: i64, level: i64, optname: i64, 1847 optval: *u8, optlen: i64) -> i64 { 1848 return __syscall(SYS_SETSOCKOPT, fd, level, optname, optval, optlen, 0) 1849} 1850 1851// Set a receive+send timeout (in whole seconds) on a socket fd. 1852// tv is munmap'd before return (LEAK FIXED 2026-07-16): this is called once per PROBE by the daemon 1853// supervisor (35/cycle forever -> ~800MB VSZ/day) and once per CONNECTION by fork-per-connection daemons. 1854// The unfreed page-per-call ballooned VSZ until heuristic overcommit made fork() return -ENOMEM (the 1855// proven pid=-12 failure class) -- likely the historical VSZ pressure behind the vsz_watchdog. 1856func sys_set_socket_timeout(fd: i64, secs: i64) -> i64 { 1857 let tv: *i64 = (sys_mmap(16)) as *i64 1858 tv[0] = secs // tv_sec 1859 tv[1] = 0 // tv_usec 1860 sys_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, tv as *u8, 16) 1861 sys_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, tv as *u8, 16) 1862 sys_munmap(tv as *u8, 16) 1863 return 0 1864} 1865 1866// alarm(2): deliver SIGALRM after `secs` seconds (0 cancels a pending alarm). No SIGALRM handler is installed, so 1867// the default action TERMINATES the process. Used as a per-request watchdog inside a forked request-child: a 1868// pathologically-slow page can then never hang the child forever (which would leak its buffers + pile up procs). 1869const SYS_ALARM: i64 = 37 1870func sys_alarm(secs: i64) -> i64 { return __syscall(SYS_ALARM, secs, 0, 0, 0, 0, 0) } 1871 1872const AF_INET: i64 = 2 1873const SOCK_STREAM: i64 = 1 1874const SOCK_DGRAM: i64 = 2 1875 1876func sys_socket(domain: i64, sock_type: i64, protocol: i64) -> i64 { 1877 return __syscall(SYS_SOCKET, domain, sock_type, protocol, 0, 0, 0) 1878} 1879// Pack an AF_INET any-address sockaddr_in (16 bytes) for `port` at `addr`. 1880// RESTORED INTO THE OWNER 2026-08-19: this lived in the old full nx_syscalls_x86_64.nx and was the 1881// one wrapper WITH LIVE CALLERS (nx_nishipages_serve, nx_udp) that the 2026-07-31 alias-stub 1882// consolidation dropped -- both lanes sat NAS-unbuildable ("I do not know the name") until the 1883// rebuild-drain surfaced them. Body verbatim from the old file, including its documented 1884// workaround: NO `as u8` casts on the byte stores -- the array-element-store already truncates 1885// when the lvalue is *u8, and casts on this path once tripped a codegen defect. 1886// (The old file's other two uncalled orphans, sys_pivot_root/sys_umount2, were left dead on a 1887// zero-caller full-tree grep -- restoring an uncalled wrapper is inventory, not capability.) 1888func sockaddr_in_init(addr: *u8, port: i64) -> i64 { 1889 addr[0] = 2 // AF_INET low byte 1890 addr[1] = 0 1891 // Port in network byte order (big-endian). 1892 let hi: i64 = (port >> 8) & 0xFF 1893 let lo: i64 = port & 0xFF 1894 addr[2] = hi 1895 addr[3] = lo 1896 addr[4] = 0 1897 addr[5] = 0 1898 addr[6] = 0 1899 addr[7] = 0 1900 addr[8] = 0 1901 addr[9] = 0 1902 addr[10] = 0 1903 addr[11] = 0 1904 addr[12] = 0 1905 addr[13] = 0 1906 addr[14] = 0 1907 addr[15] = 0 1908 return 0 1909} 1910 1911func sys_bind(fd: i64, addr: *u8, addr_len: i64) -> i64 { 1912 return __syscall(SYS_BIND, fd, addr, addr_len, 0, 0, 0) 1913} 1914func sys_listen(fd: i64, backlog: i64) -> i64 { 1915 return __syscall(SYS_LISTEN, fd, backlog, 0, 0, 0, 0) 1916} 1917// accept(2) -- accept the next pending connection on a listening socket. 1918// Single-arg form (kernel ignores NULL addr/addr_len writes). Existing 1919// nx_http_server callers use this signature; the 3-arg form is provided 1920// as sys_accept_with_addr for outliers needing peer address. 1921func sys_accept(fd: i64) -> i64 { 1922 return __syscall(SYS_ACCEPT, fd, 0, 0, 0, 0, 0) 1923} 1924func sys_accept_with_addr(fd: i64, addr: *u8, addr_len: *i64) -> i64 { 1925 return __syscall(SYS_ACCEPT, fd, addr, addr_len, 0, 0, 0) 1926} 1927// shutdown(2) -- half-close a socket. how: 0=RD, 1=WR, 2=RDWR. 1928func sys_shutdown(fd: i64, how: i64) -> i64 { 1929 return __syscall(SYS_SHUTDOWN, fd, how, 0, 0, 0, 0) 1930} 1931func sys_connect(fd: i64, addr: *u8, addr_len: i64) -> i64 { 1932 return __syscall(SYS_CONNECT, fd, addr, addr_len, 0, 0, 0) 1933} 1934func sys_sendto(fd: i64, buf: *u8, n: i64, flags: i64, 1935 dest_addr: *u8, addr_len: i64) -> i64 { 1936 return __syscall(SYS_SENDTO, fd, buf, n, flags, dest_addr, addr_len) 1937} 1938func sys_recvfrom(fd: i64, buf: *u8, n: i64, flags: i64, 1939 src_addr: *u8, addr_len: *i64) -> i64 { 1940 return __syscall(SYS_RECVFROM, fd, buf, n, flags, src_addr, addr_len) 1941} 1942 1943// ---- SCM_RIGHTS DESCRIPTOR PASSING (sendmsg/recvmsg over AF_UNIX) ----------------------------- 1944// ADDED 2026-08-21 for /compare/trafficsafety TS1. Until now sys_sendmsg was ABSENT-PROVEN from the 1945// whole tree (corpus_complete=1), so the mechanism nginx, HAProxy and Envoy all use for hitless 1946// replacement -- MOVING the listening descriptor rather than re-binding it -- could not be written 1947// at all. SO_REUSEPORT co-binding is an ACCEPT-DISTRIBUTION primitive, NOT a handoff primitive: 1948// LWN documents that changing the set of listening sockets on a port drops connections during the 1949// three-way handshake, so co-binding proves two binders and can never prove zero drops. 1950// 1951// EVERY OFFSET BELOW IS MEASURED, NOT RECALLED. They were read out of the platform's own headers 1952// with offsetof/sizeof/CMSG_LEN compiled for x86_64: 1953// msghdr 56 = name 0 | namelen 8 (u32) | iov 16 | iovlen 24 | control 32 | controllen 40 | flags 48 (u32) 1954// iovec 16 = base 0 | len 8 1955// cmsghdr 16 = len 0 (u64) | level 8 (u32) | type 12 (u32), data at 16 1956// CMSG_LEN(4)=20 CMSG_SPACE(4)=24 sendmsg=46 recvmsg=47 socketpair=53 1957// AF_UNIX=1 SOL_SOCKET=1 SCM_RIGHTS=1 MSG_CMSG_CLOEXEC=1073741824 1958// A WRONG LAYOUT HERE DOES NOT FAIL LOUD. The syscall still returns a positive byte count and 1959// simply transfers no descriptor, which is why the gate for this proves the property by passing a 1960// REAL descriptor between two REAL processes and then USING it, never by reading a return code. 1961// x86_64 Linux numbers, DELIBERATELY UNGUARDED, and the reason is a measurement rather than a 1962// preference. The first draft of this block wrapped these three in the same 1963// @ifdef TARGET_X86_64 / @ifndef pair every other syscall number in this file uses. On an x86 build 1964// that made every call ENOSYS, and the probe that caught it printed why: 1965// CONSTS SYS_SENDMSG=211 SYS_RECVMSG=212 SYS_SOCKETPAIR=199 SYS_WRITE=64 1966// N sendmsg PLAIN via the CONST rc=-38 (211 is unassigned on x86_64) 1967// N2 sendmsg PLAIN via the LITERAL rc=1 1968// SYS_WRITE reading 64 is the tell and it is NOT MINE: the file's own original guarded block 1969// resolves to its RV64 branch when the constant is referenced, on a build whose sys_write plainly 1970// works. So a constant inside these guards is not reliably the value the guard appears to select. 1971// !! A GUARD THAT SILENTLY SELECTS THE OTHER TARGET'S NUMBER IS WORSE THAN NO GUARD: the call still 1972// compiles, still returns, and dispatches a DIFFERENT SYSCALL. Syscall 199 on x86_64 is 1973// fremovexattr, which is why socketpair appeared to answer EFAULT for every input including a NULL 1974// vector and an unsupported domain -- varying the ARGUMENTS can never reveal that the NUMBER is 1975// wrong, because every variant was equally wrong. 1976// => RV64 support for these three is an OPEN, NAMED requirement, blocked on that toolchain 1977// behaviour. It is left undone and stated rather than papered over with a guard measured not to 1978// work. The estate already keeps nx_syscalls_x86_64.nx as the explicit single-target mirror for 1979// exactly this class of problem. 1980const SYS_SENDMSG: i64 = 46 1981const SYS_RECVMSG: i64 = 47 1982const SYS_SOCKETPAIR: i64 = 53 1983const SCM_AF_UNIX: i64 = 1 1984const SCM_SOL_SOCKET: i64 = 1 1985const SCM_RIGHTS_TYPE: i64 = 1 1986const SCM_MSG_CMSG_CLOEXEC: i64 = 1073741824 1987const SCM_MSGHDR_BYTES: i64 = 56 1988const SCM_MSGHDR_OFF_IOV: i64 = 16 1989const SCM_MSGHDR_OFF_IOVLEN: i64 = 24 1990const SCM_MSGHDR_OFF_CTRL: i64 = 32 1991const SCM_MSGHDR_OFF_CTRLLEN: i64 = 40 1992const SCM_IOVEC_BYTES: i64 = 16 1993const SCM_IOVEC_OFF_BASE: i64 = 0 1994const SCM_IOVEC_OFF_LEN: i64 = 8 1995const SCM_CMSG_OFF_LEN: i64 = 0 1996const SCM_CMSG_OFF_LEVEL: i64 = 8 1997const SCM_CMSG_OFF_TYPE: i64 = 12 1998const SCM_CMSG_OFF_DATA: i64 = 16 1999const SCM_CMSG_LEN_1FD: i64 = 20 2000const SCM_CMSG_SPACE_1FD: i64 = 24 2001const SCM_IOV_COUNT_ONE: i64 = 1 2002const SCM_U32_BYTES: i64 = 4 2003const SCM_BYTE_RADIX: i64 = 256 2004const SCM_FDPAIR_BYTES: i64 = 8 2005// One real data byte travels with the ancillary data ON PURPOSE: a sendmsg carrying SCM_RIGHTS and 2006// NO ordinary payload is the classic silent no-transfer, and it returns 0 rather than an error. 2007const SCM_PAYLOAD_BYTES: i64 = 1 2008const SCM_PAYLOAD_BYTE: i64 = 70 2009// Distinguishable refusals, each naming WHICH conjunct failed -- a compound assertion that will not 2010// name its failing conjunct is a false-alarm generator. All are negative and all sit far outside the 2011// errno range, so no caller can confuse one with a kernel error or with a valid descriptor. 2012const SCM_ERR_NO_CMSG: i64 = 0 - 901 2013const SCM_ERR_CMSG_LEN: i64 = 0 - 902 2014const SCM_ERR_CMSG_LEVEL: i64 = 0 - 903 2015const SCM_ERR_CMSG_TYPE: i64 = 0 - 904 2016 2017func scm_zero(base: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { base[i] = 0; i = i + 1 } return 0 } 2018func scm_put_i64(base: *u8, off: i64, v: i64) -> i64 { 2019 let p: *i64 = ((base as i64) + off) as *i64 2020 p[0] = v 2021 return 0 2022} 2023func scm_get_i64(base: *u8, off: i64) -> i64 { 2024 let p: *i64 = ((base as i64) + off) as *i64 2025 return p[0] 2026} 2027// The two cmsg header fields and the descriptor slot itself are 4-byte ints, so they are packed and 2028// unpacked byte by byte in little-endian order. Radix arithmetic rather than bit shifts, matching 2029// sockaddr_in_init's documented style on this exact path. 2030func scm_put_u32(base: *u8, off: i64, v: i64) -> i64 { 2031 var i: i64 = 0 2032 var m: i64 = v 2033 while i < SCM_U32_BYTES { 2034 base[off + i] = m % SCM_BYTE_RADIX 2035 m = m / SCM_BYTE_RADIX 2036 i = i + 1 2037 } 2038 return 0 2039} 2040func scm_get_u32(base: *u8, off: i64) -> i64 { 2041 var v: i64 = 0 2042 var mult: i64 = 1 2043 var i: i64 = 0 2044 while i < SCM_U32_BYTES { 2045 v = v + (base[off + i] as i64) * mult 2046 mult = mult * SCM_BYTE_RADIX 2047 i = i + 1 2048 } 2049 return v 2050} 2051 2052func sys_sendmsg(fd: i64, msg: *u8, flags: i64) -> i64 { 2053 return __syscall(SYS_SENDMSG, fd, msg, flags, 0, 0, 0) 2054} 2055func sys_recvmsg(fd: i64, msg: *u8, flags: i64) -> i64 { 2056 return __syscall(SYS_RECVMSG, fd, msg, flags, 0, 0, 0) 2057} 2058// socketpair(2). sv receives TWO 4-byte descriptors, so it is a *u8 read with scm_get_u32 -- a 2059// single *i64 read would splice both descriptors into one number and the second would vanish. 2060// !! THIS NUMBER IS NOT REACHING socketpair, AND THE FIRST DIAGNOSIS OF THAT WAS WRONG. 2061// Measured 2026-08-21: every call returns -14 (EFAULT) -- with a valid pointer, with a NULL vector, 2062// and with an UNSUPPORTED DOMAIN alike. The first reading of that evidence was "the host refuses 2063// this call for every input", and it was REFUTED by measuring the emitted constants instead of the 2064// arguments. TARGET_X86_64 is hard-pinned UNDEFINED in this toolchain (see nx_syscalls_x86_64.nx 2065// and nx_tokenizer.nx), so the @ifndef branch is what compiles and the x86 backend TRANSLATES RV64 2066// syscall numbers at emit time. Under that translation 53 is RV64 fchmodat, whose SECOND argument 2067// is a path pointer -- and SOCK_STREAM==1 as a path pointer is exactly EFAULT, every time, 2068// regardless of the other arguments. 2069// * VARYING THE ARGUMENTS CAN NEVER REVEAL THAT THE SYSCALL NUMBER IS WRONG: every variant is 2070// equally wrong, so a set of controls that all agree reads as a confident finding about the host. 2071// The control that actually discriminated was PRINTING THE CONSTANT the binary emits. 2072// => The likely correct value here is the RV64 number 199, exactly as sendmsg/recvmsg above needed 2073// their own numbers rather than the guarded pair. That is NOT asserted: it is UNTESTED, and this 2074// comment says so rather than shipping a plausible number with a confident sentence. 2075// => NOTHING DEPENDS ON IT. The descriptor-passing lane uses a NAMED AF_UNIX rendezvous 2076// (sys_unix_listen + sys_unix_connect_fd below), which is proven end to end by nx_scm_rights_gate 2077// and is also what nginx, HAProxy and systemd actually use to move a listener between processes. 2078// socketpair was only ever the convenience. 2079func sys_socketpair(domain: i64, sock_type: i64, protocol: i64, sv: *u8) -> i64 { 2080 return __syscall(SYS_SOCKETPAIR, domain, sock_type, protocol, sv, 0, 0) 2081} 2082 2083// Bind+listen a NAMED AF_UNIX stream socket -- the accepting half of the rendezvous whose 2084// connecting half is nx_unix_connect. Returns the listening fd, or a negative errno. 2085// The caller owns the path: unlink it first (a stale node makes bind return EADDRINUSE) and unlink 2086// it after, because an AF_UNIX bind leaves a filesystem entry that outlives the process. 2087const SCM_SUN_PATH_OFF: i64 = 2 // sockaddr_un = [sa_family: u16][sun_path: 108] 2088const SCM_SUN_BYTES: i64 = 110 2089const SCM_SUN_PATH_MAX: i64 = 107 2090func sys_unix_listen(path: *u8, backlog: i64) -> i64 { 2091 let fd: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0) 2092 if fd < 0 { return fd } 2093 let sa: *u8 = sys_mmap(SCM_SUN_BYTES) 2094 var i: i64 = 0 2095 while i < SCM_SUN_BYTES { sa[i] = 0; i = i + 1 } 2096 sa[0] = SCM_AF_UNIX 2097 sa[1] = 0 2098 var p: i64 = 0 2099 while path[p] != (0 as u8) { 2100 if p >= SCM_SUN_PATH_MAX { sys_close(fd); return 0 - 36 } 2101 sa[SCM_SUN_PATH_OFF + p] = path[p] 2102 p = p + 1 2103 } 2104 let br: i64 = sys_bind(fd, sa, SCM_SUN_PATH_OFF + p + 1) 2105 if br < 0 { sys_close(fd); return br } 2106 let lr: i64 = sys_listen(fd, backlog) 2107 if lr < 0 { sys_close(fd); return lr } 2108 return fd 2109} 2110 2111// The CONNECTING half of the same rendezvous. Returns the connected fd or a negative errno. 2112// RESIDUAL NAMED RATHER THAN LEFT SILENT: nx_unix_socket.nx already carries an nx_unix_connect with 2113// this exact body. It is not composed here because that file also defines a main(), so importing it 2114// would inject a second main into every one of the 52 daemons that reach nx_http_server -- a 2115// resolution-by-definition-order hazard this tree has already been bitten by. The primitive belongs 2116// in the shim; the older standalone file should be reduced to a caller of this one, and that is a 2117// separate change to a file with its own consumers rather than something to fold in silently here. 2118func sys_unix_connect_fd(path: *u8) -> i64 { 2119 let fd: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0) 2120 if fd < 0 { return fd } 2121 let sa: *u8 = sys_mmap(SCM_SUN_BYTES) 2122 var i: i64 = 0 2123 while i < SCM_SUN_BYTES { sa[i] = 0; i = i + 1 } 2124 sa[0] = SCM_AF_UNIX 2125 sa[1] = 0 2126 var p: i64 = 0 2127 while path[p] != (0 as u8) { 2128 if p >= SCM_SUN_PATH_MAX { sys_close(fd); return 0 - 36 } 2129 sa[SCM_SUN_PATH_OFF + p] = path[p] 2130 p = p + 1 2131 } 2132 let cr: i64 = sys_connect(fd, sa, SCM_SUN_PATH_OFF + p + 1) 2133 if cr < 0 { sys_close(fd); return cr } 2134 return fd 2135} 2136 2137// Send ONE open descriptor over a connected AF_UNIX socket. Returns the sendmsg result: the number 2138// of ordinary data bytes sent (SCM_PAYLOAD_BYTES on success) or a negative errno. The descriptor 2139// itself is NOT closed here -- both ends legitimately hold it until the sender chooses to let go, 2140// and that overlap is the entire point: there must be no instant at which zero processes hold the 2141// listening socket. 2142func sys_send_fd(sock: i64, fd: i64) -> i64 { 2143 let msg: *u8 = sys_mmap(SCM_MSGHDR_BYTES) 2144 let iov: *u8 = sys_mmap(SCM_IOVEC_BYTES) 2145 let cbuf: *u8 = sys_mmap(SCM_CMSG_SPACE_1FD) 2146 let data: *u8 = sys_mmap(SCM_PAYLOAD_BYTES) 2147 scm_zero(msg, SCM_MSGHDR_BYTES) 2148 scm_zero(cbuf, SCM_CMSG_SPACE_1FD) 2149 data[0] = SCM_PAYLOAD_BYTE 2150 scm_put_i64(iov, SCM_IOVEC_OFF_BASE, data as i64) 2151 scm_put_i64(iov, SCM_IOVEC_OFF_LEN, SCM_PAYLOAD_BYTES) 2152 scm_put_i64(msg, SCM_MSGHDR_OFF_IOV, iov as i64) 2153 scm_put_i64(msg, SCM_MSGHDR_OFF_IOVLEN, SCM_IOV_COUNT_ONE) 2154 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRL, cbuf as i64) 2155 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRLLEN, SCM_CMSG_SPACE_1FD) 2156 scm_put_i64(cbuf, SCM_CMSG_OFF_LEN, SCM_CMSG_LEN_1FD) 2157 scm_put_u32(cbuf, SCM_CMSG_OFF_LEVEL, SCM_SOL_SOCKET) 2158 scm_put_u32(cbuf, SCM_CMSG_OFF_TYPE, SCM_RIGHTS_TYPE) 2159 scm_put_u32(cbuf, SCM_CMSG_OFF_DATA, fd) 2160 let r: i64 = sys_sendmsg(sock, msg, 0) 2161 sys_munmap(msg, SCM_MSGHDR_BYTES) 2162 sys_munmap(iov, SCM_IOVEC_BYTES) 2163 sys_munmap(cbuf, SCM_CMSG_SPACE_1FD) 2164 sys_munmap(data, SCM_PAYLOAD_BYTES) 2165 return r 2166} 2167 2168// Receive ONE descriptor from a connected AF_UNIX socket. Returns the NEW descriptor number in this 2169// process (>= 0), a negative errno from recvmsg, or one of the SCM_ERR_* codes above. 2170// flags: 0, or SCM_MSG_CMSG_CLOEXEC so the arriving descriptor is not leaked into grandchildren -- 2171// the estate has already lost a port for six days to exactly that inheritance (nx_cloexec_gate). 2172// THE VALIDATION IS THE WHOLE POINT. recvmsg happily returns a positive byte count having delivered 2173// no ancillary data at all, so the kernel's REWRITTEN msg_controllen is read back rather than the 2174// value we asked for, and each of the three cmsg header fields is checked separately so a failure 2175// says which one. 2176func sys_recv_fd(sock: i64, flags: i64) -> i64 { 2177 let msg: *u8 = sys_mmap(SCM_MSGHDR_BYTES) 2178 let iov: *u8 = sys_mmap(SCM_IOVEC_BYTES) 2179 let cbuf: *u8 = sys_mmap(SCM_CMSG_SPACE_1FD) 2180 let data: *u8 = sys_mmap(SCM_PAYLOAD_BYTES) 2181 scm_zero(msg, SCM_MSGHDR_BYTES) 2182 scm_zero(cbuf, SCM_CMSG_SPACE_1FD) 2183 scm_put_i64(iov, SCM_IOVEC_OFF_BASE, data as i64) 2184 scm_put_i64(iov, SCM_IOVEC_OFF_LEN, SCM_PAYLOAD_BYTES) 2185 scm_put_i64(msg, SCM_MSGHDR_OFF_IOV, iov as i64) 2186 scm_put_i64(msg, SCM_MSGHDR_OFF_IOVLEN, SCM_IOV_COUNT_ONE) 2187 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRL, cbuf as i64) 2188 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRLLEN, SCM_CMSG_SPACE_1FD) 2189 let r: i64 = sys_recvmsg(sock, msg, flags) 2190 var out: i64 = r 2191 if r >= 0 { 2192 out = SCM_ERR_NO_CMSG 2193 if scm_get_i64(msg, SCM_MSGHDR_OFF_CTRLLEN) >= SCM_CMSG_LEN_1FD { 2194 out = SCM_ERR_CMSG_LEN 2195 if scm_get_i64(cbuf, SCM_CMSG_OFF_LEN) == SCM_CMSG_LEN_1FD { 2196 out = SCM_ERR_CMSG_LEVEL 2197 if scm_get_u32(cbuf, SCM_CMSG_OFF_LEVEL) == SCM_SOL_SOCKET { 2198 out = SCM_ERR_CMSG_TYPE 2199 if scm_get_u32(cbuf, SCM_CMSG_OFF_TYPE) == SCM_RIGHTS_TYPE { 2200 out = scm_get_u32(cbuf, SCM_CMSG_OFF_DATA) 2201 } 2202 } 2203 } 2204 } 2205 } 2206 sys_munmap(msg, SCM_MSGHDR_BYTES) 2207 sys_munmap(iov, SCM_IOVEC_BYTES) 2208 sys_munmap(cbuf, SCM_CMSG_SPACE_1FD) 2209 sys_munmap(data, SCM_PAYLOAD_BYTES) 2210 return out 2211} 2212 2213// Ordinary permission bits only. Special privilege bits are never copied by staging. 2214const NX_FILE_PERMISSION_MASK:i64=511 2215const NX_FILE_DESCRIPTOR_INVALID:i64=0-22 2216func sys_fchmod_fd(fd:i64,mode:i64)->i64{ 2217 if fd<0 || mode<0 || mode>NX_FILE_PERMISSION_MASK {return NX_FILE_DESCRIPTOR_INVALID} 2218 return __syscall(52,fd,mode,0,0,0,0) 2219} 2220// Portable descriptor syscall; consumers below use the x86-64 stat ABI layout. 2221const NX_STAT_X64_BYTES:i64=144 2222const NX_STAT_X64_MODE_OFFSET:i64=24 2223const NX_STAT_X64_BLOCK_OFFSET:i64=56 2224const NX_STAT_X64_SIZE_OFFSET:i64=48 2225const NX_STAT_X64_DEVICE_OFFSET:i64=0 2226const NX_STAT_X64_INODE_OFFSET:i64=8 2227const NX_STAT_TYPE_MASK:i64=61440 2228const NX_STAT_REGULAR_FILE:i64=32768 2229func sys_fstat_fd(fd:i64,stat:*u8)->i64{ 2230 if fd<0 || (stat as i64)==0{return NX_FILE_DESCRIPTOR_INVALID} 2231 return __syscall(80,fd,stat,0,0,0,0) 2232} 2233func sys_stat_permissions(stat:*u8)->i64{ 2234 let mode:i64=(stat[NX_STAT_X64_MODE_OFFSET] as i64)+((stat[NX_STAT_X64_MODE_OFFSET+1] as i64)<<8) 2235 return mode & NX_FILE_PERMISSION_MASK 2236} 2237 2238 2239 2240func nx_quicksort_partition(arr: *nx_int, lo: nx_idx, hi: nx_idx) -> nx_idx { 2241 let pivot: nx_int = arr[hi] 2242 var i: nx_idx = lo 2243 var j: nx_idx = lo 2244 while j < hi { 2245 if arr[j] <= pivot { 2246 let tmp: nx_int = arr[i] 2247 arr[i] = arr[j] 2248 arr[j] = tmp 2249 i = i + 1 2250 } 2251 j = j + 1 2252 } 2253 let tmp2: nx_int = arr[i] 2254 arr[i] = arr[hi] 2255 arr[hi] = tmp2 2256 return i 2257} 2258 2259func nx_quicksort_recur(arr: *nx_int, lo: nx_idx, hi: nx_idx) -> nx_int { 2260 if lo < hi { 2261 let p: nx_idx = nx_quicksort_partition(arr, lo, hi) 2262 if p > 0 { nx_quicksort_recur(arr, lo, p - 1) } 2263 nx_quicksort_recur(arr, p + 1, hi) 2264 } 2265 return 0 2266} 2267 2268// Sort arr[0..n) in place, ascending. 2269func nx_quicksort(arr: *nx_int, n: nx_idx) -> nx_int { 2270 if n > 1 { nx_quicksort_recur(arr, 0, n - 1) } 2271 return 0 2272} 2273 2274// Private candidate: generic surface query composed from canonical NXA attachment validation. 2275// Returns existing face,bary0,bary1,bary2,denominator tuple; no new disk section or material kind. 2276// Deterministic garment-to-body binding, shared by asset preparation and worker conversion. 2277// Native NXA geometry conversion for GPU upload; no platform API or allocator dependency. 2278// nx_nxa.nx -- Provides shared identity and integrity primitives for the NXA (Nishi Animated 3D) format version 1. 2279const NXA_MAGIC_1000003: i64 = 1000003 2280// nx_nxa.nx -- NXA (Nishi Animated 3D) format v1: shared identity + integrity primitives. 2281// The format spec lives in knowledge/nxa_format_spec.md. This organ is the ONLY place the 2282// magic, tags, and checksum are defined -- writer (nx_fbx_measure) and readers (nx_mesh_view) 2283// import it so they can never drift (DRY, rule-15). 2284// license_tier: ORIGINAL 2285 2286const NXA_VER: i64 = 1 2287 2288// magic = the 8 ASCII bytes "NXANIM01" read as one little-endian i64 (no 64-bit hex literal risk) 2289func nxa_magic() -> i64 { 2290 let s: *u8 = "NXANIM01" as *u8 2291 return ((s[0] & 0xff) as i64) | (((s[1] & 0xff) as i64) << 8) 2292 | (((s[2] & 0xff) as i64) << 16) | (((s[3] & 0xff) as i64) << 24) 2293 | (((s[4] & 0xff) as i64) << 32) | (((s[5] & 0xff) as i64) << 40) 2294 | (((s[6] & 0xff) as i64) << 48) | (((s[7] & 0xff) as i64) << 56) 2295} 2296// 4-char section tag ("VERT", "TRIS", ...) as u32 2297func nxa_tag4(s: *u8) -> i64 { 2298 return ((s[0] & 0xff) as i64) | (((s[1] & 0xff) as i64) << 8) 2299 | (((s[2] & 0xff) as i64) << 16) | (((s[3] & 0xff) as i64) << 24) 2300} 2301// order-sensitive rolling checksum over i64 words (seeded so writers can fold split buffers) 2302func nxa_check2(seed: i64, w: *i64, nw: i64) -> i64 { 2303 var c: i64 = seed 2304 var i: i64 = 0 2305 while i < nw { c = c*NXA_MAGIC_1000003 + w[i]; i = i + 1 } 2306 return c 2307} 2308// Validate a section and return its TOC entry's word offset, preserving payload extent metadata. 2309// Returns a nonnegative entry index, 2310// or -1 not-found/not-NXA, -2 future-version (refuse), -3 corrupt (refuse). 2311func nxa_section_entry(b: *u8, flen: i64, tag: i64) -> i64 { 2312 if flen < 32 { return 0 - 1 } 2313 let h: *i64 = b as *i64 2314 if h[0] != nxa_magic() { return 0 - 1 } 2315 if h[1] > NXA_VER { return 0 - 2 } 2316 if h[1] < 1 { return 0 - 3 } 2317 let ns: i64 = h[2] 2318 if ns < 1 { return 0 - 3 } 2319 // The available TOC bytes bound the count before multiplication; v1 declares no 64-section ceiling. 2320 if ns > (flen - 32)/32 { return 0 - 3 } 2321 let tb: *i64 = ((b as i64) + 32) as *i64 2322 if h[3] != nxa_check2(1, tb, ns*4) { return 0 - 3 } 2323 var s: i64 = 0 2324 while s < ns { 2325 if tb[s*4] == tag { 2326 let off: i64 = tb[s*4+1] 2327 let wl: i64 = tb[s*4+2] 2328 if off < 32 + ns*32 { return 0 - 3 } 2329 // Wire offsets are byte offsets, but callers receive an i64-word index. 2330 // Validate alignment and remaining bytes before address/length arithmetic. 2331 if off % 8 != 0 { return 0 - 3 } 2332 if off > flen { return 0 - 3 } 2333 if wl < 0 { return 0 - 3 } 2334 if wl > (flen - off)/8 { return 0 - 3 } 2335 let pw: *i64 = ((b as i64) + off) as *i64 2336 if tb[s*4+3] != nxa_check2(1, pw, wl) { return 0 - 3 } 2337 return 4+s*4 2338 } 2339 s = s + 1 2340 } 2341 return 0 - 1 2342} 2343 2344// Compatibility entry point: payload word offset, with unchanged negative return classes. 2345func nxa_find(b: *u8,flen: i64,tag: i64) -> i64 { 2346 let entry: i64=nxa_section_entry(b,flen,tag) 2347 if entry<0 { return entry } 2348 let h: *i64=b as *i64 2349 return h[entry+1]/8 2350} 2351 2352// Fixed-stride v1 arrays carry [count][count * stride words]. 2353// Check exact shape by division before multiplication or any element access. 2354func nxa_counted_section(b: *u8,flen: i64,tag: i64,stride: i64) -> i64 { 2355 if stride<=0 { return 0-3 } 2356 let entry: i64=nxa_section_entry(b,flen,tag) 2357 if entry<0 { return entry } 2358 let h: *i64=b as *i64 2359 let words: i64=h[entry+2] 2360 if words<1 { return 0-3 } 2361 let offset: i64=h[entry+1]/8 2362 let count: i64=h[offset] 2363 if count<0 { return 0-3 } 2364 let remaining: i64=words-1 2365 if remaining%stride!=0 { return 0-3 } 2366 if count!=remaining/stride { return 0-3 } 2367 return offset 2368} 2369 2370 2371const NGP_COMPONENTS: i64 = 3 // VERT xyz and TRIS corners in the NXA v1 schema. 2372const NGP_GPU_WORD: i64 = 4 // WebGPU float32/uint32 wire representation. 2373const NGP_U32_MAX: i64 = 4294967295 2374const NGP_E_OUTPUT: i64 = 0-4 2375const NGP_E_INDEX: i64 = 0-5 2376const NGP_E_OVERLAP: i64 = 0-6 2377 2378const NGP_SKIN_INFLUENCES: i64 = 4 // NXA v1: four joint indices, then four Q12 weights. 2379const NGP_SKIN_WORDS: i64 = NGP_SKIN_INFLUENCES*2 2380const NGP_SKEL_WORDS: i64 = 8 // NXA v1 parent, bind translation xyz, quaternion xyzw. 2381const NGP_WEIGHT_ONE: i64 = 4096 // NXA v1 Q12 weight denominator. 2382const NGP_F32_INTEGER_BITS: i64 = 24 // IEEE-754 binary32 significand precision. 2383const NGP_E_SKIN_COUNT: i64 = 0-7 2384const NGP_E_SKIN_JOINT: i64 = 0-8 2385const NGP_E_SKIN_WEIGHT: i64 = 0-9 2386const NGP_E_TEXC_SHAPE: i64=0-10 2387const NGP_E_TEXC_VALUE: i64=0-11 2388const NGP_F32_ABS_MASK: i64=2147483647 2389const NGP_F32_SIGN_MASK: i64=2147483648 2390 2391func ngp_reason(rc: i64) -> *u8 { 2392 if rc==0-1 { return "NXA identity or required section absent" as *u8 } 2393 if rc==0-2 { return "NXA version newer than this reader" as *u8 } 2394 if rc==0-3 { return "NXA checksum, extent or element shape invalid" as *u8 } 2395 if rc==NGP_E_OUTPUT { return "output extent insufficient; allocate the reported geometry byte requirement" as *u8 } 2396 if rc==NGP_E_INDEX { return "triangle index outside vertex range or uint32 representation" as *u8 } 2397 if rc==NGP_E_OVERLAP { return "output overlaps immutable source; provide a distinct upload buffer" as *u8 } 2398 if rc==NGP_E_SKIN_COUNT { return "SKIN vertex count differs from VERT; repair the source binding" as *u8 } 2399 if rc==NGP_E_SKIN_JOINT { return "SKIN joint is outside SKEL or exact GPU index representation" as *u8 } 2400 if rc==NGP_E_SKIN_WEIGHT { return "SKIN Q12 weights are out of range or do not sum to one" as *u8 } 2401 if rc==NGP_E_TEXC_SHAPE { return "TEXC vertex count, stride or extent differs from the source geometry" as *u8 } 2402 if rc==NGP_E_TEXC_VALUE { return "TEXC UV or owning joint is outside the NXA v1 atlas representation" as *u8 } 2403 return "asset buffers packed" as *u8 2404} 2405func ngp_section(b: *u8,n: i64,tag: *u8) -> i64 { 2406 return nxa_counted_section(b,n,nxa_tag4(tag),NGP_COMPONENTS) 2407} 2408func ngp_bytes(b: *u8,n: i64,tag: *u8) -> i64 { 2409 let at: i64=ngp_section(b,n,tag) 2410 if at<0 { return at } 2411 let h: *i64=b as *i64 2412 // Validated i64 payload fits n; its float32/uint32 representation is half its size. 2413 return h[at]*NGP_COMPONENTS*NGP_GPU_WORD 2414} 2415func ngp_output(b: *u8,n: i64,out: *u8,cap: i64,need: i64) -> i64 { 2416 if cap<need { return NGP_E_OUTPUT } 2417 if need==0 { return 0 } 2418 if (out as i64)<=0 { return NGP_E_OUTPUT } 2419 let src: i64=b as i64 2420 let dst: i64=out as i64 2421 if dst>=src { 2422 if dst-src<n { return NGP_E_OVERLAP } 2423 } else { 2424 if src-dst<need { return NGP_E_OVERLAP } 2425 } 2426 return 0 2427} 2428func ngp_vertices(b: *u8,n: i64,out: *u8,cap: i64) -> i64 { 2429 let at: i64=ngp_section(b,n,"VERT" as *u8) 2430 if at<0 { return at } 2431 let h: *i64=b as *i64 2432 let count: i64=h[at]*NGP_COMPONENTS 2433 let need: i64=count*NGP_GPU_WORD 2434 let valid: i64=ngp_output(b,n,out,cap,need) 2435 if valid<0 { return valid } 2436 let dst: *u32=out as *u32 2437 var i: i64=0 2438 while i<count { dst[i]=__f32_from_i64(h[at+1+i]) as u32; i=i+1 } 2439 return need 2440} 2441func ngp_indices(b: *u8,n: i64,out: *u8,cap: i64) -> i64 { 2442 let va: i64=ngp_section(b,n,"VERT" as *u8) 2443 if va<0 { return va } 2444 let ta: i64=ngp_section(b,n,"TRIS" as *u8) 2445 if ta<0 { return ta } 2446 let h: *i64=b as *i64 2447 let nv: i64=h[va] 2448 let count: i64=h[ta]*NGP_COMPONENTS 2449 let need: i64=count*NGP_GPU_WORD 2450 let valid: i64=ngp_output(b,n,out,cap,need) 2451 if valid<0 { return valid } 2452 // Validate every index before modifying the destination, including late invalid elements. 2453 var i: i64=0 2454 while i<count { 2455 let v: i64=h[ta+1+i] 2456 if v<0 { return NGP_E_INDEX } 2457 if v>=nv { return NGP_E_INDEX } 2458 if v>NGP_U32_MAX { return NGP_E_INDEX } 2459 i=i+1 2460 } 2461 let dst: *u32=out as *u32 2462 i=0 2463 while i<count { dst[i]=h[ta+1+i] as u32; i=i+1 } 2464 return need 2465} 2466 2467// Validate source-to-skeleton binding before any derived GPU allocation or write. 2468func ngp_skin_section(b: *u8,n: i64) -> i64 { 2469 let va: i64=ngp_section(b,n,"VERT" as *u8) 2470 if va<0 { return va } 2471 let sa: i64=nxa_counted_section(b,n,nxa_tag4("SKEL" as *u8),NGP_SKEL_WORDS) 2472 if sa<0 { return sa } 2473 let ka: i64=nxa_counted_section(b,n,nxa_tag4("SKIN" as *u8),NGP_SKIN_WORDS) 2474 if ka<0 { return ka } 2475 let h: *i64=b as *i64 2476 if h[ka]!=h[va] { return NGP_E_SKIN_COUNT } 2477 let count: i64=h[ka] 2478 let joints: i64=h[sa] 2479 var v: i64=0 2480 while v<count { 2481 let at: i64=ka+1+v*NGP_SKIN_WORDS 2482 var sum: i64=0 2483 var k: i64=0 2484 while k<NGP_SKIN_INFLUENCES { 2485 let joint: i64=h[at+k] 2486 if joint<0 { return NGP_E_SKIN_JOINT } 2487 if joint>=joints { return NGP_E_SKIN_JOINT } 2488 if joint>(1<<NGP_F32_INTEGER_BITS) { return NGP_E_SKIN_JOINT } 2489 let weight: i64=h[at+NGP_SKIN_INFLUENCES+k] 2490 if weight<0 { return NGP_E_SKIN_WEIGHT } 2491 if weight>NGP_WEIGHT_ONE { return NGP_E_SKIN_WEIGHT } 2492 sum=sum+weight 2493 k=k+1 2494 } 2495 if sum!=NGP_WEIGHT_ONE { return NGP_E_SKIN_WEIGHT } 2496 v=v+1 2497 } 2498 return ka 2499} 2500func ngp_skin_bytes(b: *u8,n: i64) -> i64 { 2501 let at: i64=ngp_skin_section(b,n) 2502 if at<0 { return at } 2503 let h: *i64=b as *i64 2504 return h[at]*NGP_SKIN_WORDS*NGP_GPU_WORD 2505} 2506// Interleaved float32x4 joints + float32x4 weights; offsets/stride derive from the v1 schema. 2507// Q12 weights convert exactly to binary32; never silently normalize malformed source weights. 2508func ngp_skin(b: *u8,n: i64,out: *u8,cap: i64) -> i64 { 2509 let at: i64=ngp_skin_section(b,n) 2510 if at<0 { return at } 2511 let h: *i64=b as *i64 2512 let count: i64=h[at] 2513 let need: i64=count*NGP_SKIN_WORDS*NGP_GPU_WORD 2514 let valid: i64=ngp_output(b,n,out,cap,need) 2515 if valid<0 { return valid } 2516 let dst: *u32=out as *u32 2517 let denominator: i64=__f32_from_i64(NGP_WEIGHT_ONE) 2518 var v: i64=0 2519 while v<count { 2520 let src: i64=at+1+v*NGP_SKIN_WORDS 2521 let target: i64=v*NGP_SKIN_WORDS 2522 var k: i64=0 2523 while k<NGP_SKIN_INFLUENCES { 2524 dst[target+k]=__f32_from_i64(h[src+k]) as u32 2525 dst[target+NGP_SKIN_INFLUENCES+k]=__f32_div(__f32_from_i64(h[src+NGP_SKIN_INFLUENCES+k]),denominator) as u32 2526 k=k+1 2527 } 2528 v=v+1 2529 } 2530 return need 2531} 2532 2533const NGP_TEXC_HEADER: i64=4 // NXA TEXC nv, stride, atlas grid, optional region table offset. 2534const NGP_TEXC_STRIDE: i64=3 // u Q16, v Q16, owning joint. 2535const NGP_TEXC_UV: i64=2 2536const NGP_TEXC_ONE: i64=65536 2537 2538// Read UVs from the source atlas; optional region metadata remains in the immutable NXA. 2539func ngp_texcoord_section(b: *u8,n: i64) -> i64 { 2540 let va: i64=ngp_section(b,n,"VERT" as *u8) 2541 if va<0 { return va } 2542 let sk: i64=nxa_counted_section(b,n,nxa_tag4("SKEL" as *u8),NGP_SKEL_WORDS) 2543 if sk<0 { return sk } 2544 let entry: i64=nxa_section_entry(b,n,nxa_tag4("TEXC" as *u8)) 2545 if entry<0 { return entry } 2546 let h: *i64=b as *i64 2547 let words: i64=h[entry+2] 2548 if words<NGP_TEXC_HEADER { return NGP_E_TEXC_SHAPE } 2549 let at: i64=h[entry+1]/8 2550 let count: i64=h[at] 2551 if count!=h[va] || h[at+1]!=NGP_TEXC_STRIDE { return NGP_E_TEXC_SHAPE } 2552 if count>(words-NGP_TEXC_HEADER)/NGP_TEXC_STRIDE { return NGP_E_TEXC_SHAPE } 2553 var v: i64=0 2554 while v<count { 2555 let row: i64=at+NGP_TEXC_HEADER+v*NGP_TEXC_STRIDE 2556 var k: i64=0 2557 while k<NGP_TEXC_UV { 2558 if h[row+k]<0 || h[row+k]>=NGP_TEXC_ONE { return NGP_E_TEXC_VALUE } 2559 k=k+1 2560 } 2561 if h[row+NGP_TEXC_UV]<0 || h[row+NGP_TEXC_UV]>=h[sk] { return NGP_E_TEXC_VALUE } 2562 v=v+1 2563 } 2564 return at 2565} 2566func ngp_texcoord_bytes(b: *u8,n: i64) -> i64 { 2567 let at: i64=ngp_texcoord_section(b,n) 2568 if at<0 { return at } 2569 let h: *i64=b as *i64 2570 return h[at]*NGP_TEXC_UV*NGP_GPU_WORD 2571} 2572func ngp_texcoords(b: *u8,n: i64,out: *u8,cap: i64) -> i64 { 2573 let at: i64=ngp_texcoord_section(b,n) 2574 if at<0 { return at } 2575 let h: *i64=b as *i64 2576 let count: i64=h[at] 2577 let need: i64=count*NGP_TEXC_UV*NGP_GPU_WORD 2578 let valid: i64=ngp_output(b,n,out,cap,need) 2579 if valid<0 { return valid } 2580 let dst: *u32=out as *u32 2581 let denominator: i64=__f32_from_i64(NGP_TEXC_ONE) 2582 var v: i64=0 2583 while v<count { 2584 var k: i64=0 2585 while k<NGP_TEXC_UV { 2586 let value: i64=h[at+NGP_TEXC_HEADER+v*NGP_TEXC_STRIDE+k] 2587 dst[v*NGP_TEXC_UV+k]=__f32_div(__f32_from_i64(value),denominator) as u32 2588 k=k+1 2589 } 2590 v=v+1 2591 } 2592 return need 2593} 2594 2595// Normal sums are scaled before squaring, so length squared is in [1,3]. 2596// Newton refinement stops on identical binary32 bits; precision bounds the iterations. 2597func ngp_normal_length(square: i64) -> i64 { 2598 var root: i64=__f32_from_i64(1) 2599 let two: i64=__f32_from_i64(2) 2600 var i: i64=0 2601 while i<NGP_F32_INTEGER_BITS { 2602 let next: i64=__f32_div(__f32_add(root,__f32_div(square,root)),two) 2603 if next==root { return root } 2604 root=next;i=i+1 2605 } 2606 return root 2607} 2608func ngp_normal_edge(h: *i64,va: i64,a: i64,b: i64,k: i64,scale: i64) -> i64 { 2609 let av: i64=__f32_div(__f32_from_i64(h[va+1+a*NGP_COMPONENTS+k]),scale) 2610 let bv: i64=__f32_div(__f32_from_i64(h[va+1+b*NGP_COMPONENTS+k]),scale) 2611 return ngp_float_difference(bv,av) 2612} 2613func ngp_float_difference(a: i64,b: i64) -> i64 { return __f32_add(a,b^NGP_F32_SIGN_MASK) } 2614// Area-weighted normals for a mesh lacking authored normals. Never weld source vertices: 2615// duplicated seam/hard-edge vertices remain independent. Degenerate sums remain zero. 2616func ngp_normals(b: *u8,n: i64,out: *u8,cap: i64) -> i64 { 2617 let va: i64=ngp_section(b,n,"VERT" as *u8) 2618 if va<0 { return va } 2619 let ta: i64=ngp_section(b,n,"TRIS" as *u8) 2620 if ta<0 { return ta } 2621 let h: *i64=b as *i64 2622 let nv: i64=h[va] 2623 let count: i64=h[ta]*NGP_COMPONENTS 2624 let need: i64=nv*NGP_COMPONENTS*NGP_GPU_WORD 2625 let valid: i64=ngp_output(b,n,out,cap,need) 2626 if valid<0 { return valid } 2627 var i: i64=0 2628 while i<count { 2629 let index: i64=h[ta+1+i] 2630 if index<0 || index>=nv || index>NGP_U32_MAX { return NGP_E_INDEX } 2631 i=i+1 2632 } 2633 // One common scale preserves relative face areas while keeping cross products finite. 2634 var scale: i64=__f32_from_i64(1) 2635 i=0 2636 while i<nv*NGP_COMPONENTS { 2637 let magnitude: i64=__f32_from_i64(h[va+1+i])&NGP_F32_ABS_MASK 2638 if magnitude>scale { scale=magnitude } 2639 i=i+1 2640 } 2641 let dst: *u32=out as *u32 2642 i=0 2643 while i<nv*NGP_COMPONENTS { dst[i]=0 as u32;i=i+1 } 2644 i=0 2645 while i<count { 2646 let a: i64=h[ta+1+i];let c: i64=h[ta+2+i];let d: i64=h[ta+3+i] 2647 var k: i64=0 2648 while k<NGP_COMPONENTS { 2649 let u: i64=(k+1)%NGP_COMPONENTS;let v: i64=(k+2)%NGP_COMPONENTS 2650 let face: i64=ngp_float_difference(__f32_mul(ngp_normal_edge(h,va,a,c,u,scale),ngp_normal_edge(h,va,a,d,v,scale)),__f32_mul(ngp_normal_edge(h,va,a,c,v,scale),ngp_normal_edge(h,va,a,d,u,scale))) 2651 var corner: i64=0 2652 while corner<NGP_COMPONENTS { 2653 let at: i64=h[ta+1+i+corner]*NGP_COMPONENTS+k 2654 dst[at]=__f32_add(dst[at] as i64,face) as u32 2655 corner=corner+1 2656 } 2657 k=k+1 2658 } 2659 i=i+NGP_COMPONENTS 2660 } 2661 i=0 2662 while i<nv { 2663 var largest: i64=0;var k: i64=0 2664 while k<NGP_COMPONENTS { 2665 let magnitude: i64=(dst[i*NGP_COMPONENTS+k] as i64)&NGP_F32_ABS_MASK 2666 if magnitude>largest { largest=magnitude } 2667 k=k+1 2668 } 2669 if largest>0 { 2670 var square: i64=0;k=0 2671 while k<NGP_COMPONENTS { 2672 let at: i64=i*NGP_COMPONENTS+k 2673 let value: i64=__f32_div(dst[at] as i64,largest) 2674 dst[at]=value as u32;square=__f32_add(square,__f32_mul(value,value));k=k+1 2675 } 2676 let length: i64=ngp_normal_length(square);k=0 2677 while k<NGP_COMPONENTS { 2678 let at: i64=i*NGP_COMPONENTS+k 2679 dst[at]=__f32_div(dst[at] as i64,length) as u32;k=k+1 2680 } 2681 } 2682 i=i+1 2683 } 2684 return need 2685} 2686 2687 2688const NGB_WORD: i64=8 2689const NGB_COORD_LIMIT: i64=1<<NGP_F32_INTEGER_BITS // Exact integer positions in the incumbent float32 upload. 2690const NGB_E_POLICY: i64=0-20 2691const NGB_E_COORD: i64=0-21 2692const NGB_E_CANDIDATE: i64=0-22 2693 2694func ngb_coordinates(h: *i64,at: i64) -> i64 { 2695 var i: i64=0 2696 while i<h[at]*NGP_COMPONENTS { 2697 let v: i64=h[at+1+i] 2698 if v<0-NGB_COORD_LIMIT || v>NGB_COORD_LIMIT { return NGB_E_COORD } 2699 i=i+1 2700 } 2701 return 0 2702} 2703// step and lower_band are caller-owned binding policy, never hidden asset-specific constants. 2704// Below lower_band, queries bind only above that band; otherwise the candidate floor is zero. 2705// Coordinates are exact float32 integers and squared distances fit exact binary64 integers. 2706// Ties select the lowest eligible source index. No candidate is an error, never vertex-zero fallback. 2707func ngb_map(b: *u8,n: i64,out: *u8,cap: i64,step: i64,lower_band: i64) -> i64 { 2708 if step<=0 || lower_band<0 || lower_band>NGB_COORD_LIMIT { return NGB_E_POLICY } 2709 let va: i64=ngp_section(b,n,"VERT" as *u8) 2710 if va<0 { return va } 2711 let ga: i64=ngp_section(b,n,"GVRT" as *u8) 2712 if ga<0 { return ga } 2713 let h: *i64=b as *i64 2714 let nv: i64=h[va];let ng: i64=h[ga] 2715 let need: i64=ng*NGB_WORD 2716 let valid: i64=ngp_output(b,n,out,cap,need) 2717 if valid<0 { return valid } 2718 let vc: i64=ngb_coordinates(h,va) 2719 if vc<0 { return vc } 2720 let gc: i64=ngb_coordinates(h,ga) 2721 if gc<0 { return gc } 2722 var above_zero: i64=0;var above_band: i64=0 2723 var u: i64=0 2724 while u<nv { 2725 let z: i64=h[va+1+u*NGP_COMPONENTS+2] 2726 if z>=0 { above_zero=1 } 2727 if z>=lower_band { above_band=1 } 2728 // Subtraction prevents an arbitrary caller stride overflowing the loop index. 2729 if step>=nv-u { u=nv } else { u=u+step } 2730 } 2731 var v: i64=0 2732 while v<ng { 2733 let z: i64=h[ga+1+v*NGP_COMPONENTS+2] 2734 if z<lower_band { if above_band==0 { return NGB_E_CANDIDATE } } 2735 else { if above_zero==0 { return NGB_E_CANDIDATE } } 2736 v=v+1 2737 } 2738 // Every refusal precedes the first destination write. 2739 let dst: *i64=out as *i64 2740 v=0 2741 while v<ng { 2742 let query: i64=ga+1+v*NGP_COMPONENTS 2743 var floor: i64=0 2744 if h[query+2]<lower_band { floor=lower_band } 2745 var best: i64=0-1;var distance: i64=0 2746 u=0 2747 while u<nv { 2748 let candidate: i64=va+1+u*NGP_COMPONENTS 2749 if h[candidate+2]>=floor { 2750 let dx: i64=h[candidate]-h[query] 2751 let dy: i64=h[candidate+1]-h[query+1] 2752 let dz: i64=h[candidate+2]-h[query+2] 2753 let d: i64=dx*dx+dy*dy+dz*dz 2754 if best<0 || d<distance { best=u;distance=d } 2755 } 2756 if step>=nv-u { u=nv } else { u=u+step } 2757 } 2758 dst[v]=best;v=v+1 2759 } 2760 return need 2761} 2762 2763const NGB_CLIP_E_INPUT:i64=-30 2764const NGB_CLIP_E_RANGE:i64=-32 2765const NGB_CLIP_I64_MAX:i64=9223372036854775807 2766func ngb_abs(x:i64)->i64{if x<0{return 0-x};return x} 2767func ngb_clip_product(a:i64,b:i64)->i64 { 2768 if a<0||b<0{return -1};if b>0{if a>NGB_CLIP_I64_MAX/b{return -1}};return a*b 2769} 2770func ngb_clip_gcd(a:i64,b:i64)->i64 {var x:i64=a;var y:i64=b;while y>0{let z:i64=x%y;x=y;y=z};return x} 2771 2772// Twice polygon area in the canonical source triangle's (bary1,bary2) plane. 2773// Exact numerator/denominator; positive winding and <=1 means no inverted/expanded source coverage. 2774// Input records are already validated by the clipping boundary. Large common denominators refuse. 2775func ngb_clip_area_ratio_v1(poly:*i64,n:i64,result:*i64)->i64{ 2776 if n<3||n>7{return NGB_CLIP_E_INPUT};var den:i64=1;var i:i64=0 2777 while i<n{if poly[i*4+3]<=0{return NGB_CLIP_E_INPUT};den=ngb_clip_product(den,poly[i*4+3]);if den<0{return NGB_CLIP_E_RANGE};i=i+1} 2778 var sum:i64=0;i=0 2779 while i<n{let j:i64=(i+1)%n;let a:i64=ngb_clip_product(poly[i*4+1],poly[j*4+2]);let b:i64=ngb_clip_product(poly[i*4+2],poly[j*4+1]);if a<0||b<0{return NGB_CLIP_E_RANGE} 2780 let pair:i64=ngb_clip_product(poly[i*4+3],poly[j*4+3]);if pair<=0{return NGB_CLIP_E_RANGE} 2781 let term:i64=ngb_clip_product(ngb_abs(a-b),den/pair);if term<0{return NGB_CLIP_E_RANGE} 2782 if a>=b{if sum>NGB_CLIP_I64_MAX-term{return NGB_CLIP_E_RANGE};sum=sum+term} 2783 else{if sum<0-NGB_CLIP_I64_MAX+term{return NGB_CLIP_E_RANGE};sum=sum-term};i=i+1 2784 } 2785 let div:i64=ngb_clip_gcd(ngb_abs(sum),den);result[0]=sum/div;result[1]=den/div;return 0 2786} 2787 2788// GAT1 v1: additive source-surface attachments; legacy GARM is unchanged. 2789// Header 12 words: version,record_count,triangle_count,record_stride,VERT_check,TRIS_check, 2790// source_nv,source_nt,kind,total_words,triangle_stride,reserved. 2791// Record 8 words: source_face,bary0,bary1,bary2,denominator,normal_offset_units,material,flags. 2792// Triangle 3 words: attachment record indices. Coefficients remain exact i64 on disk. 2793const NGB_ATTACH_HDR:i64=12 2794const NGB_ATTACH_REC:i64=8 2795const NGB_ATTACH_COORD_MAX:i64=16777216 2796const NGB_ATTACH_E_SOURCE:i64=-40 2797const NGB_ATTACH_E_SHAPE:i64=-41 2798const NGB_ATTACH_E_GEOMETRY:i64=-42 2799// Exact integer coordinates and products of differences fit i64 under this boundary. 2800func ngb_attach_source_v1(b:*u8,n:i64)->i64{ 2801 if (b as i64)==0{return NGB_ATTACH_E_SOURCE} 2802 let va:i64=nxa_counted_section(b,n,nxa_tag4("VERT"),3);let ta:i64=nxa_counted_section(b,n,nxa_tag4("TRIS"),3) 2803 if va<0||ta<0{return NGB_ATTACH_E_SOURCE};let h:*i64=b as *i64;let nv:i64=h[va];let nt:i64=h[ta] 2804 if nv<3||nt<1{return NGB_ATTACH_E_SOURCE};var i:i64=0 2805 while i<nv*3{let p:i64=h[va+1+i];if p<0-NGB_ATTACH_COORD_MAX||p>NGB_ATTACH_COORD_MAX{return NGB_ATTACH_E_SOURCE};i=i+1} 2806 i=0;while i<nt*3{let v:i64=h[ta+1+i];if v<0||v>=nv{return NGB_ATTACH_E_SOURCE};i=i+1};return 0 2807} 2808func ngb_attach_face_v1(pos:*i64,idx:*i64,face:i64)->i64{ 2809 let a:i64=idx[face*3]*3;let b:i64=idx[face*3+1]*3;let c:i64=idx[face*3+2]*3 2810 let x:i64=pos[b]-pos[a];let y:i64=pos[b+1]-pos[a+1];let z:i64=pos[b+2]-pos[a+2] 2811 let u:i64=pos[c]-pos[a];let v:i64=pos[c+1]-pos[a+1];let w:i64=pos[c+2]-pos[a+2] 2812 if y*w-z*v==0&&z*u-x*w==0&&x*v-y*u==0{return NGB_ATTACH_E_GEOMETRY};return 0 2813} 2814// Validate before consumer upload. Native NXA checks are integrity checks, not cryptographic authentication. 2815// Caller workspace: 14 i64 words, disjoint from section/source; no output mesh or device mutation here. 2816func ngb_attach_validate_v1(b:*u8,n:i64,s:*i64,nw:i64,work:*i64,work_words:i64)->i64{ 2817 if (s as i64)==0||(work as i64)==0||nw<NGB_ATTACH_HDR||work_words<14{return NGB_ATTACH_E_SHAPE} 2818 let sr:i64=ngb_attach_source_v1(b,n);if sr<0{return sr} 2819 if s[0]!=1||s[3]!=NGB_ATTACH_REC||s[10]!=3||s[11]!=0||s[9]!=nw{return NGB_ATTACH_E_SHAPE} 2820 let nr:i64=s[1];let nt:i64=s[2];if nr<3||nt<1||nr>(nw-NGB_ATTACH_HDR)/NGB_ATTACH_REC{return NGB_ATTACH_E_SHAPE} 2821 let to:i64=NGB_ATTACH_HDR+nr*NGB_ATTACH_REC;if (nw-to)%3!=0||nt!=(nw-to)/3{return NGB_ATTACH_E_SHAPE} 2822 if s[8]<1||s[8]>9{return NGB_ATTACH_E_SHAPE} 2823 let ve:i64=nxa_section_entry(b,n,nxa_tag4("VERT"));let te:i64=nxa_section_entry(b,n,nxa_tag4("TRIS"));let h:*i64=b as *i64 2824 let va:i64=h[ve+1]/8;let ta:i64=h[te+1]/8 2825 if s[4]!=h[ve+3]||s[5]!=h[te+3]||s[6]!=h[va]||s[7]!=h[ta]{return NGB_ATTACH_E_SOURCE} 2826 let pos:*i64=((b as i64)+(va+1)*8) as *i64;let idx:*i64=((b as i64)+(ta+1)*8) as *i64 2827 var i:i64=0;while i<nr{let r:i64=NGB_ATTACH_HDR+i*NGB_ATTACH_REC;let face:i64=s[r];let den:i64=s[r+4] 2828 if face<0||face>=s[7]||den<=0{return NGB_ATTACH_E_SHAPE} 2829 var sum:i64=0;var k:i64=1;while k<4{let a:i64=s[r+k];if a<0||a>den-sum{return NGB_ATTACH_E_SHAPE};sum=sum+a;k=k+1} 2830 if sum!=den||s[r+5]<0||s[r+5]>NGB_ATTACH_COORD_MAX||s[r+6]!=s[8]||s[r+7]!=0{return NGB_ATTACH_E_SHAPE} 2831 if ngb_attach_face_v1(pos,idx,face)<0{return NGB_ATTACH_E_GEOMETRY};i=i+1 2832 } 2833 i=0;while i<nt{var face:i64=-1;var k:i64=0 2834 while k<3{let v:i64=s[to+i*3+k];if v<0||v>=nr{return NGB_ATTACH_E_SHAPE};let r:i64=NGB_ATTACH_HDR+v*NGB_ATTACH_REC 2835 if k==0{face=s[r]}else{if face!=s[r]{return NGB_ATTACH_E_GEOMETRY}} 2836 var j:i64=0;while j<4{work[k*4+j]=s[r+1+j];j=j+1};k=k+1 2837 } 2838 let area:*i64=((work as i64)+96) as *i64;let ar:i64=ngb_clip_area_ratio_v1(work,3,area) 2839 if ar<0{return ar};if area[0]<=0||area[0]>area[1]{return NGB_ATTACH_E_GEOMETRY};i=i+1 2840 };return 0 2841} 2842 2843// GAP1 upload v1: 16 uint32 header lanes, then position xyz, source-index xyzw, 2844// barycentric xyz/source-unit normal-offset w and uint32 triangle indices. 2845// Disk GAT1 remains exact i64. GPU conversion occurs only after complete native validation. 2846const NGB_PACK_HEADER:i64=64 2847const NGB_PACK_VERTEX:i64=44 2848const NGB_E_PACK_INDEX:i64=-43 2849const NGB_E_PACK_EXTENT:i64=-44 2850// This is an integer-only exactness test; it does not round before checking. 2851func ngb_source_index_f32_v1(v:i64,nv:i64)->i64{ 2852 if v<0||v>=nv||v>2147483647{return NGB_E_PACK_INDEX} 2853 var top:i64=16777216;var spacing:i64=1 2854 while v>=top*2{top=top*2;spacing=spacing*2} 2855 if v>top{spacing=spacing*2} 2856 if v%spacing!=0{return NGB_E_PACK_INDEX};return 0 2857} 2858func ngb_attachment_bytes_v1(b:*u8,n:i64,work:*i64,work_bytes:i64)->i64{ 2859 if (b as i64)==0||work_bytes<112{return NGB_ATTACH_E_SHAPE} 2860 let wr:i64=ngp_output(b,n,work as *u8,work_bytes,112);if wr<0{return wr} 2861 let ae:i64=nxa_section_entry(b,n,nxa_tag4("GAT1"));if ae<0{return ae} 2862 let h:*i64=b as *i64;var vi:i64=0;var ti:i64=0;var ai:i64=0;var entry:i64=0 2863 while entry<h[2]{let tag:i64=h[4+entry*4];if tag==nxa_tag4("VERT"){vi=vi+1};if tag==nxa_tag4("TRIS"){ti=ti+1};if tag==nxa_tag4("GAT1"){ai=ai+1};entry=entry+1} 2864 if vi!=1||ti!=1||ai!=1{return NGB_ATTACH_E_SHAPE} 2865 let s:*i64=((b as i64)+h[ae+1]) as *i64 2866 let rc:i64=ngb_attach_validate_v1(b,n,s,h[ae+2],work,14);if rc<0{return rc} 2867 let nr:i64=s[1];let nt:i64=s[2] 2868 if nr>(4294967295-NGB_PACK_HEADER)/NGB_PACK_VERTEX{return NGB_E_PACK_EXTENT} 2869 let base:i64=NGB_PACK_HEADER+nr*NGB_PACK_VERTEX 2870 if nt>(4294967295-base)/12{return NGB_E_PACK_EXTENT} 2871 let ta:i64=nxa_counted_section(b,n,nxa_tag4("TRIS"),3) 2872 var i:i64=0;while i<nr{let face:i64=s[NGB_ATTACH_HDR+i*NGB_ATTACH_REC];var k:i64=0 2873 while k<3{let si:i64=h[ta+1+face*3+k];if ngb_source_index_f32_v1(si,s[6])<0{return NGB_E_PACK_INDEX};k=k+1};i=i+1} 2874 return base+nt*12 2875} 2876func ngb_attachment_pack_v1(b:*u8,n:i64,out:*u8,cap:i64,work:*i64,work_bytes:i64)->i64{ 2877 if cap>0&&(out as i64)>0{let early:i64=ngp_output(work as *u8,112,out,cap,cap);if early<0{return early}} 2878 let need:i64=ngb_attachment_bytes_v1(b,n,work,work_bytes);if need<0{return need} 2879 let rc:i64=ngp_output(b,n,out,cap,need);if rc<0{return rc} 2880 let overlap:i64=ngp_output(work as *u8,112,out,cap,need);if overlap<0{return overlap} 2881 let ae:i64=nxa_section_entry(b,n,nxa_tag4("GAT1"));let h:*i64=b as *i64;let s:*i64=((b as i64)+h[ae+1]) as *i64 2882 let va:i64=nxa_counted_section(b,n,nxa_tag4("VERT"),3);let ta:i64=nxa_counted_section(b,n,nxa_tag4("TRIS"),3) 2883 let nr:i64=s[1];let nt:i64=s[2];let po:i64=64;let jo:i64=po+nr*12;let wo:i64=jo+nr*16;let io:i64=wo+nr*16 2884 let dst:*u32=out as *u32;var i:i64=0;while i<16{dst[i]=0 as u32;i=i+1} 2885 dst[0]=827343175 as u32;dst[1]=1 as u32;dst[2]=nr as u32;dst[3]=nt as u32;dst[4]=s[6] as u32;dst[5]=s[8] as u32 2886 dst[6]=po as u32;dst[7]=jo as u32;dst[8]=wo as u32;dst[9]=io as u32;dst[10]=need as u32;dst[11]=1 as u32 2887 i=0;while i<nr{let r:i64=NGB_ATTACH_HDR+i*NGB_ATTACH_REC;let face:i64=s[r];let den:i64=__f32_from_i64(s[r+4]);var k:i64=0 2888 while k<3{let si:i64=h[ta+1+face*3+k];dst[jo/4+i*4+k]=__f32_from_i64(si) as u32;dst[wo/4+i*4+k]=__f32_div(__f32_from_i64(s[r+1+k]),den) as u32;k=k+1} 2889 dst[jo/4+i*4+3]=0 as u32;dst[wo/4+i*4+3]=__f32_from_i64(s[r+5]) as u32 2890 var q:i64=0;while q<3{var acc:i64=__f32_from_i64(0);k=0 2891 while k<3{let si:i64=h[ta+1+face*3+k];let ratio:i64=dst[wo/4+i*4+k] as i64;acc=__f32_add(acc,__f32_mul(__f32_from_i64(h[va+1+si*3+q]),ratio));k=k+1} 2892 dst[po/4+i*3+q]=acc as u32;q=q+1};i=i+1} 2893 let to:i64=NGB_ATTACH_HDR+nr*NGB_ATTACH_REC;i=0;while i<nt*3{dst[io/4+i]=s[to+i] as u32;i=i+1};return need 2894} 2895 2896const NSA_E_INPUT:i64=-50 2897const NSA_E_RANGE:i64=-51 2898const NSA_E_MISS:i64=-52 2899const NSA_E_AMBIGUOUS:i64=-53 2900// Exact fraction comparison without cross-products. Inputs nonnegative, denominators positive. 2901func nsa_unsigned_ratio_cmp(a:i64,b:i64,c:i64,d:i64)->i64{ 2902 var x:i64=a;var y:i64=b;var z:i64=c;var w:i64=d;var sign:i64=1 2903 while true{let p:i64=x/y;let q:i64=z/w;if p<q{return 0-sign};if p>q{return sign} 2904 let r:i64=x%y;let s:i64=z%w;if r==0{if s==0{return 0};return 0-sign};if s==0{return sign} 2905 x=y;y=r;z=w;w=s;sign=0-sign 2906 };return 0 2907} 2908func nsa_ratio_cmp(a:i64,b:i64,c:i64,d:i64)->i64{ 2909 if a<0{if c>=0{return -1};return 0-nsa_unsigned_ratio_cmp(0-a,b,0-c,d)} 2910 if c<0{return 1};return nsa_unsigned_ratio_cmp(a,b,c,d) 2911} 2912// Coordinate bounds inherited from canonical source validation make 2D determinants exact i64. 2913// y numerator is checked independently, since determinant times depth can exceed i64. 2914func nsa_triangle_y_v1(p:*i64,idx:*i64,face:i64,qx:i64,qz:i64,result:*i64)->i64{ 2915 let a:i64=idx[face*3]*3;let b:i64=idx[face*3+1]*3;let c:i64=idx[face*3+2]*3 2916 let x:i64=p[b]-p[a];let z:i64=p[b+2]-p[a+2];let u:i64=p[c]-p[a];let v:i64=p[c+2]-p[a+2] 2917 let dx:i64=qx-p[a];let dz:i64=qz-p[a+2];var den:i64=x*v-z*u 2918 if den==0{return NSA_E_MISS};var w1:i64=dx*v-dz*u;var w2:i64=x*dz-z*dx 2919 if den<0{den=0-den;w1=0-w1;w2=0-w2};if w1<0||w2<0||w1>den||w2>den-w1{return NSA_E_MISS} 2920 var w0:i64=den-w1-w2;let gcd:i64=ngb_clip_gcd(ngb_clip_gcd(w0,w1),ngb_clip_gcd(w2,den));w0=w0/gcd;w1=w1/gcd;w2=w2/gcd;den=den/gcd 2921 result[0]=w0;result[1]=w1;result[2]=w2;result[3]=den;var depth:i64=0;var k:i64=0 2922 while k<3{let y:i64=p[idx[face*3+k]*3+1];let mag:i64=ngb_clip_product(ngb_abs(y),result[k]);if mag<0{return NSA_E_RANGE} 2923 if y<0{if depth<0-NGB_CLIP_I64_MAX+mag{return NSA_E_RANGE};depth=depth-mag} 2924 else{if depth>NGB_CLIP_I64_MAX-mag{return NSA_E_RANGE};depth=depth+mag};k=k+1 2925 };result[4]=depth;return 0 2926} 2927// direction=-1 chooses smallest Y, +1 greatest Y. Integer query coordinates are caller data. 2928// Caller output: five i64 words; scratch: twelve disjoint i64 words. Output unchanged on refusal. 2929// Equal-depth overlapping hits refuse instead of guessing an attachment seam. 2930func nsa_landmark_y_v1(b:*u8,n:i64,qx:i64,qz:i64,direction:i64,out:*i64,cap:i64,work:*i64,words:i64)->i64{ 2931 if direction!=-1&&direction!=1{return NSA_E_INPUT} 2932 if qx<0-NGB_ATTACH_COORD_MAX||qx>NGB_ATTACH_COORD_MAX||qz<0-NGB_ATTACH_COORD_MAX||qz>NGB_ATTACH_COORD_MAX{return NSA_E_RANGE} 2933 if cap<5||words<12{return NSA_E_INPUT};let valid:i64=ngb_attach_source_v1(b,n);if valid<0{return valid} 2934 let ow:i64=ngp_output(b,n,out as *u8,40,40);if ow<0{return ow} 2935 let sw:i64=ngp_output(b,n,work as *u8,96,96);if sw<0{return sw} 2936 let overlap:i64=ngp_output(work as *u8,96,out as *u8,40,40);if overlap<0{return overlap} 2937 let va:i64=nxa_counted_section(b,n,nxa_tag4("VERT"),3);let ta:i64=nxa_counted_section(b,n,nxa_tag4("TRIS"),3);let h:*i64=b as *i64 2938 let p:*i64=((b as i64)+(va+1)*8) as *i64;let idx:*i64=((b as i64)+(ta+1)*8) as *i64 2939 return nsa_landmark_mesh_y_v1(p,idx,h[ta],qx,qz,direction,out,work) 2940} 2941// Internal mesh view: source and disjoint storage already validated by boundary. 2942func nsa_landmark_mesh_y_v1(p:*i64,idx:*i64,nt:i64,qx:i64,qz:i64,direction:i64,out:*i64,work:*i64)->i64{ 2943 let trial:*i64=work;let best:*i64=((work as i64)+48) as *i64;var face:i64=0;var selected:i64=-1;var ambiguous:i64=0 2944 while face<nt{let rc:i64=nsa_triangle_y_v1(p,idx,face,qx,qz,trial);if rc==NSA_E_RANGE{return rc} 2945 if rc==0{var comparison:i64=0;if selected>=0{comparison=nsa_ratio_cmp(trial[4],trial[3],best[4],best[3])} 2946 if selected<0||comparison==direction{selected=face;ambiguous=0;var k:i64=0;while k<5{best[k]=trial[k];k=k+1}} 2947 else{if comparison==0{ambiguous=1}} 2948 };face=face+1 2949 };if selected<0{return NSA_E_MISS};if ambiguous!=0{return NSA_E_AMBIGUOUS} 2950 out[0]=selected;var k:i64=0;while k<4{out[k+1]=best[k];k=k+1};return 0 2951} 2952 2953// Topological correspondence only: labels identify connected source vertices, never anatomy. 2954// Stable label is the minimum source vertex index, independent of triangle order. 2955func nsa_component_root_v1(labels:*i64,v:i64)->i64{ 2956 var root:i64=v;while labels[root]!=root{root=labels[root]} 2957 var q:i64=v;while labels[q]!=q{let next:i64=labels[q];labels[q]=root;q=next};return root 2958} 2959func nsa_component_join_v1(labels:*i64,a:i64,b:i64)->i64{ 2960 let x:i64=nsa_component_root_v1(labels,a);let y:i64=nsa_component_root_v1(labels,b) 2961 if x<y{labels[y]=x}else{if y<x{labels[x]=y}};return 0 2962} 2963// Boundary validation completes before output writes. Capacity is measured in i64 labels. 2964// Required capacity derives from the validated VERT count; isolated vertices remain components. 2965func nsa_component_labels_v1(b:*u8,n:i64,labels:*i64,capacity:i64)->i64{ 2966 let valid:i64=ngb_attach_source_v1(b,n);if valid<0{return valid} 2967 let va:i64=nxa_counted_section(b,n,nxa_tag4("VERT"),3);let ta:i64=nxa_counted_section(b,n,nxa_tag4("TRIS"),3);let h:*i64=b as *i64 2968 let nv:i64=h[va];let nt:i64=h[ta];if capacity<nv{return NSA_E_INPUT} 2969 if nv>NGB_CLIP_I64_MAX/8{return NSA_E_RANGE} 2970 let space:i64=ngp_output(b,n,labels as *u8,nv*8,nv*8);if space<0{return space} 2971 let idx:*i64=((b as i64)+(ta+1)*8) as *i64 2972 var i:i64=0;while i<nv{labels[i]=i;i=i+1} 2973 i=0;while i<nt{let a:i64=idx[i*3];nsa_component_join_v1(labels,a,idx[i*3+1]);nsa_component_join_v1(labels,a,idx[i*3+2]);i=i+1} 2974 var count:i64=0;i=0;while i<nv{labels[i]=nsa_component_root_v1(labels,i);if labels[i]==i{count=count+1};i=i+1};return count 2975} 2976 2977const NSA_E_MIXED_SUPPORT:i64=-54 2978// Derive rigid attachment support from the actual connected component and skin weights. 2979// A mixed-joint component requires shared deformation, never an inferred replacement joint. 2980func nsa_rigid_component_joint_v1(b:*u8,n:i64,seed:i64,labels:*i64,capacity:i64)->i64{ 2981 let rc:i64=nsa_component_labels_v1(b,n,labels,capacity);if rc<0{return rc} 2982 let va:i64=nxa_counted_section(b,n,nxa_tag4("VERT"),3);let sk:i64=nxa_counted_section(b,n,nxa_tag4("SKIN"),8);let sj:i64=nxa_counted_section(b,n,nxa_tag4("SKEL"),8);if va<0||sk<0||sj<0{return NSA_E_INPUT} 2983 let h:*i64=b as *i64;let nv:i64=h[va];let nj:i64=h[sj];if seed<0||seed>=nv||h[sk]!=nv||nj<1{return NSA_E_INPUT} 2984 let component:i64=labels[seed];var selected:i64=-1;var v:i64=0 2985 while v<nv{if labels[v]==component{var sum:i64=0;var k:i64=0;while k<4{let j:i64=h[sk+1+v*8+k];let w:i64=h[sk+1+v*8+4+k];if j<0||j>=nj||w<0||w>4096{return NSA_E_INPUT};sum=sum+w;if w>0{if selected<0{selected=j}else{if selected!=j{return NSA_E_MIXED_SUPPORT}}};k=k+1};if sum!=4096{return NSA_E_INPUT}};v=v+1};return selected 2986} 2987 2988// Exact source component boundary edges, not anatomical landmarks. 2989// A measure call uses out=null,out_words=0 and still requires labels/sort workspace. 2990// Labels need nv words; sort workspace needs 3*nt words. Output is [a,b] per edge. 2991// All storage is caller owned. Output remains unchanged on refusal. 2992const NSA_E_NONMANIFOLD:i64=-55 2993func nsa_component_boundary_v1(b:*u8,n:i64,seed:i64,labels:*i64,label_words:i64,work:*i64,work_words:i64,out:*i64,out_words:i64)->i64{ 2994 let valid:i64=ngb_attach_source_v1(b,n);if valid<0{return valid} 2995 let va:i64=nxa_counted_section(b,n,nxa_tag4("VERT"),3);let ta:i64=nxa_counted_section(b,n,nxa_tag4("TRIS"),3);let h:*i64=b as *i64;let nv:i64=h[va];let nt:i64=h[ta] 2996 if seed<0||seed>=nv||label_words<nv||nt>NGB_CLIP_I64_MAX/24||nv>NGB_CLIP_I64_MAX/8||work_words<nt*3||out_words<0||out_words>NGB_CLIP_I64_MAX/8{return NSA_E_INPUT} 2997 if ngb_clip_product(nv,nv)<0{return NSA_E_RANGE} 2998 let lb:i64=nv*8;let wb:i64=nt*24;let ob:i64=out_words*8 2999 if ngp_output(b,n,labels as *u8,lb,lb)<0||ngp_output(b,n,work as *u8,wb,wb)<0||ngp_output(labels as *u8,lb,work as *u8,wb,wb)<0{return NSA_E_INPUT} 3000 if ob>0{if ngp_output(b,n,out as *u8,ob,ob)<0||ngp_output(labels as *u8,lb,out as *u8,ob,ob)<0||ngp_output(work as *u8,wb,out as *u8,ob,ob)<0{return NSA_E_INPUT}} 3001 if ob==0&&(out as i64)!=0{return NSA_E_INPUT} 3002 let rc:i64=nsa_component_labels_v1(b,n,labels,label_words);if rc<0{return rc};let root:i64=labels[seed];let idx:*i64=((b as i64)+(ta+1)*8) as *i64 3003 var count:i64=0;var t:i64=0;while t<nt{if labels[idx[t*3]]==root{var k:i64=0;while k<3{let a:i64=idx[t*3+k];let c:i64=idx[t*3+(k+1)%3];if a<c{work[count]=a*nv+c}else{work[count]=c*nv+a};count=count+1;k=k+1}};t=t+1} 3004 nx_quicksort(work as *nx_int,count);var at:i64=0;var boundary:i64=0 3005 while at<count{var end:i64=at+1;while end<count&&work[end]==work[at]{end=end+1};if end-at>2{return NSA_E_NONMANIFOLD};if end-at==1{boundary=boundary+1};at=end} 3006 if ob==0{return boundary};if boundary>out_words/2{return NSA_E_INPUT} 3007 at=0;var written:i64=0;while at<count{var end:i64=at+1;while end<count&&work[end]==work[at]{end=end+1};if end-at==1{out[written*2]=work[at]/nv;out[written*2+1]=work[at]%nv;written=written+1};at=end} 3008 return written 3009} 3010 3011 3012func sa_put(s:*u8)->i64{var n:i64=0;while s[n]!=0{n=n+1};return sys_write(1,s,n)} 3013func sa_num(x:i64)->i64{let p:*u8=sys_mmap(32) as *u8;if (p as i64)<=0{return -1};var n:i64=x;var i:i64=31;if n<0{sa_put("-");n=0-n};p[i]=0;i=i-1;if n==0{p[i]=48;i=i-1};while n>0{p[i]=(48+n%10) as u8;n=n/10;i=i-1};sa_put(((p as i64)+i+1) as *u8);return sys_munmap(p,32)} 3014func sa_check(ok:i64,name:*u8,c:*i64)->i64{c[0]=c[0]+1;if ok==1{sa_put("PASS ")}else{sa_put("FAIL ");c[1]=c[1]+1};sa_put(name);sa_put(" 3015");return ok} 3016 3017func main(argc:i64,argv:*i64)->i64{ 3018 if argc!=2{return 2};let len:*i64=sys_mmap(16) as *i64;let b:*u8=sys_map_file(argv[1] as *u8,len);if (b as i64)==0{return 3};let n:i64=len[0];let valid:i64=ngb_attach_source_v1(b,n);if valid<0{return 4} 3019 let h:*i64=b as *i64;let va:i64=nxa_counted_section(b,n,nxa_tag4("VERT"),3);let ta:i64=nxa_counted_section(b,n,nxa_tag4("TRIS"),3);let nv:i64=h[va];let nt:i64=h[ta] 3020 let labels:*i64=sys_mmap_try(nv*8) as *i64;let work:*i64=sys_mmap_try(nt*24) as *i64;let out:*i64=sys_mmap_try(nt*48) as *i64;let ctr:*i64=sys_mmap_try(16) as *i64;if (labels as i64)<=0||(work as i64)<=0||(out as i64)<=0||(ctr as i64)<=0{return 5};ctr[0]=0;ctr[1]=0;var rc:i64=0;var same:i64=0; 3021 rc=nsa_component_boundary_v1(b,n,11012,labels,nv,work,nt*3,out,nt*6);sa_check(rc==10,"actual component11012 boundary cardinality",ctr); 3022same=1;if out[0]!=11012||out[1]!=11015{same=0};if out[2]!=11012||out[3]!=11024{same=0};if out[4]!=11015||out[5]!=11017{same=0};if out[6]!=11017||out[7]!=11036{same=0};if out[8]!=11024||out[9]!=11026{same=0};if out[10]!=11026||out[11]!=11028{same=0};if out[12]!=11028||out[13]!=11030{same=0};if out[14]!=11030||out[15]!=11032{same=0};if out[16]!=11032||out[17]!=11034{same=0};if out[18]!=11034||out[19]!=11036{same=0};sa_check(same==1,"actual component11012 exact endpoint identity",ctr); 3023rc=nsa_component_boundary_v1(b,n,13110,labels,nv,work,nt*3,out,nt*6);sa_check(rc==10,"actual component13110 boundary cardinality",ctr); 3024same=1;if out[0]!=13112||out[1]!=13113{same=0};if out[2]!=13112||out[3]!=13123{same=0};if out[4]!=13113||out[5]!=13115{same=0};if out[6]!=13115||out[7]!=13135{same=0};if out[8]!=13123||out[9]!=13125{same=0};if out[10]!=13125||out[11]!=13126{same=0};if out[12]!=13126||out[13]!=13128{same=0};if out[14]!=13128||out[15]!=13131{same=0};if out[16]!=13131||out[17]!=13133{same=0};if out[18]!=13133||out[19]!=13135{same=0};sa_check(same==1,"actual component13110 exact endpoint identity",ctr); 3025rc=nsa_component_boundary_v1(b,n,13411,labels,nv,work,nt*3,out,nt*6);sa_check(rc==48,"actual component13411 boundary cardinality",ctr); 3026same=1;if out[0]!=13412||out[1]!=13413{same=0};if out[2]!=13412||out[3]!=13482{same=0};if out[4]!=13413||out[5]!=13442{same=0};if out[6]!=13426||out[7]!=13427{same=0};if out[8]!=13426||out[9]!=13472{same=0};if out[10]!=13427||out[11]!=13433{same=0};if out[12]!=13430||out[13]!=13431{same=0};if out[14]!=13430||out[15]!=13460{same=0};if out[16]!=13431||out[17]!=13472{same=0};if out[18]!=13433||out[19]!=13481{same=0};if out[20]!=13435||out[21]!=13436{same=0};if out[22]!=13435||out[23]!=13439{same=0};if out[24]!=13436||out[25]!=13437{same=0};if out[26]!=13437||out[27]!=13459{same=0};if out[28]!=13439||out[29]!=13442{same=0};if out[30]!=13459||out[31]!=13460{same=0};if out[32]!=13476||out[33]!=13477{same=0};if out[34]!=13476||out[35]!=13478{same=0};if out[36]!=13477||out[37]!=13487{same=0};if out[38]!=13478||out[39]!=13479{same=0};if out[40]!=13479||out[41]!=13480{same=0};if out[42]!=13480||out[43]!=13481{same=0};if out[44]!=13482||out[45]!=13672{same=0};if out[46]!=13487||out[47]!=13762{same=0};if out[48]!=13492||out[49]!=13493{same=0};if out[50]!=13492||out[51]!=13645{same=0};if out[52]!=13493||out[53]!=13498{same=0};if out[54]!=13498||out[55]!=13634{same=0};if out[56]!=13501||out[57]!=13502{same=0};if out[58]!=13501||out[59]!=13650{same=0};if out[60]!=13502||out[61]!=13651{same=0};if out[62]!=13504||out[63]!=13505{same=0};if out[64]!=13504||out[65]!=13600{same=0};if out[66]!=13505||out[67]!=13672{same=0};if out[68]!=13600||out[69]!=13620{same=0};if out[70]!=13608||out[71]!=13609{same=0};if out[72]!=13608||out[73]!=13612{same=0};if out[74]!=13609||out[75]!=13645{same=0};if out[76]!=13612||out[77]!=13651{same=0};if out[78]!=13613||out[79]!=13614{same=0};if out[80]!=13613||out[81]!=13634{same=0};if out[82]!=13614||out[83]!=13615{same=0};if out[84]!=13615||out[85]!=13619{same=0};if out[86]!=13619||out[87]!=13620{same=0};if out[88]!=13648||out[89]!=13649{same=0};if out[90]!=13648||out[91]!=13656{same=0};if out[92]!=13649||out[93]!=13650{same=0};if out[94]!=13656||out[95]!=13762{same=0};sa_check(same==1,"actual component13411 exact endpoint identity",ctr); 3027rc=nsa_component_boundary_v1(b,n,13510,labels,nv,work,nt*3,out,nt*6);sa_check(rc==52,"actual component13510 boundary cardinality",ctr); 3028same=1;if out[0]!=13512||out[1]!=13513{same=0};if out[2]!=13512||out[3]!=13523{same=0};if out[4]!=13513||out[5]!=13596{same=0};if out[6]!=13522||out[7]!=13523{same=0};if out[8]!=13522||out[9]!=13557{same=0};if out[10]!=13527||out[11]!=13528{same=0};if out[12]!=13527||out[13]!=13599{same=0};if out[14]!=13528||out[15]!=13537{same=0};if out[16]!=13531||out[17]!=13532{same=0};if out[18]!=13531||out[19]!=13535{same=0};if out[20]!=13532||out[21]!=13560{same=0};if out[22]!=13535||out[23]!=13537{same=0};if out[24]!=13557||out[25]!=13559{same=0};if out[26]!=13559||out[27]!=13565{same=0};if out[28]!=13560||out[29]!=13561{same=0};if out[30]!=13561||out[31]!=13588{same=0};if out[32]!=13562||out[33]!=13563{same=0};if out[34]!=13562||out[35]!=13592{same=0};if out[36]!=13563||out[37]!=13564{same=0};if out[38]!=13564||out[39]!=13565{same=0};if out[40]!=13573||out[41]!=13574{same=0};if out[42]!=13573||out[43]!=13576{same=0};if out[44]!=13574||out[45]!=13592{same=0};if out[46]!=13576||out[47]!=13588{same=0};if out[48]!=13596||out[49]!=13678{same=0};if out[50]!=13599||out[51]!=13684{same=0};if out[52]!=13677||out[53]!=13678{same=0};if out[54]!=13677||out[55]!=13688{same=0};if out[56]!=13683||out[57]!=13684{same=0};if out[58]!=13683||out[59]!=13702{same=0};if out[60]!=13685||out[61]!=13688{same=0};if out[62]!=13685||out[63]!=13698{same=0};if out[64]!=13698||out[65]!=13699{same=0};if out[66]!=13699||out[67]!=13731{same=0};if out[68]!=13701||out[69]!=13702{same=0};if out[70]!=13701||out[71]!=13711{same=0};if out[72]!=13705||out[73]!=13706{same=0};if out[74]!=13705||out[75]!=13735{same=0};if out[76]!=13706||out[77]!=13709{same=0};if out[78]!=13709||out[79]!=13711{same=0};if out[80]!=13731||out[81]!=13733{same=0};if out[82]!=13733||out[83]!=13739{same=0};if out[84]!=13735||out[85]!=13736{same=0};if out[86]!=13736||out[87]!=13756{same=0};if out[88]!=13737||out[89]!=13738{same=0};if out[90]!=13737||out[91]!=13740{same=0};if out[92]!=13738||out[93]!=13759{same=0};if out[94]!=13739||out[95]!=13740{same=0};if out[96]!=13743||out[97]!=13744{same=0};if out[98]!=13743||out[99]!=13759{same=0};if out[100]!=13744||out[101]!=13746{same=0};if out[102]!=13746||out[103]!=13756{same=0};sa_check(same==1,"actual component13510 exact endpoint identity",ctr); 3029rc=nsa_component_boundary_v1(b,n,13763,labels,nv,work,nt*3,out,nt*6);sa_check(rc==48,"actual component13763 boundary cardinality",ctr); 3030same=1;if out[0]!=13764||out[1]!=13765{same=0};if out[2]!=13764||out[3]!=13834{same=0};if out[4]!=13765||out[5]!=13794{same=0};if out[6]!=13778||out[7]!=13779{same=0};if out[8]!=13778||out[9]!=13824{same=0};if out[10]!=13779||out[11]!=13785{same=0};if out[12]!=13781||out[13]!=13782{same=0};if out[14]!=13781||out[15]!=13814{same=0};if out[16]!=13782||out[17]!=13824{same=0};if out[18]!=13785||out[19]!=13833{same=0};if out[20]!=13787||out[21]!=13788{same=0};if out[22]!=13787||out[23]!=13790{same=0};if out[24]!=13788||out[25]!=13813{same=0};if out[26]!=13790||out[27]!=13791{same=0};if out[28]!=13791||out[29]!=13794{same=0};if out[30]!=13813||out[31]!=13814{same=0};if out[32]!=13828||out[33]!=13829{same=0};if out[34]!=13828||out[35]!=13839{same=0};if out[36]!=13829||out[37]!=13830{same=0};if out[38]!=13830||out[39]!=13831{same=0};if out[40]!=13831||out[41]!=13832{same=0};if out[42]!=13832||out[43]!=13833{same=0};if out[44]!=13834||out[45]!=14024{same=0};if out[46]!=13839||out[47]!=14114{same=0};if out[48]!=13844||out[49]!=13847{same=0};if out[50]!=13844||out[51]!=13850{same=0};if out[52]!=13847||out[53]!=13997{same=0};if out[54]!=13850||out[55]!=13986{same=0};if out[56]!=13852||out[57]!=13853{same=0};if out[58]!=13852||out[59]!=14002{same=0};if out[60]!=13853||out[61]!=14003{same=0};if out[62]!=13856||out[63]!=13857{same=0};if out[64]!=13856||out[65]!=13952{same=0};if out[66]!=13857||out[67]!=14024{same=0};if out[68]!=13952||out[69]!=13972{same=0};if out[70]!=13960||out[71]!=13961{same=0};if out[72]!=13960||out[73]!=13964{same=0};if out[74]!=13961||out[75]!=13997{same=0};if out[76]!=13964||out[77]!=14003{same=0};if out[78]!=13965||out[79]!=13966{same=0};if out[80]!=13965||out[81]!=13968{same=0};if out[82]!=13966||out[83]!=13971{same=0};if out[84]!=13968||out[85]!=13986{same=0};if out[86]!=13971||out[87]!=13972{same=0};if out[88]!=14000||out[89]!=14001{same=0};if out[90]!=14000||out[91]!=14008{same=0};if out[92]!=14001||out[93]!=14002{same=0};if out[94]!=14008||out[95]!=14114{same=0};sa_check(same==1,"actual component13763 exact endpoint identity",ctr); 3031rc=nsa_component_boundary_v1(b,n,13862,labels,nv,work,nt*3,out,nt*6);sa_check(rc==52,"actual component13862 boundary cardinality",ctr); 3032same=1;if out[0]!=13864||out[1]!=13865{same=0};if out[2]!=13864||out[3]!=13875{same=0};if out[4]!=13865||out[5]!=13948{same=0};if out[6]!=13874||out[7]!=13875{same=0};if out[8]!=13874||out[9]!=13908{same=0};if out[10]!=13879||out[11]!=13880{same=0};if out[12]!=13879||out[13]!=13951{same=0};if out[14]!=13880||out[15]!=13889{same=0};if out[16]!=13883||out[17]!=13884{same=0};if out[18]!=13883||out[19]!=13887{same=0};if out[20]!=13884||out[21]!=13912{same=0};if out[22]!=13887||out[23]!=13889{same=0};if out[24]!=13908||out[25]!=13910{same=0};if out[26]!=13910||out[27]!=13916{same=0};if out[28]!=13912||out[29]!=13913{same=0};if out[30]!=13913||out[31]!=13940{same=0};if out[32]!=13914||out[33]!=13915{same=0};if out[34]!=13914||out[35]!=13917{same=0};if out[36]!=13915||out[37]!=13944{same=0};if out[38]!=13916||out[39]!=13917{same=0};if out[40]!=13925||out[41]!=13926{same=0};if out[42]!=13925||out[43]!=13928{same=0};if out[44]!=13926||out[45]!=13944{same=0};if out[46]!=13928||out[47]!=13940{same=0};if out[48]!=13948||out[49]!=14029{same=0};if out[50]!=13951||out[51]!=14036{same=0};if out[52]!=14029||out[53]!=14030{same=0};if out[54]!=14030||out[55]!=14040{same=0};if out[56]!=14035||out[57]!=14036{same=0};if out[58]!=14035||out[59]!=14054{same=0};if out[60]!=14037||out[61]!=14040{same=0};if out[62]!=14037||out[63]!=14050{same=0};if out[64]!=14050||out[65]!=14051{same=0};if out[66]!=14051||out[67]!=14083{same=0};if out[68]!=14053||out[69]!=14054{same=0};if out[70]!=14053||out[71]!=14063{same=0};if out[72]!=14057||out[73]!=14058{same=0};if out[74]!=14057||out[75]!=14087{same=0};if out[76]!=14058||out[77]!=14061{same=0};if out[78]!=14061||out[79]!=14063{same=0};if out[80]!=14083||out[81]!=14085{same=0};if out[82]!=14085||out[83]!=14091{same=0};if out[84]!=14087||out[85]!=14088{same=0};if out[86]!=14088||out[87]!=14108{same=0};if out[88]!=14089||out[89]!=14090{same=0};if out[90]!=14089||out[91]!=14092{same=0};if out[92]!=14090||out[93]!=14111{same=0};if out[94]!=14091||out[95]!=14092{same=0};if out[96]!=14095||out[97]!=14096{same=0};if out[98]!=14095||out[99]!=14111{same=0};if out[100]!=14096||out[101]!=14098{same=0};if out[102]!=14098||out[103]!=14108{same=0};sa_check(same==1,"actual component13862 exact endpoint identity",ctr); 3033 3034 out[0]=991;out[1]=992;sa_check(nsa_component_boundary_v1(b,n,13411,labels,nv,work,nt*3,0 as *i64,0)==48&&out[0]==991,"measure returns required edges without output",ctr); 3035 sa_check(nsa_component_boundary_v1(b,n,13411,labels,nv,work,nt*3,out,95)==NSA_E_INPUT&&out[0]==991&&out[1]==992,"one word short output refuses before write",ctr); 3036 sa_check(nsa_component_boundary_v1(b,n,-1,labels,nv,work,nt*3,out,96)==NSA_E_INPUT&&out[0]==991,"negative seed refuses",ctr); 3037 sa_check(nsa_component_boundary_v1(b,n,nv,labels,nv,work,nt*3,out,96)==NSA_E_INPUT&&out[0]==991,"one past seed refuses",ctr); 3038 sa_check(nsa_component_boundary_v1(b,n,13411,labels,nv-1,work,nt*3,out,96)==NSA_E_INPUT&&out[0]==991,"short label storage refuses",ctr); 3039 sa_check(nsa_component_boundary_v1(b,n,13411,labels,nv,work,nt*3-1,out,96)==NSA_E_INPUT&&out[0]==991,"short sort workspace refuses",ctr); 3040 sa_check(nsa_component_boundary_v1(b,n,13411,labels,nv,work,nt*3,labels,96)==NSA_E_INPUT,"output-label alias refuses",ctr); 3041 sa_check(nsa_component_boundary_v1(b,n,13411,labels,nv,work,nt*3,work,96)==NSA_E_INPUT,"output-sort alias refuses",ctr); 3042 sa_check(nsa_component_boundary_v1(b,n,13411,labels,nv,labels,nt*3,out,96)==NSA_E_INPUT,"label-sort alias refuses",ctr); 3043 sa_check(nsa_component_boundary_v1(b,n,13411,labels,nv,work,nt*3,b as *i64,96)==NSA_E_INPUT,"source output alias refuses",ctr); 3044 sa_check(nsa_component_boundary_v1(b,n,13411,labels,nv,work,nt*3,out,0)==NSA_E_INPUT,"non-null zero-capacity output refuses",ctr); 3045 sa_put("BOUNDARY-GATE total=");sa_num(ctr[0]);sa_put(" fail=");sa_num(ctr[1]);sa_put("\n");if ctr[1]!=0{return 1};return 0 3046}