code wiki / (root) / nx_itoa_lib.nx

nx_itoa_lib.nx source

↩ module page · 95 lines · 4781 B

1// nx_itoa_lib.nx -- THE shared integer->decimal emitter. ONE copy, so the corpus stops retyping it. 2// 3// LIFTED, NEVER COPIED (2026-07-31, debt 1785557603). ccz_cat_num was already correct, already 4// MSB-first, already zero-allocation, and already had 10+ callers -- it was simply IMPRISONED inside 5// nx_crashresume_census_core.nx, a crash-resume census organ. Seven files imported an entire census 6// just to print an integer. That import cost, NOT ignorance of the primitive, is why ~87 sites 7// hand-rolled their own. LAW: WHEN A CORRECT PRIMITIVE IS RETYPED, MEASURE ITS IMPORT COST BEFORE 8// BLAMING DISCOVERABILITY -- people do not retype what is CHEAP to reach. 9// 10// THE LEAK WAS NEVER IN THE PRIMITIVE, IT WAS IN THE MISSING WRAPPER. ccz_cat_num allocates nothing. 11// What every clone hand-rolled was the fd shim around it, e.g. nx_lock_reap_gate.g_putn: 12// let b: *u8 = sys_mmap(32); let e: i64 = ccz_cat_num(b, 0, v); sys_write(1, b, e); return 0 13// -- one mmap per call, never freed. nxi_fd below is that shim, written ONCE and always freeing. 14// 15// The census now imports THIS file; NishiLang import is transitive (verified: nx_lock_reap_gate 16// imports only nx_syscalls + nx_lock_reap_core, and resolves ccz_cat_num through the core), so all 17// existing callers keep resolving with no edit. 18// 19// LAYERING: lives in runtime/ so BOTH runtime/ and _hdl_build/ can import it. 20// license_tier: ORIGINAL No hw writes (Rule 26). 21import "nx_syscalls.nx" 22// MSB-FIRST (2026-07-31, debt 1785516350): the previous body built digits LEAST-significant first, 23// which comes out BACKWARDS and therefore needed a sys_mmap(32) scratch buffer to reverse through -- 24// and never freed it, leaking a page per call across 12+ importers. Emitting MOST-significant first 25// needs no buffer at all, so this now ALLOCATES NOTHING. Output bytes and the NUL-terminate contract 26// are unchanged; this is a rewrite of the algorithm, not a sprinkled munmap (rule 3). 27const CCZ_ASCII_0: i64 = 48 28const CCZ_MINUS: i64 = 45 29const CCZ_DEC: i64 = 10 30func ccz_cat_num(buf: *u8, off: i64, v: i64) -> i64 { 31 var o: i64 = off 32 var m: i64 = v 33 if m == 0 { buf[o] = CCZ_ASCII_0 as u8; o = o + 1; buf[o] = 0 as u8; return o } 34 if m < 0 { buf[o] = CCZ_MINUS as u8; o = o + 1; m = 0 - m } 35 // i64 MIN negates to itself and stays negative; clamp rather than loop forever on the digit walk. 36 if m < 0 { m = 0 } 37 var pw: i64 = 1 38 while m / pw >= CCZ_DEC { pw = pw * CCZ_DEC } 39 while pw > 0 { 40 buf[o] = (CCZ_ASCII_0 + ((m / pw) % CCZ_DEC)) as u8 41 o = o + 1 42 pw = pw / CCZ_DEC 43 } 44 buf[o] = 0 as u8 45 return o 46} 47 48// max i64 is 19 digits + sign + the NUL ccz_cat_num writes; 24 leaves slack, well under one page. 49const NXI_BUF: i64 = 24 50const NXI_STDOUT: i64 = 1 51const NXI_STDERR: i64 = 2 52 53// Write v as decimal to fd. ONE buffer, ALWAYS freed -- the balanced shape nx_mmapbal certifies. 54// This is the drop-in for every hand-rolled putn/gn/wn/pn clone. Returns bytes written. 55func nxi_fd(fd: i64, v: i64) -> i64 { 56 let b: *u8 = sys_mmap(NXI_BUF) 57 let n: i64 = ccz_cat_num(b, 0, v) 58 sys_write(fd, b, n) 59 sys_munmap(b, NXI_BUF) 60 return n 61} 62 63func nxi_out(v: i64) -> i64 { return nxi_fd(NXI_STDOUT, v) } 64func nxi_err(v: i64) -> i64 { return nxi_fd(NXI_STDERR, v) } 65 66// NUL-FREE buffer form (2026-07-31). ccz_cat_num NUL-terminates -- it writes dst[ret]=0 -- which is 67// right for its own callers but WRONG as a drop-in for the large clone family whose contract is 68// "append digits, touch nothing else, return the new offset". Pointing those at ccz_cat_num would 69// write one byte past the returned offset, and a clone that patches a number into the MIDDLE of an 70// already-built buffer would have the next byte clobbered. nx_office_serve.of_catn alone has 60 71// call sites, none of them audited for that. 72// So the lib carries BOTH contracts explicitly rather than making every migrator guess: 73// ccz_cat_num -> digits + NUL, returns the offset BEFORE the NUL 74// nxi_buf -> digits only, returns the offset AFTER them, ZERO bytes touched beyond 75// Both are MSB-first and allocate NOTHING. Constants are the CCZ_ ones lifted with ccz_cat_num. 76func nxi_buf(dst: *u8, off: i64, v: i64) -> i64 { 77 var p: i64 = off 78 var m: i64 = v 79 if m < 0 { 80 dst[p] = CCZ_MINUS as u8 81 p = p + 1 82 m = 0 - m 83 } 84 // i64 MIN negates to ITSELF and stays negative. Clamp to 0 rather than looping forever or 85 // emitting garbage -- a documented bound, never a silent wrong number. 86 if m < 0 { m = 0 } 87 var pw: i64 = 1 88 while m / pw >= CCZ_DEC { pw = pw * CCZ_DEC } 89 while pw > 0 { 90 dst[p] = (CCZ_ASCII_0 + ((m / pw) % CCZ_DEC)) as u8 91 p = p + 1 92 pw = pw / CCZ_DEC 93 } 94 return p 95}