code wiki / (root) / nx_nxrun.nx

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}