code wiki / (root) / nx_ccgate_lib.nx

nx_ccgate_lib.nx source

↩ module page · 244 lines · 10294 B

1// nx_ccgate_lib.nx -- THE ONE compiler-gate driver: fork/exec a toolchain compiler on a fixture with 2// an optional mode flag, assemble with nxasm, run the fixture by phase, read back stderr captures. 3// Extracted 2026-08-23 from the LN3 gate (nx_boundscheck_gate, whose helpers it reproduces verbatim 4// in behaviour) the day a SECOND and THIRD compiler-mode gate needed them (nx_chkarith_gate for LN1, 5// nx_opt_eqsat_wire_gate for LN8): three private copies of one driver is the duplicate-ruler defect, 6// and a fix to the driver (e.g. the noexec-/tmp placement lesson) must land once. 7// 8// CONVENTIONS CARRIED (all measured in the LN3 gate's record): 9// * data captures (.s, .err) live in /tmp/<gate>/; every ELF THAT MUST RUN lives under _build/ -- 10// the NAS mounts /tmp noexec (execve EACCES 126), so a runnable staged in /tmp passes its compile 11// teeth and fails every run tooth with no diagnostic; 12// * per-run-unique names carry the gate's pid (two concurrent runs racing one rename source is a 13// documented incident class); 14// * ccg_build returns phase codes (0 ok, 1 compile-fail, 2 asm-fail, 3 rename-fail) that the gate 15// PRINTS before its teeth -- a FAIL-only tooth cannot say why; 16// * ccg_anchor_root chdirs into buildroot when the tools-API exec lane starts the gate in the 17// serving root (whose _offc carries no compiler); the laptop lane misses the probe and stays put. 18// license_tier: ORIGINAL No hw writes (Rule 26). 19import "nx_syscalls.nx" 20import "nx_gate_verdict.nx" 21 22const CCG_PATH_CAP: i64 = 128 // per-run artifact path buffer; every path here is < 64 bytes 23const CCG_MODE_RW: i64 = 0x1a4 // 0644 capture files 24const CCG_MODE_X: i64 = 0x1ed // 0755 runnables and the /tmp/<gate> dir 25 26// fork + redirects + execve; parent waits; returns RAW wait status (the equiv-gate shape). 27func ccg_run(path: *u8, argv: *i64, redir_out: i64, redir_err: i64) -> i64 { 28 let pid: i64 = sys_fork() 29 if pid == 0 { 30 if redir_out >= 0 { sys_dup3(redir_out, 1, 0) } 31 if redir_err >= 0 { sys_dup3(redir_err, 2, 0) } 32 let envp: *i64 = sys_mmap(16) as *i64 33 envp[0] = 0 34 sys_execve(path, argv, envp) 35 sys_exit(127) 36 } 37 let st: *i64 = sys_mmap(16) as *i64 38 sys_wait4(pid, st, 0) 39 return st[0] 40} 41 42// <pfx><pid><sfx> into a fresh buffer (NUL-terminated). Per-run-unique paths. 43func ccg_path(pfx: *u8, pid: i64, sfx: *u8) -> *u8 { 44 let dst: *u8 = sys_mmap(CCG_PATH_CAP) 45 var o: i64 = 0 46 while pfx[o] != (0 as u8) { dst[o] = pfx[o]; o = o + 1 } 47 let t: *u8 = sys_mmap(24) 48 var m: i64 = pid 49 var k: i64 = 0 50 if m <= 0 { t[0] = 48 as u8; k = 1 } 51 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 52 var i: i64 = 0 53 while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 } 54 o = o + k 55 var j: i64 = 0 56 while sfx[j] != (0 as u8) { dst[o + j] = sfx[j]; j = j + 1 } 57 dst[o + j] = 0 as u8 58 return dst 59} 60 61// Compile src with cc (flag: 0 = no flag, else the given flag string is argv[1]), stderr CAPTURED to 62// err_out, then nxasm .s -> elf_out via the per-run asm temp. 0 ok, 1 compile-fail, 2 asm-fail, 63// 3 rename-fail. One build helper for every A/B pair: no second ruler. 64// COMPILE ONLY -- no assembly, no rename. Returns the compiler's RAW wait status, so a caller can 65// test `!= 0` exactly as ccg_build always has, or read the real exit code with wait_exit_code(). 66// EXTRACTED 2026-08-25 for LR5 (nx_langspec): a conformance runner has to assert the compiler's OWN 67// exit status on a refusal (the spec clause says "exits with status two"), and ccg_build collapses 68// every compile failure to 1 -- so the status was unreadable through the only driver in the estate. 69// It also removes the assembly step from ACCEPT and REFUSE cases, which never needed a runnable. 70// ccg_build now COMPOSES this rather than carrying a second copy of the argv build: there is still 71// exactly ONE place in the estate that spawns the compiler, and the refactor is behaviour-identical 72// (ccg_build's `if st != 0 { return 1 }` reads the same raw status it always did). 73func ccg_compile(cc: *u8, flag: *u8, src: *u8, s_tmp: *u8, err_out: *u8) -> i64 { 74 let a: *i64 = sys_mmap(8 * 4) as *i64 75 var n: i64 = 0 76 a[n] = cc as i64; n = n + 1 77 if (flag as i64) != 0 { a[n] = flag as i64; n = n + 1 } 78 a[n] = src as i64; n = n + 1 79 a[n] = 0 80 let sfd: i64 = sys_openat_wr(s_tmp, CCG_MODE_RW) 81 let efd: i64 = sys_openat_wr(err_out, CCG_MODE_RW) 82 let st: i64 = ccg_run(cc, a, sfd, efd) 83 sys_close(sfd) 84 sys_close(efd) 85 return st 86} 87 88func ccg_build(cc: *u8, flag: *u8, src: *u8, s_tmp: *u8, elf_out: *u8, err_out: *u8, asm_tmp: *u8) -> i64 { 89 let st: i64 = ccg_compile(cc, flag, src, s_tmp, err_out) 90 if st != 0 { return 1 } 91 let nxasm: *u8 = "_offc/nxasm_x86_main.elf\x00" 92 let a2: *i64 = sys_mmap(8 * 4) as *i64 93 a2[0] = nxasm as i64 94 a2[1] = s_tmp as i64 95 a2[2] = asm_tmp as i64 96 a2[3] = 0 97 if ccg_run(nxasm, a2, 0 - 1, 0 - 1) != 0 { return 2 } 98 if sys_renameat(asm_tmp, elf_out) != 0 { return 3 } 99 nx_chmod(elf_out, CCG_MODE_X) 100 return 0 101} 102 103// Run a built fixture with one phase argument; returns the child's EXIT CODE (not raw status). A 104// signal death does not read as any small positive code, so a crash cannot wear a trap's clothes. 105func ccg_phase(elf: *u8, phase: *u8) -> i64 { 106 let a: *i64 = sys_mmap(8 * 3) as *i64 107 a[0] = elf as i64 108 a[1] = phase as i64 109 a[2] = 0 110 let st: i64 = ccg_run(elf, a, 0 - 1, 0 - 1) 111 return wait_status_rc(st) 112} 113 114// Run a built fixture with one phase argument, CAPTURING its stdout and stderr to files. Sibling of 115// ccg_phase, which lets the child's streams fall through to the caller's -- harmless for a gate whose 116// own output is a verdict vector, WRONG for a runner whose stdout is an anchored machine-readable 117// report. MEASURED 2026-08-25 on nx_langspec's first run: a trapping fixture's multi-line FATAL text 118// landed in the MIDDLE of the report, two rows above the case that produced it. 119// ****A CHILD'S STDERR INHERITED INTO A MACHINE-READABLE REPORT IS UNTRUSTED TEXT INSIDE YOUR OWN 120// OUTPUT FORMAT. Returns the child's EXIT CODE, exactly as ccg_phase does. 121func ccg_phase_to(elf: *u8, phase: *u8, out_path: *u8, err_path: *u8) -> i64 { 122 let a: *i64 = sys_mmap(8 * 3) as *i64 123 a[0] = elf as i64 124 a[1] = phase as i64 125 a[2] = 0 126 let ofd: i64 = sys_openat_wr(out_path, CCG_MODE_RW) 127 let efd: i64 = sys_openat_wr(err_path, CCG_MODE_RW) 128 let st: i64 = ccg_run(elf, a, ofd, efd) 129 sys_close(ofd) 130 sys_close(efd) 131 return wait_status_rc(st) 132} 133 134// Does file at path contain needle? 1 yes, 0 no/unreadable. Auto-sizing reader; explicit go-flag 135// loops -- never a sentinel written into the cursor (nx_srclint class). 136func ccg_file_has(path: *u8, needle: *u8) -> i64 { 137 let szp: *i64 = sys_mmap(16) as *i64 138 let b: *u8 = sys_read_file(path, szp) 139 if (b as i64) == 0 { return 0 } 140 let n: i64 = szp[0] 141 var nl: i64 = 0 142 while needle[nl] != (0 as u8) { nl = nl + 1 } 143 if nl == 0 { return 0 } 144 var i: i64 = 0 145 var found: i64 = 0 146 var go: i64 = 1 147 var j: i64 = 0 148 var ok: i64 = 0 149 var jg: i64 = 0 150 while go == 1 { 151 if i + nl > n { go = 0 } else { 152 j = 0 153 ok = 1 154 jg = 1 155 while jg == 1 { 156 if j >= nl { jg = 0 } else { 157 if b[i + j] != needle[j] { ok = 0; jg = 0 } 158 j = j + 1 159 } 160 } 161 if ok == 1 { found = 1; go = 0 } 162 i = i + 1 163 } 164 } 165 return found 166} 167 168// Count the lines of `path` that contain BOTH needles (a line-anchored two-marker scan). -1 if the 169// file is unreadable, so a gate can tell "zero matches" from "could not look". 170func ccg_file_count_lines_with2(path: *u8, n1: *u8, n2: *u8) -> i64 { 171 let szp: *i64 = sys_mmap(16) as *i64 172 let b: *u8 = sys_read_file(path, szp) 173 if (b as i64) == 0 { return 0 - 1 } 174 let n: i64 = szp[0] 175 var l1: i64 = 0 176 while n1[l1] != (0 as u8) { l1 = l1 + 1 } 177 var l2: i64 = 0 178 while n2[l2] != (0 as u8) { l2 = l2 + 1 } 179 var count: i64 = 0 180 var ls: i64 = 0 // line start 181 var i: i64 = 0 182 var go: i64 = 1 183 while go == 1 { 184 var at_end: i64 = 0 185 if i >= n { at_end = 1 } 186 var is_nl: i64 = 0 187 if at_end == 0 { if b[i] == (10 as u8) { is_nl = 1 } } 188 if at_end == 1 { is_nl = 1 } 189 if is_nl == 1 { 190 if ccg_mem_has(b, ls, i, n1, l1) == 1 { 191 if ccg_mem_has(b, ls, i, n2, l2) == 1 { count = count + 1 } 192 } 193 ls = i + 1 194 if at_end == 1 { go = 0 } 195 } 196 i = i + 1 197 } 198 return count 199} 200 201// Does b[lo, hi) contain needle[0..nl)? Explicit go-flags; the cursor is never the sentinel. 202func ccg_mem_has(b: *u8, lo: i64, hi: i64, needle: *u8, nl: i64) -> i64 { 203 if nl == 0 { return 0 } 204 var p: i64 = lo 205 var found: i64 = 0 206 var go: i64 = 1 207 while go == 1 { 208 if p + nl > hi { go = 0 } else { 209 var q: i64 = 0 210 var ok: i64 = 1 211 var qg: i64 = 1 212 while qg == 1 { 213 if q >= nl { qg = 0 } else { 214 if b[p + q] != needle[q] { ok = 0; qg = 0 } 215 q = q + 1 216 } 217 } 218 if ok == 1 { found = 1; go = 0 } 219 p = p + 1 220 } 221 } 222 return found 223} 224 225func ccg_val(name: *u8, v: i64) -> i64 { 226 gv_puts(" " as *u8) 227 gv_puts(name) 228 gv_puts("=" as *u8) 229 gv_num(v) 230 gv_puts("\n" as *u8) 231 return 0 232} 233 234// CWD ANCHOR: the tools-API exec lane starts organs in the SERVING ROOT; the builder and the source 235// tree live under buildroot/. Keys on THE BUILDER ITSELF (the laptop tree also carries a 236// buildroot/runtime mirror but no buildroot/_offc compiler, so it correctly stays put). 237func ccg_anchor_root() -> i64 { 238 let bfd: i64 = sys_openat_rd("buildroot/_offc/nx_cc_sovereign.elf\x00" as *u8) 239 if bfd >= 0 { sys_close(bfd); sys_chdir("buildroot\x00" as *u8); return 1 } 240 return 0 241} 242 243// rv64 getpid through the translated const path (the equiv-gate idiom). 244func ccg_pid() -> i64 { return __syscall(172, 0, 0, 0, 0, 0, 0) }