code wiki / (root) / nx_itoa_candidate_t297.nx

nx_itoa_candidate_t297.nx source

↩ module page · 77 lines · 4104 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 let end:i64=nxi_buf(buf,off,v) 32 buf[end]=0 as u8 33 return end 34} 35 36// max i64 is 19 digits + sign + the NUL ccz_cat_num writes; 24 leaves slack, well under one page. 37const NXI_BUF: i64 = 24 38const NXI_STDOUT: i64 = 1 39const NXI_STDERR: i64 = 2 40 41// Write v as decimal to fd. ONE buffer, ALWAYS freed -- the balanced shape nx_mmapbal certifies. 42// This is the drop-in for every hand-rolled putn/gn/wn/pn clone. Returns bytes written. 43func nxi_fd(fd: i64, v: i64) -> i64 { 44 let b: *u8 = sys_mmap(NXI_BUF) 45 let n: i64 = ccz_cat_num(b, 0, v) 46 sys_write(fd, b, n) 47 sys_munmap(b, NXI_BUF) 48 return n 49} 50 51func nxi_out(v: i64) -> i64 { return nxi_fd(NXI_STDOUT, v) } 52func nxi_err(v: i64) -> i64 { return nxi_fd(NXI_STDERR, v) } 53 54// NUL-FREE buffer form (2026-07-31). ccz_cat_num NUL-terminates -- it writes dst[ret]=0 -- which is 55// right for its own callers but WRONG as a drop-in for the large clone family whose contract is 56// "append digits, touch nothing else, return the new offset". Pointing those at ccz_cat_num would 57// write one byte past the returned offset, and a clone that patches a number into the MIDDLE of an 58// already-built buffer would have the next byte clobbered. nx_office_serve.of_catn alone has 60 59// call sites, none of them audited for that. 60// So the lib carries BOTH contracts explicitly rather than making every migrator guess: 61// ccz_cat_num -> digits + NUL, returns the offset BEFORE the NUL 62// nxi_buf -> digits only, returns the offset AFTER them, ZERO bytes touched beyond 63// Both are MSB-first and allocate NOTHING. Constants are the CCZ_ ones lifted with ccz_cat_num. 64func nxi_buf(dst: *u8, off: i64, v: i64) -> i64 { 65 var p:i64=off 66 var m:i64=v 67 if m<0{dst[p]=CCZ_MINUS as u8;p=p+1} 68 // A nonpositive magnitude represents every signed i64, including MIN. 69 if m>0{m=0-m} 70 var pw:i64=1 71 while m/pw<=(0-CCZ_DEC){pw=pw*CCZ_DEC} 72 while pw>0{ 73 dst[p]=(CCZ_ASCII_0-((m/pw)%CCZ_DEC)) as u8 74 p=p+1;pw=pw/CCZ_DEC 75 } 76 return p 77}