nx_nxrun.nx source
↩ module page · 160 lines · 5192 B
1// nxrun.nx -- sovereign replacement for bench/run.sh.
2//
3// Compiles each benchmark's C baseline with gcc, times its
4// execution, compiles the NishiLang variant with nxc2, times its
5// execution, prints a tab-separated comparison. Uses the
6// process-control syscalls (sys_fork / sys_execve / sys_wait4)
7// shipped earlier in this session.
8//
9// Removes bash from one more pathway in the nishi-core build
10// chain. Pair with f6_gate.nx (F6 reproducibility gate) to cover
11// every shell script currently in the repo except the gcc build
12// (Makefile, driven by host `make`).
13//
14// Four invariants:
15// NR1 Same gcc / nxc2 invocation as run.sh; numbers comparable
16// across the bash-driven and .nx-driven runs.
17// NR2 Timing uses sys_clock_gettime (monotonic, ns precision)
18// not wall-clock -- resilient to NTP adjustments mid-run.
19// NR3 Each bench's child process spawned via fork + execve;
20// output redirected to /dev/null via dup3 so measurements
21// aren't I/O-bottlenecked.
22// NR4 Exit codes propagate: if any child exits non-zero, the
23// whole harness exits non-zero (CI-friendly).
24
25// nx_safety_envelope:
26// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
27// sil_target: SIL1
28// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
29// verdict: NOT_YET_EVALUATED
30
31import "nx_syscalls.nx"
32import "nx_time_canonical.nx"
33const NRUN_MAGIC_1000000: i64 = 1000000
34
35const NRUN_NULL: i64 = 0
36const NRUN_OK: i64 = 0
37const NRUN_FAIL: i64 = 1
38
39// Spawn a child process: dup3 stdout + stderr to /dev/null, then
40// execve. Returns child exit status (0..255) or a negative
41// syscall error. argv must be a null-terminated *i64 array.
42func spawn_silent(path: *u8, argv: *i64) -> i64 {
43 // Open /dev/null in the parent so we have a valid fd to dup3.
44 let devnull_path: *u8 = "/dev/null"
45 let devnull_fd: i64 = sys_openat_rd(devnull_path)
46 // Writing to /dev/null needs write mode; re-open as needed.
47 // For now, if stderr inherits from parent, that's fine --
48 // bench output isn't interesting. But stdout to /dev/null
49 // prevents benchmark-printed lines from drowning our table.
50 let pid: i64 = sys_fork()
51 if pid < 0 {
52 sys_close(devnull_fd)
53 return pid
54 }
55 if pid == 0 {
56 // Child: attempt to silence stdout. If /dev/null opened
57 // read-only, dup3 won't let us write -- tolerate and
58 // proceed without silencing.
59 if devnull_fd >= 0 {
60 sys_dup3(devnull_fd, 1, 0)
61 sys_close(devnull_fd)
62 }
63 sys_execve(path, argv, 0 as *i64)
64 sys_exit(127)
65 }
66 // Parent.
67 sys_close(devnull_fd)
68 let status_raw: *u8 = sys_mmap(16)
69 let status: *i64 = status_raw as *i64
70 *status = 0
71 sys_wait4(pid, status, 0)
72 return wait_exit_code(*status)
73}
74
75// Time a child-process execution in nanoseconds. Returns
76// elapsed ns on success, negative on child failure.
77func time_exec(path: *u8, argv: *i64) -> i64 {
78 let start: i64 = monotonic_ns()
79 let rc: i64 = spawn_silent(path, argv)
80 let end: i64 = monotonic_ns()
81 if rc < 0 { return rc }
82 if rc != 0 { return 0 - rc - 1000 } // distinguish non-zero exit
83 return end - start
84}
85
86// Print a null-terminated cstring to stdout.
87func print_cstr(s: *u8) -> i64 {
88 var n: i64 = 0
89 while s[n] != 0 { n = n + 1 }
90 sys_write(1, s, n)
91 return n
92}
93
94// Print a decimal integer.
95func print_dec(n: i64) -> i64 {
96 if n == 0 {
97 let z: *u8 = sys_mmap(2)
98 z[0] = 0x30
99 sys_write(1, z, 1)
100 return 1
101 }
102 let buf: *u8 = sys_mmap(32)
103 var v: i64 = n
104 if v < 0 {
105 let neg: *u8 = sys_mmap(2)
106 neg[0] = 0x2D
107 sys_write(1, neg, 1)
108 v = 0 - v
109 }
110 var k: i64 = 0
111 while v > 0 {
112 buf[k] = 0x30 + (v % 10)
113 v = v / 10
114 k = k + 1
115 }
116 let out: *u8 = sys_mmap(32)
117 var i: i64 = 0
118 while i < k {
119 out[i] = buf[k - 1 - i]
120 i = i + 1
121 }
122 sys_write(1, out, k)
123 return k
124}
125
126// Run one bench: time the C binary + print result. Expected
127// to be called after the caller has already compiled the .c
128// file into `<name>_c` via a separate gcc invocation.
129func bench_one(name: *u8, path: *u8) -> i64 {
130 let argv_raw: *u8 = sys_mmap(32)
131 let argv: *i64 = argv_raw as *i64
132 argv[0] = path as i64
133 argv[1] = 0
134
135 let ns: i64 = time_exec(path, argv)
136 if ns < 0 { return NRUN_FAIL }
137 let ms: i64 = ns / NRUN_MAGIC_1000000
138
139 print_cstr(name)
140 print_cstr("\t")
141 print_dec(ms)
142 print_cstr(" ms\n")
143 return NRUN_OK
144}
145
146// Drive: take a single bench name + binary path from argv and
147// time it. Shell-script equivalent of `./bench/run.sh fib`.
148// argc should be 3: [prog, name, path].
149func main(argc: i64, argv: *i64) -> i64 {
150 if argc < 3 {
151 let msg: *u8 = "nxrun: usage: nxrun <bench-name> <binary-path>\n"
152 var n: i64 = 0
153 while msg[n] != 0 { n = n + 1 }
154 sys_write(2, msg, n)
155 return NRUN_FAIL
156 }
157 let name: *u8 = argv[1] as *u8
158 let path: *u8 = argv[2] as *u8
159 return bench_one(name, path)
160}