nx_compile_x86.nx source
↩ module page · 258 lines · 10178 B
1// nx_compile_x86.nx -- source-file driver for the NishiLang x86_64 backend.
2//
3// Reads .nx source on stdin (pre-expanded; imports already inlined),
4// runs:
5// lex_source -> parse_module -> opt_run (per function) ->
6// x86ctx_emit_module
7// and writes the AT&T x86_64 asm to stdout.
8//
9// The bash wrapper (bench/nx_compile_x86.sh) drives the source ->
10// native ELF pipeline:
11//
12// nxc2.exe --target asm runtime/nx_compile_x86.nx (build driver)
13// riscv64-as + ld -> nx_compile_x86.elf
14// cat source.nx | qemu-riscv64-static nx_compile_x86.elf
15// -> x86_64 asm on stdout
16// gcc -nostdlib -static asm -o native.elf
17// run natively
18//
19// Per cardinal feedback-no-nxc2-c-extension-only-nishilang-forward:
20// this driver is the NishiLang counterpart of nxc2.exe's --target
21// x86_64 path. No C-side modifications.
22//
23// nx_safety_envelope:
24// intended_use: "Compile a .nx source (pre-import-expanded)
25// to native x86_64 SysV asm via the IR-driven
26// x86_64 backend."
27// sil_target: SIL3
28// asil_target: QM
29// dal_target: DAL B
30// iec_62304_class: NONE
31// evidence: [no_floating_point,
32// bounded_stdin_read_per_jpl_rule_2,
33// sealed_pipeline_verdict_codes,
34// opt_optional_via_env]
35// hazard_register: [bug-tape-source-exceeds-cap-silently-truncated,
36// bug-tape-opcode-not-yet-wired-passes-through]
37// residual_risk: "Hard 2 MiB source cap; pasting larger files
38// truncates silently. Streaming follow-up
39// when needed."
40// verdict: NOT_YET_EVALUATED
41
42import "nx_syscalls.nx"
43import "nx_types.nx"
44import "nx_lex_kinds.nx"
45import "nx_outbuf.nx"
46import "nx_ir.nx"
47import "nx_tokenizer.nx"
48import "nx_parse.nx"
49import "nx_opt.nx"
50import "nx_x86_64.nx"
51import "nx_ir_dump.nx"
52import "nx_ir_validate.nx"
53import "nx_x86_regalloc.nx"
54import "nx_x86_64_ctx.nx"
55import "nx_import.nx"
56const NX_MAGIC_4096: i64 = 4096
57const NX_MAGIC_262144: i64 = 262144
58
59const NX_COMPILE_X86_SRC_CAP: i64 = 2097152 // 2 MiB
60const NX_COMPILE_X86_OUT_CAP: i64 = 16777216 // 16 MiB
61const NX_COMPILE_X86_EXPAND_CAP: i64 = 4194304 // 4 MiB import-expanded
62
63const NX_COMPILE_X86_OK: i64 = 0
64const NX_COMPILE_X86_EMPTY: i64 = 1
65const NX_COMPILE_X86_LEX_FAIL: i64 = 2
66const NX_COMPILE_X86_PARSE_FAIL: i64 = 3
67const NX_COMPILE_X86_NO_FN: i64 = 4
68
69func nxcx_read_stdin(buf: *u8, cap: i64) -> i64 {
70 var total: i64 = 0
71 let BUDGET: i64 = (cap / NX_MAGIC_4096) + 16
72 var iter: i64 = 0
73 var done: i64 = 0
74 while done == 0 {
75 if iter >= BUDGET { done = 1 }
76 if done == 0 {
77 if total >= cap { done = 1 }
78 if done == 0 {
79 let dst: *u8 = ((buf as i64) + total) as *u8
80 let want: i64 = cap - total
81 let got: i64 = sys_read(0, dst, want)
82 if got < 0 { done = 1 }
83 if got == 0 { done = 1 }
84 if got > 0 { total = total + got }
85 }
86 }
87 iter = iter + 1
88 }
89 return total
90}
91
92func nxcx_log(msg: *u8, n: i64) -> i64 {
93 return sys_write(2, msg, n)
94}
95
96func main(argc: i64, argv: *i64) -> i64 {
97 // ---- 1. Acquire source ----
98 //
99 // If argv[1] is provided, treat it as a file path and run import
100 // expansion (mirrors nx_nxc.nx). Else read raw source from stdin
101 // (no import expansion -- caller pre-expands).
102 var in_buf: *u8 = 0 as *u8
103 var in_n: i64 = 0
104 // Walk argv skipping flags so callers can mix `--target x86_64`
105 // (ignored by this binary -- it only emits x86_64) with a path
106 // argument, matching nxc2.exe's CLI shape. First arg not
107 // starting with '-' is the source path; absent it, read stdin.
108 var path_addr: i64 = 0
109 var i: i64 = 1
110 while i < argc {
111 let arg: *u8 = argv[i] as *u8
112 if arg[0] == 0x2D { // '-' flag
113 // --target NAME consumes the next argv slot too.
114 if arg[1] == 0x2D { // '--' long flag
115 if arg[2] == 0x74 { // 't' (target)
116 i = i + 2
117 continue
118 }
119 }
120 i = i + 1
121 continue
122 }
123 path_addr = arg as i64
124 i = argc // exit
125 }
126 if path_addr != 0 {
127 let path: *u8 = path_addr as *u8
128 let expand_buf: *u8 = sys_mmap(NX_COMPILE_X86_EXPAND_CAP)
129 let ctx: *ExpandCtx = expand_ctx_new(expand_buf,
130 NX_COMPILE_X86_EXPAND_CAP)
131 let rc: i64 = expand_imports(ctx, path)
132 if rc < 0 {
133 nxcx_log("nx_compile_x86: expand_imports failed\n" as *u8, 40)
134 return 10 - rc
135 }
136 let op: *i64 = ctx.out_pos
137 let end: i64 = *op
138 expand_buf[end] = 0 as u8
139 in_buf = expand_buf
140 in_n = end
141 }
142 if path_addr == 0 {
143 in_buf = sys_mmap(NX_COMPILE_X86_SRC_CAP)
144 in_n = nxcx_read_stdin(in_buf, NX_COMPILE_X86_SRC_CAP)
145 }
146 if in_n <= 0 {
147 nxcx_log("nx_compile_x86: empty input\n" as *u8, 28)
148 return NX_COMPILE_X86_EMPTY
149 }
150
151 // ---- 2. Lex ----
152 // DIAGNOSTICS: hand the parser the source so errors can show the offending line with a caret
153 // (additive -- an unset source means no snippet, never a wrong one).
154 nx_diag_set_source(in_buf, in_n)
155 let toks: *Tok = lex_source(in_buf, NX_MAGIC_262144)
156 if toks == (0 as *Tok) {
157 nxcx_log("nx_compile_x86: lex_source failed\n" as *u8, 35)
158 return NX_COMPILE_X86_LEX_FAIL
159 }
160
161 // ---- 3. Parse ----
162 let m: *Module = parse_module(toks, 0 as *Module)
163 if m == (0 as *Module) {
164 nxcx_log("nx_compile_x86: parse_module failed\n" as *u8, 37)
165 return NX_COMPILE_X86_PARSE_FAIL
166 }
167 if m.n_functions <= 0 {
168 nxcx_log("nx_compile_x86: no functions parsed\n" as *u8, 37)
169 return NX_COMPILE_X86_NO_FN
170 }
171
172 // ---- 3b. Post-parse IR validation (log-only, mirrors nx_nxc.nx) ----
173 //
174 // V4 (every block ends in a terminator) is the prevention gate for
175 // the unterminated-final-block class: parse_function used to leave a
176 // body's last open block with no OP_RETURN, and the emitter's
177 // creation-order layout made it FALL THROUGH into a sibling block
178 // (nx_ttt_evaluate draw-detect clobber, found 2026-06-09). Log-only
179 // until the validator earns abort authority, same policy as nx_nxc.
180 let v_post_parse: i64 = ir_validate_module(m, "post-parse" as *u8)
181 if v_post_parse > 0 {
182 nxcx_log("nx_compile_x86: post-parse validator found violations (see above)\n" as *u8, 66)
183 }
184
185 // ---- 4. Run opt on each function ----
186 //
187 // Enabled: the curated opt_run fixpoint (mem2reg + sccp + const-fold +
188 // copyprop + cse + gvn + licm + alloca_const + dce + thread_jumps +
189 // tail_call + strength_reduce + dse + block_merge + reassoc + sweep).
190 // mem2reg promotes alloca'd loop/state vars to SSA -> the dominant
191 // crypto-loop reload cost. Gated by the self-host gauntlet (byte-stable
192 // 2nd-gen fixpoint + differential KATs + register-survival KAT).
193 var fi: i64 = 0
194 while fi < m.n_functions {
195 let fn_base: i64 = m.functions as i64
196 let f: *Function = (fn_base + fi * 176) as *Function
197 opt_run(f)
198 fi = fi + 1
199 }
200
201 // ---- 4a. Single-block leaf inlining (module pass) ----
202 //
203 // Reuses the shared opt_inline_module organ (nx_opt.nx), run HERE -- after
204 // per-function opt so callee bodies are clean SSA; the older nxc.nx front-
205 // ends run it PRE-opt where it almost never fires. When it fires we re-run
206 // opt_run so copyprop/const-fold/DCE clean the spliced bodies in the
207 // caller's context. Closes the second half of the racing-bench spectral
208 // gap (the ~400K uninlined eval_A calls gcc inlines).
209 //
210 // OPT-IN (/tmp/nx_inline_on): opt_inline_module returns 0 by default while
211 // an nx_cc miscompile it exposes (an instr-walk in inline_call/
212 // opt_inline_module processing only its first element; root cause not yet
213 // identified) is outstanding, so this is a no-op on the normal path. See
214 // opt_inline_module's comment + the memory note.
215 let n_inlined: i64 = opt_inline_module(m)
216 if n_inlined > 0 {
217 var fj: i64 = 0
218 while fj < m.n_functions {
219 let fn_base2: i64 = m.functions as i64
220 let f2: *Function = (fn_base2 + fj * 176) as *Function
221 opt_run(f2)
222 fj = fj + 1
223 }
224 }
225
226 // ---- 4b. (Optional) IR dump for bootstrap-divergence diagnosis ----
227 //
228 // If /tmp/nx_ir_dump is present, dump every function's IR to
229 // stderr in a byte-comparable format. Run the same source
230 // through qemu-RV64 + native lanes, diff the traces, pinpoint
231 // the diverging instruction. 3-step recipe:
232 // 1. touch /tmp/nx_ir_dump
233 // 2. cat repro.nx | qemu-riscv64-static _offc/nx_compile_x86.elf > /dev/null 2> /tmp/rv64.trace
234 // 3. cat repro.nx | _offc/nx_compile_x86_native.elf > /dev/null 2> /tmp/native.trace
235 // diff -u /tmp/rv64.trace /tmp/native.trace | head -30
236 let ird_marker: *u8 = "/tmp/nx_ir_dump"
237 let ird_len_p: *i64 = sys_mmap(8) as *i64
238 let ird_buf: *u8 = sys_read_file(ird_marker, ird_len_p)
239 if (ird_buf as i64) != 0 {
240 var fi: i64 = 0
241 while fi < m.n_functions {
242 let fn_base: i64 = m.functions as i64
243 let f: *Function = (fn_base + fi * 176) as *Function
244 nx_ir_dump_function(f, "post-parse" as *u8)
245 fi = fi + 1
246 }
247 }
248
249 // ---- 5. Emit x86_64 asm via the IR-driven backend ----
250 let o: *OutBuf = out_new(NX_COMPILE_X86_OUT_CAP)
251 out_str(o, "# Generated by nx_compile_x86.nx via nx_x86_64_ctx\n")
252 out_str(o, " .att_syntax prefix\n")
253 x86ctx_emit_module(m, o)
254
255 // ---- 6. Write asm to stdout ----
256 sys_write(1, o.buf, o.pos)
257 return NX_COMPILE_X86_OK
258}