nxc.nx source
↩ module page · 282 lines · 12341 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
29import "syscalls.nx"
30import "types.nx"
31import "lex_kinds.nx"
32import "outbuf.nx"
33import "ir.nx"
34import "ir_validate.nx"
35import "lex.nx"
36import "parse.nx"
37import "opt.nx"
38import "regalloc.nx"
39import "nx_riscv.nx"
40import "crt0.nx"
41import "import.nx"
42import "nxasm_v2.nx"
43import "elf_writer.nx"
44
45// Drive: source text -> ELF bytes written to `fd`.
46//
47// Buffer sizes calibrated against nxc.nx-compiling-itself (the
48// Thompson-defense bootstrap target). Source ~6KLOC NishiLang
49// expands to ~85K lines of asm + ~340K bytes of code + ~250K
50// tokens. Generous headroom to handle real programs without
51// overflow:
52//
53// tokens: 256K (was 4K) -- Source <= ~25K LOC sustainable
54// asm_out: 16 MB (was 128K) -- compiler-class outputs fit
55// code_buf: 4 MB (was 64K) -- 1M instructions max
56//
57// All allocated via sys_mmap which is page-aligned + lazy-faulted,
58// so unused pages cost ~zero RSS.
59func nxc_compile(src: *u8, fd: i64) -> i64 {
60 // Stage 1: lex source into tokens. 256K-token cap covers
61 // any program up to ~25K source lines.
62 sys_write(2, "nxc: lex start\n" as *u8, 15)
63 let toks: *Tok = lex_source(src, 262144)
64 if toks == (0 as *Tok) { return 1 }
65 sys_write(2, "nxc: lex done\n" as *u8, 14)
66
67 // Stage 2: parse into a Module.
68 let m: *Module = parse_module(toks, 0 as *Module)
69 if m == (0 as *Module) { return 2 }
70 sys_write(2, "nxc: parse done\n" as *u8, 16)
71
72 // Stage 2.4: structural CFG validator on the post-parse IR.
73 // Catches V1 (compaction skew), V2 (BR/BR_COND target out of
74 // range), V3 (succ ptr disagrees with op id) before any opt
75 // pass runs. Reports per-violation to stderr; non-zero
76 // total just logs (doesn't abort) until we're confident the
77 // validator itself is bug-free.
78 let v_post_parse: i64 = ir_validate_module(m, "post-parse" as *u8)
79 if v_post_parse > 0 {
80 sys_write(2, "nxc: post-parse validator found violations (see above)\n" as *u8, 55)
81 }
82
83 // Stage 2.5: module-level cross-function inlining (F4 pass).
84 // Walks every call site; tiny pure callees get inlined into
85 // their callers. Defined in opt.nx since 188; previously
86 // unwired, now active in the sovereign self-host pipeline.
87 opt_inline_module(m)
88
89 // Stage 3: opt + regalloc + riscv emit for every function.
90 // Prepend the sovereign _start stub so the ELF entry point
91 // lands on valid code that forwards argc/argv to main + exits.
92 //
93 // Progress markers on stderr: one '.' per 10 functions compiled
94 // so bootstrap_proof.sh can see liveness during a slow qemu run.
95 // Suppressed when NXC_QUIET is set (not yet wired; future flag).
96 sys_write(2, "nxc: lex+parse done\n" as *u8, 20)
97 let asm_out: *OutBuf = out_new(16777216) // 16 MB
98 emit_crt0_start(asm_out)
99 var fi: i64 = 0
100 while fi < m.n_functions {
101 let fn_base: i64 = m.functions as i64
102 let f: *Function = (fn_base + fi * 176) as *Function
103 let name_addr: i64 = f.name_start
104 let fn_name: *u8 = name_addr as *u8
105 opt_run(f)
106 // STUB(validator, T#validator-001): post-opt validator
107 // intentionally disabled while opt_sweep SWEEP3 leaves
108 // succ pointers stale (T#opt-002). The validator would
109 // correctly fire V3 on every reachable function, drowning
110 // real-bug signal. Re-enable in tandem with T#opt-002
111 // closing.
112 // Plan: re-add this call once SWEEP3 either rebuilds
113 // succs cleanly OR all succ-walking passes have moved
114 // to BR-op walking (matching opt_sweep's BFS post-4f4e682).
115 // Closes when: T#opt-002 closes.
116 // let v_post_opt: i64 = ir_validate_function(f, "post-opt" as *u8)
117 // if v_post_opt > 0 {
118 // sys_write(2, "nxc: post-opt validator violations in " as *u8, 38)
119 // sys_write(2, fn_name, 16)
120 // sys_write(2, "\n" as *u8, 1)
121 // }
122 let locs_raw: *u8 = sys_mmap(f.n_values * 24 + 64)
123 let locs: *ValueLoc = locs_raw as *ValueLoc
124 let cs_raw: *u8 = sys_mmap(16)
125 let cs: *i64 = cs_raw as *i64
126 *cs = 0
127 let cs_fpr_raw: *u8 = sys_mmap(16)
128 let cs_fpr: *i64 = cs_fpr_raw as *i64
129 *cs_fpr = 0
130 let sb_raw: *u8 = sys_mmap(16)
131 let sb: *i64 = sb_raw as *i64
132 *sb = 0
133 regalloc_function(f, locs, cs, cs_fpr, sb)
134 // NOTE: ra_slot is hardcoded `8` here. Passing *sb instead
135 // would be correct in principle (ra above spill area), but
136 // nx_regalloc.nx undercounts spills relative to its actual
137 // emission, so *sb is unreliable. Hardcoded 8 happens to
138 // work for simple programs that fit within tiny frames.
139 // Real fix: port C's compute_liveness so NX regalloc accounts
140 // spills correctly, THEN switch to *sb. See task #10.
141 emit_function(f, locs, asm_out, fn_name, 16, 8, *cs, *cs_fpr)
142 fi = fi + 1
143 if (fi - (fi / 10) * 10) == 0 {
144 sys_write(2, "." as *u8, 1)
145 }
146 }
147
148 // Stage 3.5: emit module-level globals as .Lg<id>: .asciz "..."
149 // entries. Mirrors riscv.c's globals dump. Each VK_GLOBAL Value
150 // points here via its const_int (the global id); without these
151 // labels, every `la <reg>, .Lg<N>` in the function bodies points
152 // to nothing (was T#selfhost-003).
153 sys_write(2, "\nnxc: dumping globals (n=" as *u8, 25)
154 let ng_buf: *u8 = sys_mmap(16)
155 var ng: i64 = m.n_globals
156 var ngk: i64 = 0
157 if ng == 0 { ng_buf[ngk] = 0x30; ngk = 1 }
158 while ng > 0 { ng_buf[ngk] = 0x30 + (ng - (ng / 10) * 10); ng = ng / 10; ngk = ngk + 1 }
159 var ngj: i64 = ngk - 1
160 while ngj >= 0 { sys_write(2, (((ng_buf as i64) + ngj) as *u8), 1); ngj = ngj - 1 }
161 sys_write(2, ")\n" as *u8, 2)
162
163 out_str(asm_out, "\n .section .rodata\n" as *u8)
164 let g_base: i64 = m.globals as i64
165 var gi: i64 = 0
166 while gi < m.n_globals {
167 // Stride 80 unified across writers + dump + alloc.
168 // T#selfhost-006 root cause was parse.nx::parse_module
169 // sizing the function pool at 256 slots; nxc.nx has 419
170 // functions, so functions 257..419 overflowed into the
171 // adjacent globals pool, corrupting written globals.
172 // Closed 2026-04-26 by bumping that pool to 4096 slots.
173 // With the pool no longer corrupted, the natural struct
174 // field access works again.
175 let g: *Global = (g_base + gi * 80) as *Global
176 if g.zero_init == 0 {
177 // Use `.byte` listing instead of `.asciz` to bypass any
178 // assembler-side escape interpretation -- lex hands us
179 // raw bytes (escapes already processed at lex time when
180 // it works correctly; passes through as-is otherwise),
181 // so the safe contract is "bytes in, bytes out".
182 out_str(asm_out, ".Lg" as *u8)
183 out_i64(asm_out, g.id)
184 out_str(asm_out, ":\n .byte " as *u8)
185 var glen: i64 = g.len
186 if glen < 0 { glen = 0 }
187 if glen > 65536 { glen = 65536 }
188 var bk: i64 = 0
189 while bk < glen {
190 if bk > 0 { out_str(asm_out, ", " as *u8) }
191 out_i64(asm_out, g.bytes[bk])
192 bk = bk + 1
193 }
194 if glen > 0 { out_str(asm_out, ", 0\n" as *u8) }
195 if glen == 0 { out_str(asm_out, "0\n" as *u8) }
196 }
197 gi = gi + 1
198 }
199
200 sys_write(2, "\nnxc: codegen done\n" as *u8, 19)
201
202 // Stage 4: assemble the .s text into machine bytes.
203 // 4 MB code buffer covers up to ~1 M instructions.
204 // When fd 3 is open the caller can capture the .s by redirecting
205 // it -- harmless when fd 3 is closed (sys_write returns -EBADF).
206 sys_write(3, asm_out.buf, asm_out.pos)
207 let code_buf: *u8 = sys_mmap(4194304)
208 let code_len: i64 = assemble(asm_out.buf, asm_out.pos, code_buf, 4194304)
209 if code_len <= 0 {
210 // Decode + print the negative return code so failures localise
211 // (-2 = pass-1 failure, -3 = pass-2 failure; nxasm_v2.nx
212 // prints the offending mnemonic + label name itself).
213 sys_write(2, "nxc: assemble returned " as *u8, 23)
214 var clen: i64 = code_len
215 if clen < 0 {
216 sys_write(2, "-" as *u8, 1)
217 clen = 0 - clen
218 }
219 let cnbuf: *u8 = sys_mmap(32)
220 var ck: i64 = 0
221 if clen == 0 { cnbuf[ck] = 0x30; ck = 1 }
222 while clen > 0 {
223 cnbuf[ck] = 0x30 + (clen - (clen / 10) * 10)
224 clen = clen / 10
225 ck = ck + 1
226 }
227 var cj: i64 = ck - 1
228 while cj >= 0 {
229 sys_write(2, (((cnbuf as i64) + cj) as *u8), 1)
230 cj = cj - 1
231 }
232 sys_write(2, "\n" as *u8, 1)
233 return 3
234 }
235
236 // Stage 5: wrap in an ELF executable and emit.
237 write_elf(code_buf, code_len, fd)
238 return 0
239}
240
241// Sovereign CLI: `nxc <source.nx>` prints the compiled ELF to stdout.
242// Pipe to a file: `nxc hello.nx > hello && chmod +x hello && ./hello`.
243//
244// Main signature matches the RV64 Linux kernel's entry contract:
245// a0 = argc, a1 = argv (pointer to an array of char* pointers).
246// Our `_start` stub marshals these from the initial stack; this
247// function unpacks the path string from argv[1] and drives the
248// compiler pipeline.
249//
250// When invoked with no args we fall back to a hardcoded demo source
251// so `./nxc` (no arg) still produces a working ELF that exits 30.
252// Top-level buffer cap for the import-expanded source text. Every
253// runtime/*.nx combined is well under 1 MB; 4 MB gives headroom for
254// large trees. One allocation, no realloc path.
255const EXPAND_OUT_CAP: i64 = 4194304
256
257func main(argc: i64, argv: *i64) -> i64 {
258 if argc < 2 {
259 // Fallback demo: source baked in for smoke-testing the chain.
260 // Single file, no imports -- exercises the compile pipeline
261 // without touching the import preprocessor.
262 let demo: *u8 = "func main() -> i64 { return __syscall(93, 30, 0, 0, 0, 0, 0) }"
263 return nxc_compile(demo, 1)
264 }
265 // argv[1] is a (char *). Interpret as a *u8 path and drive the
266 // full preprocessor -> compile chain.
267 let path: *u8 = (argv[1]) as *u8
268
269 sys_write(2, "nxc: expand start\n" as *u8, 18)
270 let expand_buf: *u8 = sys_mmap(EXPAND_OUT_CAP)
271 let ctx: *ExpandCtx = expand_ctx_new(expand_buf, EXPAND_OUT_CAP)
272 let rc: i64 = expand_imports(ctx, path)
273 if rc < 0 { return 10 - rc } // nonzero exit encodes error kind
274 sys_write(2, "nxc: expand done\n" as *u8, 17)
275
276 // Null-terminate the expanded buffer so the lexer sees EOF.
277 let op: *i64 = ctx.out_pos
278 let end: i64 = *op
279 expand_buf[end] = 0
280
281 return nxc_compile(expand_buf, 1)
282}