code wiki / (root) / nx_nxc.nx

nx_nxc.nx source

↩ module page · 303 lines · 13173 B

1// nxc.nx -- the full NishiLang sovereign-compiler driver. 2// 3// Chains every piece of our stack in pure NishiLang: 4// 5// source (.nx) -> import-expand -> lex -> parse -> opt 6// -> regalloc -> riscv -> nxasm (assemble) 7// -> elf_writer (wrap) -> sys_write(stdout) 8// 9// Output is a self-contained RV64 Linux ELF executable. Zero third- 10// party code on the program path: no gcc, no binutils `as`, no ld, 11// no libc, no external crt0. The only external thing is the Linux 12// kernel ABI, which NishiOS replaces 1:1. 13// 14// CLI: `nxc <path>.nx` reads the file, runs the import expander to 15// inline `import \"...\"` directives into a single source blob, then 16// drives the compile pipeline and writes the ELF to stdout. Pipe 17// to a file + chmod +x to run: 18// nxc hello.nx > hello && chmod +x hello && ./hello 19// 20// With no args it compiles a baked-in demo source so `./nxc` still 21// exits successfully for smoke-testing the chain. 22// 23// Status (2026-04-22): parse.nx has full monomorphization for 24// generic struct + enum; the earlier \"syntax accepted but no 25// substitution\" gap is closed. Outstanding work to fully retire 26// gcc from the compiler bootstrap is QEMU/real-RV64 verification of 27// the produced ELF (Phase 5 roadmap). 28 29// nx_safety_envelope: 30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 31// sil_target: SIL1 32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 33// verdict: NOT_YET_EVALUATED 34 35import "nx_syscalls.nx" 36import "nx_types.nx" 37import "nx_lex_kinds.nx" 38import "nx_outbuf.nx" 39import "nx_ir.nx" 40import "nx_ir_validate.nx" 41import "nx_tokenizer.nx" 42import "nx_parse.nx" 43import "nx_opt.nx" 44import "nx_regalloc.nx" 45import "nx_riscv.nx" 46import "crt0.nx" 47import "nx_import.nx" 48import "nxasm_v2.nx" 49import "elf_writer.nx" 50 51// Drive: source text -> ELF bytes written to `fd`. 52// 53// Buffer sizes calibrated against nxc.nx-compiling-itself (the 54// Thompson-defense bootstrap target). Source ~6KLOC NishiLang 55// expands to ~85K lines of asm + ~340K bytes of code + ~250K 56// tokens. Generous headroom to handle real programs without 57// overflow: 58// 59// tokens: 256K (was 4K) -- Source <= ~25K LOC sustainable 60// asm_out: 16 MB (was 128K) -- compiler-class outputs fit 61// code_buf: 4 MB (was 64K) -- 1M instructions max 62// 63// All allocated via sys_mmap which is page-aligned + lazy-faulted, 64// so unused pages cost ~zero RSS. 65func nxc_compile(src: *u8, fd: i64) -> i64 { 66 // Stage 1: lex source into tokens. 256K-token cap covers 67 // any program up to ~25K source lines. 68 sys_write(2, "nxc: lex start\n" as *u8, 15) 69 let toks: *Tok = lex_source(src, 262144) 70 if toks == (0 as *Tok) { return 1 } 71 sys_write(2, "nxc: lex done\n" as *u8, 14) 72 73 // Stage 2: parse into a Module. 74 let m: *Module = parse_module(toks, 0 as *Module) 75 if m == (0 as *Module) { return 2 } 76 sys_write(2, "nxc: parse done\n" as *u8, 16) 77 78 // Stage 2.4: structural CFG validator on the post-parse IR. 79 // Catches V1 (compaction skew), V2 (BR/BR_COND target out of 80 // range), V3 (succ ptr disagrees with op id) before any opt 81 // pass runs. Reports per-violation to stderr; non-zero 82 // total just logs (doesn't abort) until we're confident the 83 // validator itself is bug-free. 84 let v_post_parse: i64 = ir_validate_module(m, "post-parse" as *u8) 85 if v_post_parse > 0 { 86 sys_write(2, "nxc: post-parse validator found violations (see above)\n" as *u8, 55) 87 } 88 89 // Stage 2.5: module-level cross-function inlining (F4 pass). 90 // Walks every call site; tiny pure callees get inlined into 91 // their callers. Defined in opt.nx since 188; previously 92 // unwired, now active in the sovereign self-host pipeline. 93 opt_inline_module(m) 94 95 // Stage 3: opt + regalloc + riscv emit for every function. 96 // Prepend the sovereign _start stub so the ELF entry point 97 // lands on valid code that forwards argc/argv to main + exits. 98 // 99 // Progress markers on stderr: one '.' per 10 functions compiled 100 // so bootstrap_proof.sh can see liveness during a slow qemu run. 101 // Suppressed when NXC_QUIET is set (not yet wired; future flag). 102 sys_write(2, "nxc: lex+parse done\n" as *u8, 20) 103 let asm_out: *OutBuf = out_new(16777216) // 16 MB 104 emit_crt0_start(asm_out) 105 var fi: i64 = 0 106 while fi < m.n_functions { 107 let fn_base: i64 = m.functions as i64 108 let f: *Function = (fn_base + fi * 176) as *Function 109 let name_addr: i64 = f.name_start 110 let fn_name: *u8 = name_addr as *u8 111 opt_run(f) 112 // STUB(validator, T#validator-001): post-opt validator 113 // intentionally disabled while opt_sweep SWEEP3 leaves 114 // succ pointers stale (T#opt-002). The validator would 115 // correctly fire V3 on every reachable function, drowning 116 // real-bug signal. Re-enable in tandem with T#opt-002 117 // closing. 118 // Plan: re-add this call once SWEEP3 either rebuilds 119 // succs cleanly OR all succ-walking passes have moved 120 // to BR-op walking (matching opt_sweep's BFS post-4f4e682). 121 // Closes when: T#opt-002 closes. 122 // let v_post_opt: i64 = ir_validate_function(f, "post-opt" as *u8) 123 // if v_post_opt > 0 { 124 // sys_write(2, "nxc: post-opt validator violations in " as *u8, 38) 125 // sys_write(2, fn_name, 16) 126 // sys_write(2, "\n" as *u8, 1) 127 // } 128 let locs_raw: *u8 = sys_mmap(f.n_values * 24 + 64) 129 let locs: *ValueLoc = locs_raw as *ValueLoc 130 let cs_raw: *u8 = sys_mmap(16) 131 let cs: *i64 = cs_raw as *i64 132 *cs = 0 133 let cs_fpr_raw: *u8 = sys_mmap(16) 134 let cs_fpr: *i64 = cs_fpr_raw as *i64 135 *cs_fpr = 0 136 let sb_raw: *u8 = sys_mmap(16) 137 let sb: *i64 = sb_raw as *i64 138 *sb = 0 139 regalloc_function(f, locs, cs, cs_fpr, sb) 140 emit_function(f, locs, asm_out, fn_name, 16, 8, *cs, *cs_fpr) 141 fi = fi + 1 142 if (fi - (fi / 10) * 10) == 0 { 143 sys_write(2, "." as *u8, 1) 144 } 145 } 146 147 // Stage 3.5: emit module-level globals as .Lg<id>: .asciz "..." 148 // entries. Mirrors riscv.c's globals dump. Each VK_GLOBAL Value 149 // points here via its const_int (the global id); without these 150 // labels, every `la <reg>, .Lg<N>` in the function bodies points 151 // to nothing (was T#selfhost-003). 152 sys_write(2, "\nnxc: dumping globals (n=" as *u8, 25) 153 let ng_buf: *u8 = sys_mmap(16) 154 var ng: i64 = m.n_globals 155 var ngk: i64 = 0 156 if ng == 0 { ng_buf[ngk] = 0x30; ngk = 1 } 157 while ng > 0 { ng_buf[ngk] = 0x30 + (ng - (ng / 10) * 10); ng = ng / 10; ngk = ngk + 1 } 158 var ngj: i64 = ngk - 1 159 while ngj >= 0 { sys_write(2, (((ng_buf as i64) + ngj) as *u8), 1); ngj = ngj - 1 } 160 sys_write(2, ")\n" as *u8, 2) 161 162 out_str(asm_out, "\n .section .rodata\n" as *u8) 163 let g_base: i64 = m.globals as i64 164 var gi: i64 = 0 165 while gi < m.n_globals { 166 // Stride 80 unified across writers + dump + alloc. 167 // T#selfhost-006 root cause was parse.nx::parse_module 168 // sizing the function pool at 256 slots; nxc.nx has 419 169 // functions, so functions 257..419 overflowed into the 170 // adjacent globals pool, corrupting written globals. 171 // Closed 2026-04-26 by bumping that pool to 4096 slots. 172 // With the pool no longer corrupted, the natural struct 173 // field access works again. 174 let g: *Global = (g_base + gi * 80) as *Global 175 if g.zero_init == 0 { 176 // Use `.byte` listing instead of `.asciz` to bypass any 177 // assembler-side escape interpretation -- lex hands us 178 // raw bytes (escapes already processed at lex time when 179 // it works correctly; passes through as-is otherwise), 180 // so the safe contract is "bytes in, bytes out". 181 out_str(asm_out, ".Lg" as *u8) 182 out_i64(asm_out, g.id) 183 out_str(asm_out, ":\n .byte " as *u8) 184 var glen: i64 = g.len 185 if glen < 0 { glen = 0 } 186 if glen > 65536 { glen = 65536 } 187 var bk: i64 = 0 188 while bk < glen { 189 if bk > 0 { out_str(asm_out, ", " as *u8) } 190 out_i64(asm_out, g.bytes[bk]) 191 bk = bk + 1 192 } 193 if glen > 0 { out_str(asm_out, ", 0\n" as *u8) } 194 if glen == 0 { out_str(asm_out, "0\n" as *u8) } 195 } 196 gi = gi + 1 197 } 198 199 sys_write(2, "\nnxc: codegen done\n" as *u8, 19) 200 201 // Stage 4: assemble the .s text into machine bytes. 202 // 4 MB code buffer covers up to ~1 M instructions. 203 // When fd 3 is open the caller can capture the .s by redirecting 204 // it -- harmless when fd 3 is closed (sys_write returns -EBADF). 205 sys_write(3, asm_out.buf, asm_out.pos) 206 let code_buf: *u8 = sys_mmap(4194304) 207 let code_len: i64 = assemble(asm_out.buf, asm_out.pos, code_buf, 4194304) 208 if code_len <= 0 { 209 // Decode + print the negative return code so failures localise 210 // (-2 = pass-1 failure, -3 = pass-2 failure; nxasm_v2.nx 211 // prints the offending mnemonic + label name itself). 212 sys_write(2, "nxc: assemble returned " as *u8, 23) 213 var clen: i64 = code_len 214 if clen < 0 { 215 sys_write(2, "-" as *u8, 1) 216 clen = 0 - clen 217 } 218 let cnbuf: *u8 = sys_mmap(32) 219 var ck: i64 = 0 220 if clen == 0 { cnbuf[ck] = 0x30; ck = 1 } 221 while clen > 0 { 222 cnbuf[ck] = 0x30 + (clen - (clen / 10) * 10) 223 clen = clen / 10 224 ck = ck + 1 225 } 226 var cj: i64 = ck - 1 227 while cj >= 0 { 228 sys_write(2, (((cnbuf as i64) + cj) as *u8), 1) 229 cj = cj - 1 230 } 231 sys_write(2, "\n" as *u8, 1) 232 return 3 233 } 234 235 // Stage 5: wrap in an ELF executable and emit. 236 write_elf(code_buf, code_len, fd) 237 return 0 238} 239 240// Sovereign CLI: `nxc <source.nx>` prints the compiled ELF to stdout. 241// Pipe to a file: `nxc hello.nx > hello && chmod +x hello && ./hello`. 242// 243// Main signature matches the RV64 Linux kernel's entry contract: 244// a0 = argc, a1 = argv (pointer to an array of char* pointers). 245// Our `_start` stub marshals these from the initial stack; this 246// function unpacks the path string from argv[1] and drives the 247// compiler pipeline. 248// 249// When invoked with no args we fall back to a hardcoded demo source 250// so `./nxc` (no arg) still produces a working ELF that exits 30. 251// Top-level buffer cap for the import-expanded source text. Every 252// runtime/*.nx combined is well under 1 MB; 4 MB gives headroom for 253// large trees. One allocation, no realloc path. 254const EXPAND_OUT_CAP: i64 = 4194304 255 256func main(argc: i64, argv: *i64) -> i64 { 257 if argc < 2 { 258 // Fallback demo: source baked in for smoke-testing the chain. 259 // Single file, no imports -- exercises the compile pipeline 260 // without touching the import preprocessor. 261 // 262 // PREVENT layer (2026-05-16): we LOUDLY announce on stderr 263 // that the demo branch was taken. Previously this branch 264 // ran silently when callers passed a real input file via 265 // argv but the CRT0 didn't propagate argv to main (the 266 // tri_start.s bug). The result: every "self-host compiles 267 // input.nx" test was actually compiling the demo string, 268 // and nobody noticed for sessions. Now any test that meant 269 // to compile a real file sees `nxc: DEMO BRANCH` on stderr 270 // and the CI/bench wrapper can grep for it. Additive -- 271 // demo still works as the no-arg fallback; loud signal lets 272 // tests distinguish intent. 273 sys_write(2, "nxc: DEMO BRANCH taken (argc<2; CRT0 may be missing argv setup)\n" as *u8, 64) 274 let demo: *u8 = "func main() -> i64 { return __syscall(93, 30, 0, 0, 0, 0, 0) }" 275 return nxc_compile(demo, 1) 276 } 277 // argv[1] is a (char *). Interpret as a *u8 path and drive the 278 // full preprocessor -> compile chain. 279 let path: *u8 = (argv[1]) as *u8 280 281 sys_write(2, "nxc: expand start\n" as *u8, 18) 282 // F14 four-pillar PREVENT: granular phase markers inside expand 283 // so a future hang is localizable to <10 LOC without re-bisecting. 284 // Each marker is 1 char unique; trailing newline. Cardinal: 285 // intelligent + additive (no rule blocks code; new visibility 286 // only). 287 sys_write(2, "nxc: expand A\n" as *u8, 14) 288 let expand_buf: *u8 = sys_mmap(EXPAND_OUT_CAP) 289 sys_write(2, "nxc: expand B\n" as *u8, 14) 290 let ctx: *ExpandCtx = expand_ctx_new(expand_buf, EXPAND_OUT_CAP) 291 sys_write(2, "nxc: expand C\n" as *u8, 14) 292 let rc: i64 = expand_imports(ctx, path) 293 sys_write(2, "nxc: expand D\n" as *u8, 14) 294 if rc < 0 { return 10 - rc } // nonzero exit encodes error kind 295 sys_write(2, "nxc: expand done\n" as *u8, 17) 296 297 // Null-terminate the expanded buffer so the lexer sees EOF. 298 let op: *i64 = ctx.out_pos 299 let end: i64 = *op 300 expand_buf[end] = 0 301 302 return nxc_compile(expand_buf, 1) 303}