nx_dwline_e2e_gate.nx source
↩ module page · 351 lines · 16721 B
1// nx_dwline_e2e_gate.nx -- COMPILER -> ASSEMBLER -> ELF -> DECODER, on the real toolchain.
2//
3// nx_dwline_roundtrip_gate proves our DWARF encoder and decoder agree ON A BUFFER. That is a real
4// two-system check but it does NOT prove the shipped pipeline produces such a buffer. This gate
5// drives the actual sovereign toolchain -- nx_cc_sovereign.elf -g, then nxasm_x86_main.elf -- reads
6// the ELF that falls out, and resolves an address through nx_addr2line_lib.
7//
8// GROUND TRUTH IS THE SOURCE FILE, NOT OUR OWN OUTPUT. runtime/nx_probe_dbgtiny.nx defines
9// dbgtiny_a on line 5 and main on line 9. GNU readelf independently resolved 0x400096 -> line 5 and
10// 0x4000AD -> line 9 on this exact probe, so asserting "e_entry resolves to line 9" is checked
11// against an external oracle and against the source, not against a number we made up.
12//
13// THE LOAD-BEARING TOOTH IS THE NEGATIVE CONTROL. Compiling the SAME probe WITHOUT -g must produce
14// an ELF with NO .debug_line at all. Without it, a pipeline that emitted debug info unconditionally
15// would pass every positive tooth here -- and it would silently regress the published "168 B minimal
16// static binary" measurement, which is a MEASURED exceed on /compare/lang. DDR-002 named that
17// tradeoff as its load-bearing risk; this is the check that holds it.
18//
19// license_tier: ORIGINAL expect_exit: 0
20import "syscalls.nx"
21import "nx_elf_read.nx"
22import "nx_addr2line_lib.nx"
23import "nx_deploy_lib.nx"
24import "nx_gate_verdict.nx"
25
26const E2E_CAP: i64 = 4194304
27const E2E_CC: *u8 = "_offc/nx_cc_sovereign.elf" as *u8
28const E2E_ASM: *u8 = "_offc/nxasm_x86_main.elf" as *u8
29const E2E_SRC: *u8 = "runtime/nx_probe_dbgtiny.nx" as *u8
30const E2E_SG: *u8 = "/tmp/nx_dwe2e_g.s" as *u8
31const E2E_EG: *u8 = "/tmp/nx_dwe2e_g.elf" as *u8
32const E2E_SN: *u8 = "/tmp/nx_dwe2e_nog.s" as *u8
33const E2E_EN: *u8 = "/tmp/nx_dwe2e_nog.elf" as *u8
34
35func e2e_streq(a: *u8, b: *u8) -> i64 {
36 var i: i64 = 0
37 while a[i] != (0 as u8) {
38 if a[i] != b[i] { return 0 }
39 i = i + 1
40 }
41 if b[i] != (0 as u8) { return 0 }
42 return 1
43}
44
45// STDOUT-ONLY capture. dep_run_capture dup3s the output fd over BOTH 1 and 2, which is right for
46// showing a tool"s output to a human and WRONG for capturing a compiler"s primary artifact: the
47// compiler correctly writes its "fn=NAME" trace to fd 2 (nx_x86_64_ctx.nx:3166), and merging the
48// streams prepended that trace to the .s, so nxasm rejected it with rc=5. The compiler was never
49// at fault; my capture helper was. A CAPTURE THAT MERGES STDERR CANNOT CAPTURE AN ARTIFACT.
50// Implemented HERE rather than added to nx_deploy_lib on purpose: that lib is one of the 17
51// measured SHADOWED names whose two copies differ, so widening it would edit a file whose other
52// copy is what actually compiles for someone else.
53func e2e_run_out(elf: *u8, args: *i64, nargs: i64, outfile: *u8) -> i64 {
54 let pid: i64 = sys_fork()
55 if pid == 0 {
56 let fd: i64 = sys_openat_wr(outfile, 0x1a4)
57 if fd >= 0 { sys_dup3(fd, 1, 0) }
58 let argv: *i64 = sys_mmap(8 * (nargs + 2)) as *i64
59 let envp: *i64 = sys_mmap(16) as *i64
60 envp[0] = 0
61 argv[0] = elf as i64
62 var i: i64 = 0
63 while i < nargs { argv[i+1] = args[i]; i = i + 1 }
64 argv[nargs+1] = 0
65 sys_execve(elf, argv, envp)
66 sys_exit(127)
67 }
68 let st: *i64 = sys_mmap(16) as *i64
69 sys_wait4(pid, st, 0)
70 if (st[0] % 128) != 0 { return 0 - 1 }
71 return (st[0] >> 8) & 0xff
72}
73
74// substring search, so the tooth asserts the CLI PRINTED the answer rather than merely exiting 0.
75func e2e_contains(hay: *u8, n: i64, needle: *u8) -> i64 {
76 var m: i64 = 0
77 while needle[m] != (0 as u8) { m = m + 1 }
78 if m == 0 { return 1 }
79 var i: i64 = 0
80 while i + m <= n {
81 var j: i64 = 0
82 var ok: i64 = 1
83 while j < m { if hay[i+j] != needle[j] { ok = 0; j = m } else { j = j + 1 } }
84 if ok == 1 { return 1 }
85 i = i + 1
86 }
87 return 0
88}
89// render v as 0x-prefixed lowercase hex into dst, NUL-terminated
90func e2e_hex(dst: *u8, v: i64) -> i64 {
91 dst[0] = 48 as u8
92 dst[1] = 120 as u8
93 var nyb: i64 = 15
94 var started: i64 = 0
95 var p: i64 = 2
96 while nyb >= 0 {
97 let d: i64 = (v >> (nyb * 4)) & 15
98 if d != 0 { started = 1 }
99 if started == 1 {
100 if d < 10 { dst[p] = (48 + d) as u8 } else { dst[p] = (87 + d) as u8 }
101 p = p + 1
102 }
103 nyb = nyb - 1
104 }
105 if started == 0 { dst[p] = 48 as u8; p = p + 1 }
106 dst[p] = 0 as u8
107 return p
108}
109
110func e2e_read(path: *u8, buf: *u8, cap: i64) -> i64 {
111 let fd: i64 = sys_openat_rd(path)
112 if fd < 0 { return 0 - 1 }
113 var tot: i64 = 0
114 while tot < cap {
115 let n: i64 = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot)
116 if n <= 0 { break }
117 tot = tot + n
118 }
119 sys_close(fd)
120 return tot
121}
122
123// Returns .debug_line SIZE (0 if absent) and writes its file offset to box[0].
124func e2e_debug_line(buf: *u8, len: i64, box: *i64) -> i64 {
125 let h: *NxElfHeader = nx_elf_parse_header(buf, len)
126 if h.valid != 1 { return 0 }
127 if h.e_shnum <= 0 { return 0 }
128 let shstr: *NxElfShdr = nx_elf_read_shdr(buf, h, h.e_shstrndx)
129 var i: i64 = 0
130 while i < h.e_shnum {
131 let sh: *NxElfShdr = nx_elf_read_shdr(buf, h, i)
132 let nm: *u8 = nx_elf_strtab_at(buf, shstr.sh_offset, sh.sh_name)
133 if e2e_streq(".debug_line" as *u8, nm) == 1 {
134 box[0] = sh.sh_offset
135 return sh.sh_size
136 }
137 i = i + 1
138 }
139 return 0
140}
141
142func main() -> i64 {
143 let ctr: *i64 = gv_ctr()
144 gv_head("=== NX-DWLINE-E2E GATE -- sovereign compiler + assembler produce readable DWARF ===" as *u8)
145
146 // /api/gate_run executes from the nishihost root, but every toolchain path here is relative to
147 // buildroot. Without this chdir the gate would find nothing and report a VACUOUS pass.
148 sys_chdir("buildroot" as *u8)
149
150 let args: *i64 = sys_mmap(64) as *i64
151
152 // ---- with -g ----
153 // The compiler emits assembly on STDOUT and takes exactly ONE non-flag path (nx_compile_x86.nx
154 // L104-L129: first arg not starting with - is the source, the rest are ignored). My first
155 // version passed an output path as argv[3]; it was silently DISCARDED and the .s never existed,
156 // so every downstream tooth failed for a reason that had nothing to do with DWARF.
157 // AN IGNORED ARGUMENT IS NOT AN ERROR MESSAGE -- READ THE CLI, DO NOT INFER IT FROM ITS NAME.
158 args[0] = "-g" as i64
159 args[1] = E2E_SRC as i64
160 let rc_cc_g: i64 = e2e_run_out(E2E_CC, args, 2, E2E_SG)
161 gv_check("compile-with-g-exits-0" as *u8, (rc_cc_g == 0) as i64, ctr)
162 // The capture must contain the directives, else the .s is not what we think it is.
163 let sbuf: *u8 = sys_mmap(E2E_CAP)
164 let sn: i64 = e2e_read(E2E_SG, sbuf, E2E_CAP)
165 gv_check("captured-s-is-nonempty" as *u8, (sn > 256) as i64, ctr)
166 // The .s must be PURE assembly: byte 0 is the generator comment, not a stderr trace.
167 gv_check("captured-s-is-not-polluted-by-stderr" as *u8, (sbuf[0] == (35 as u8)) as i64, ctr)
168
169 args[0] = E2E_SG as i64
170 args[1] = E2E_EG as i64
171 let rc_as_g: i64 = dep_run(E2E_ASM, args, 2)
172 gv_check("assemble-with-g-exits-0" as *u8, (rc_as_g == 0) as i64, ctr)
173
174 let gbuf: *u8 = sys_mmap(E2E_CAP)
175 let gn: i64 = e2e_read(E2E_EG, gbuf, E2E_CAP)
176 gv_check("debug-elf-is-readable" as *u8, (gn > 64) as i64, ctr)
177
178 let gbox: *i64 = sys_mmap(32) as *i64
179 let gsz: i64 = e2e_debug_line(gbuf, gn, gbox)
180 gv_check("debug-elf-carries-debug-line" as *u8, (gsz > 0) as i64, ctr)
181
182 // ---- without -g: the OPT-IN control ----
183 args[0] = E2E_SRC as i64
184 let rc_cc_n: i64 = e2e_run_out(E2E_CC, args, 1, E2E_SN)
185 gv_check("compile-without-g-exits-0" as *u8, (rc_cc_n == 0) as i64, ctr)
186
187 args[0] = E2E_SN as i64
188 args[1] = E2E_EN as i64
189 let rc_as_n: i64 = dep_run(E2E_ASM, args, 2)
190 gv_check("assemble-without-g-exits-0" as *u8, (rc_as_n == 0) as i64, ctr)
191
192 let nbuf: *u8 = sys_mmap(E2E_CAP)
193 let nn: i64 = e2e_read(E2E_EN, nbuf, E2E_CAP)
194 let nbox: *i64 = sys_mmap(32) as *i64
195 let nsz: i64 = e2e_debug_line(nbuf, nn, nbox)
196
197 // LOAD-BEARING: debug info must be OPT-IN. A toolchain emitting it unconditionally passes every
198 // positive tooth above and regresses the published minimal-binary number.
199 gv_check("neg-control-no-g-means-no-debug-line" as *u8, (nsz == 0) as i64, ctr)
200
201 // And the -g build must actually be LARGER, which is the cost side of that tradeoff being real.
202 gv_check("g-build-is-larger-than-plain" as *u8, (gn > nn) as i64, ctr)
203
204 // ---- resolve through the section ----
205 let h: *NxElfHeader = nx_elf_parse_header(gbuf, gn)
206 let sect: *u8 = ((gbuf as i64) + gbox[0]) as *u8
207 let re: *NxA2LResult = nx_a2l_run_section(sect, gsz, h.e_entry)
208
209 // CORRECTED EXPECTATION, 2026-08-07. I first asserted e_entry resolves to line 9, assuming the
210 // entry point was main(). IT IS NOT: e_entry is _start, the compiler-emitted stub that sets up
211 // argc/argv and calls main, and it carries NO .loc because it corresponds to no source line.
212 // found=0 there is the HONEST answer and the decoder was right; the tooth was wrong. Asserting
213 // it deliberately, because "the entry stub has no source location" is a real property worth
214 // locking in -- inventing a line for compiler-generated code is exactly the failure mode that
215 // sends a debugger to a line the programmer never wrote.
216 gv_check("entry-stub-_start-has-no-source-line" as *u8, (re.found == 0) as i64, ctr)
217
218 // Both real source lines must be REACHABLE from real addresses. Scanning the text range and
219 // collecting the distinct lines proves the table covers the program, without hardcoding an
220 // address whose value is a codegen detail that may legitimately move.
221 var saw5: i64 = 0
222 var saw9: i64 = 0
223 var sa: i64 = h.e_entry
224 let send: i64 = h.e_entry + 512
225 while sa < send {
226 let rs: *NxA2LResult = nx_a2l_run_section(sect, gsz, sa)
227 if rs.found == 1 {
228 if rs.line == 5 { saw5 = 1 }
229 if rs.line == 9 { saw9 = 1 }
230 }
231 sa = sa + 1
232 }
233 gv_check("scan-finds-source-line-5-dbgtiny_a" as *u8, (saw5 == 1) as i64, ctr)
234 gv_check("scan-finds-source-line-9-main" as *u8, (saw9 == 1) as i64, ctr)
235
236 // NEGATIVE CONTROL: address 0 precedes every row and must not resolve.
237 let rz: *NxA2LResult = nx_a2l_run_section(sect, gsz, 0)
238 gv_check("neg-control-addr-zero-unresolved" as *u8, (rz.found == 0) as i64, ctr)
239
240 // ---- THE SHIPPED CLI, not just the library underneath it ----------------------------------
241 // nx_addr2line was a self-test wearing a tool name. Proving the LIB resolves addresses says
242 // nothing about whether the BINARY someone runs does: it has its own arg parsing, its own file
243 // read, and its OWN section finder duplicated from this gate. A FIX ONLY THE GATE EXERCISES IS
244 // NOT A SHIPPED FIX -- so drive the promoted artifact end to end.
245 // The address is COMPUTED, never hardcoded: scan for the first row reporting line 5, so a
246 // legitimate codegen shift moves the tooth with it instead of falsely failing.
247 var a5: i64 = 0
248 var sb: i64 = h.e_entry
249 while sb < send {
250 if a5 == 0 {
251 let rr: *NxA2LResult = nx_a2l_run_section(sect, gsz, sb)
252 if rr.found == 1 { if rr.line == 5 { a5 = rr.addr } }
253 }
254 sb = sb + 1
255 }
256 gv_check("scan-recovered-a-line-5-address" as *u8, (a5 > 0) as i64, ctr)
257
258 let hexs: *u8 = sys_mmap(64)
259 e2e_hex(hexs, a5)
260 let cargs: *i64 = sys_mmap(64) as *i64
261 cargs[0] = E2E_EG as i64
262 cargs[1] = hexs as i64
263 let rc_cli: i64 = e2e_run_out("../nx_addr2line.elf" as *u8, cargs, 2, "/tmp/nx_dwe2e_cli.txt" as *u8)
264 gv_check("cli-nx_addr2line-exits-0" as *u8, (rc_cli == 0) as i64, ctr)
265
266 let cbuf: *u8 = sys_mmap(65536)
267 let cn: i64 = e2e_read("/tmp/nx_dwe2e_cli.txt" as *u8, cbuf, 65536)
268 gv_check("cli-reports-line-5" as *u8, ((cn > 0) & (e2e_contains(cbuf, cn, "line=5" as *u8) == 1)) as i64, ctr)
269
270 // NEGATIVE CONTROL on the CLI: the no-g ELF has no .debug_line, so it must REFUSE (rc=4) rather
271 // than print a location. A tool that invents a source line for an unstripped-but-undebugged
272 // binary is worse than one that has no debug support at all.
273 cargs[0] = E2E_EN as i64
274 cargs[1] = hexs as i64
275 let rc_cli_n: i64 = e2e_run_out("../nx_addr2line.elf" as *u8, cargs, 2, "/tmp/nx_dwe2e_cli_n.txt" as *u8)
276 gv_check("neg-control-cli-refuses-elf-without-debug-line" as *u8, (rc_cli_n == 4) as i64, ctr)
277
278 // ---- DDR-009 step 3: does the CLI NAME THE FUNCTION, and the RIGHT one? ---------------
279 // A tool that always answered "dbgtiny_a" would pass a single-address check. The load-bearing
280 // part is that two DIFFERENT addresses yield two DIFFERENT, CORRECT names -- discrimination,
281 // not recall. And the no-debug ELF must still REFUSE rather than invent a name.
282 var a9: i64 = 0
283 var sc2: i64 = h.e_entry
284 while sc2 < send {
285 if a9 == 0 {
286 let r9: *NxA2LResult = nx_a2l_run_section(sect, gsz, sc2)
287 if r9.found == 1 { if r9.line == 9 { a9 = r9.addr } }
288 }
289 sc2 = sc2 + 1
290 }
291 // SUBJECT CORRECTION. Everything above deliberately exercises the BLESSED toolchain
292 // (_offc/nxasm_x86_main.elf), which predates .debug_info -- so the CLI correctly printed "??"
293 // there: an honest "no function info", not a failure. Function NAMING is a property of the NEW
294 // assembler, so it needs an ELF built BY that assembler. Testing it against the blessed output
295 // would have been the instrument-subject mismatch this estate keeps paying for.
296 let nargs2: *i64 = sys_mmap(64) as *i64
297 nargs2[0] = E2E_SG as i64
298 nargs2[1] = "/tmp/nx_dwe2e_g_new.elf" as i64
299 let rcn2: i64 = e2e_run_out("../nxasm_x86_main.elf" as *u8, nargs2, 2, "/tmp/nx_dwe2e_asm2.log" as *u8)
300 gv_check("new-assembler-produced-a-debug_info-elf" as *u8, (rcn2 == 0) as i64, ctr)
301
302 let hex9: *u8 = sys_mmap(64)
303 e2e_hex(hex9, a9)
304 cargs[0] = "/tmp/nx_dwe2e_g_new.elf" as i64
305 cargs[1] = hex9 as i64
306 let rc9: i64 = e2e_run_out("../nx_addr2line.elf" as *u8, cargs, 2, "/tmp/nx_dwe2e_fn9.txt" as *u8)
307 let fb: *u8 = sys_mmap(65536)
308 let fn9: i64 = e2e_read("/tmp/nx_dwe2e_fn9.txt" as *u8, fb, 65536)
309 gv_check("cli-names-main-at-the-line-9-address" as *u8,
310 ((rc9 == 0) & (e2e_contains(fb, fn9, "main" as *u8) == 1)) as i64, ctr)
311
312 // the line-5 address must name the OTHER function -- this is the discrimination tooth.
313 // Re-run against the SAME new-assembler ELF so both names come from one subject.
314 cargs[1] = hexs as i64
315 let rc5b: i64 = e2e_run_out("../nx_addr2line.elf" as *u8, cargs, 2, "/tmp/nx_dwe2e_fn5.txt" as *u8)
316 let gb: *u8 = sys_mmap(65536)
317 let fn5: i64 = e2e_read("/tmp/nx_dwe2e_fn5.txt" as *u8, gb, 65536)
318 gv_check("cli-names-dbgtiny_a-at-the-line-5-address" as *u8,
319 (e2e_contains(gb, fn5, "dbgtiny_a" as *u8) == 1) as i64, ctr)
320
321 gv_puts("\n --- diagnostics ---\n" as *u8)
322 gv_puts(" line5_addr=" as *u8); gv_num(a5)
323 gv_puts(" cli_rc=" as *u8); gv_num(rc_cli)
324 gv_puts(" cli_rc_nog=" as *u8); gv_num(rc_cli_n)
325 gv_puts(" cli_out=" as *u8)
326 var ci: i64 = 0
327 while ci < cn { if cbuf[ci] != (10 as u8) { sys_write(1, ((cbuf as i64) + ci) as *u8, 1) } ci = ci + 1 }
328 gv_puts("\n" as *u8)
329
330 gv_puts(" rc_cc_g=" as *u8); gv_num(rc_cc_g)
331 gv_puts(" rc_as_g=" as *u8); gv_num(rc_as_g)
332 gv_puts(" rc_cc_n=" as *u8); gv_num(rc_cc_n)
333 gv_puts(" rc_as_n=" as *u8); gv_num(rc_as_n)
334 gv_puts(" s_bytes=" as *u8); gv_num(sn); gv_puts("\n" as *u8)
335 gv_puts(" first 120 bytes of the captured .s ->|" as *u8)
336 var di: i64 = 0
337 while di < 120 { if di < sn { sys_write(1, ((sbuf as i64) + di) as *u8, 1) } di = di + 1 }
338 gv_puts("|<-\n" as *u8)
339
340 gv_puts("\n --- measured ---\n" as *u8)
341 gv_puts(" g_elf_bytes=" as *u8); gv_num(gn)
342 gv_puts(" nog_elf_bytes=" as *u8); gv_num(nn)
343 gv_puts(" debug_line_bytes=" as *u8); gv_num(gsz); gv_puts("\n" as *u8)
344 gv_puts(" e_entry=" as *u8); gv_num(h.e_entry)
345 gv_puts(" -> found=" as *u8); gv_num(re.found)
346 gv_puts(" line=" as *u8); gv_num(re.line)
347 gv_puts(" row_addr=" as *u8); gv_num(re.addr); gv_puts("\n" as *u8)
348
349 return gv_verdict("nx_dwline_e2e_gate" as *u8, ctr,
350 "real nx_cc_sovereign + nxasm_x86_main output, decoded by nx_addr2line_lib; line 9 corroborated by source and by GNU readelf" as *u8)
351}