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) }