nx_compile_field_candidate_t280.nx source
↩ module page · 664 lines · 42427 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_field_candidate_t280.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"
56// Crash diagnostics (v11 rung 1a, 2026-08-13): a compiler SEGV used to print NOTHING (the r6
57// class -- "nx_cc ITSELF SEGVs, no diagnostic, symbol dump stops at main"). One call at main
58// entry turns it into instruction+fault addresses on stderr; wait-status unchanged.
59import "nx_crash.nx"
60// LN44 (2026-09-03): the --mode= conf resolver. nx_comparetree_lib owns "one document, two tree roots,
61// and SAY WHICH ONE ANSWERED". Only its GENERIC *_2dir primitives are used here, never its /compare
62// wrappers, so the compare-side order adjudication (ct_first_published et al) cannot reach the compiler.
63import "nx_comparetree_lib.nx"
64const NX_MAGIC_4096: i64 = 4096
65
66const NX_COMPILE_X86_SRC_CAP: i64 = 2097152 // 2 MiB
67const NX_COMPILE_X86_OUT_CAP: i64 = 16777216 // 16 MiB
68const NX_COMPILE_X86_EXPAND_CAP: i64 = 4194304 // 4 MiB import-expanded
69
70const NX_COMPILE_X86_OK: i64 = 0
71const NX_COMPILE_X86_EMPTY: i64 = 1
72const NX_COMPILE_X86_LEX_FAIL: i64 = 2
73const NX_COMPILE_X86_PARSE_FAIL: i64 = 3
74const NX_COMPILE_X86_NO_FN: i64 = 4
75
76func nxcx_read_stdin(buf: *u8, cap: i64) -> i64 {
77 var total: i64 = 0
78 let BUDGET: i64 = (cap / NX_MAGIC_4096) + 16
79 var iter: i64 = 0
80 var done: i64 = 0
81 while done == 0 {
82 if iter >= BUDGET { done = 1 }
83 if done == 0 {
84 if total >= cap { done = 1 }
85 if done == 0 {
86 let dst: *u8 = ((buf as i64) + total) as *u8
87 let want: i64 = cap - total
88 let got: i64 = sys_read(0, dst, want)
89 if got < 0 { done = 1 }
90 if got == 0 { done = 1 }
91 if got > 0 { total = total + got }
92 }
93 }
94 iter = iter + 1
95 }
96 return total
97}
98
99// LN27 (2026-09-02): THE LENGTH IS DERIVED FROM THE LITERAL, NEVER HAND-COUNTED. Measured across this
100// file's fourteen call sites: six carried a count that disagreed with the literal beside it (111 vs 114,
101// 129 vs 131, 40 vs 38, 35 vs 34, 37 vs 36 twice) -- a hand-counted length beside a string literal is a
102// second copy of that literal's shape, and the two drift silently: too small truncates the sentence, too
103// large leaks the bytes that follow the literal into stderr. The `n` parameter is kept so no call site
104// changes shape, but it is no longer trusted: every message is NUL-terminated by the compiler, so its
105// length is measured here at the one chokepoint, the same rule nx_diag_puts already follows.
106func nxcx_log(msg: *u8, n: i64) -> i64 {
107 var m: i64 = 0
108 while msg[m] != (0 as u8) { m = m + 1 }
109 return sys_write(2, msg, m)
110}
111
112// ---- --mode=<name>: THE COMPILER'S MODES AS DATA (2026-09-01) ----------------------------------
113// Operator: "real speed like an F1 ... a real daily driver best in the world, so that it has different
114// modes like the drag race and F1". A mode is a NAMED BUNDLE of the declared flags this driver already
115// parses, declared in knowledge/lang_modes.conf (`mode|<name>|<flags>|<intent>`), and it is expanded
116// HERE, before the ordinary argv loop, into exactly those tokens -- so a mode can never do anything a
117// hand-typed flag list could not, and the flag loop below is untouched. An undeclared mode, or a
118// missing conf, REFUSES the build (NX_COMPILE_X86_BAD_MODE): a silent fallback to the default would be
119// a mode wearing the wrong name, which is the shape of defect this estate keeps paying for.
120// The conf is resolved from EITHER tree root -- see the LN44 note below. It is deliberately no longer a
121// single CWD-relative path: the one CWD the build queue actually runs in could not resolve that path.
122const NX_COMPILE_X86_BAD_MODE: i64 = 9
123const NXCX_MODE_PREFIX_LEN: i64 = 7 // "--mode="
124const NXCX_MODE_MAX_TOKENS: i64 = 32
125// LN44 (2026-09-03) -- THE CONF NOW RESOLVES FROM EITHER TREE ROOT, AND THE RESOLVER SAYS WHICH ONE WON.
126// The incumbent bare path, knowledge/lang_modes.conf, deleted in this change, resolved only when the process
127// CWD was buildroot. The BUILD QUEUE starts in
128// the SERVING ROOT, where that path does not exist -- so a --mode= build issued from the queue could never
129// read the conf and would REFUSE (NX_COMPILE_X86_BAD_MODE), while the identical command run from buildroot
130// succeeded. The same literal naming a different file depending on the caller's CWD is exactly the defect
131// nx_comparetree_lib was written for, so its resolver is COMPOSED here rather than a second one written.
132// POSITION 1 IS THE INCUMBENT PATH, BYTE FOR BYTE, AND THAT IS WHAT MAKES THIS A WIDENING: every CWD that
133// resolved before still resolves FIRST and to the same file, and position 2 can only turn a REFUSAL into a
134// success. It cannot make a build that passes today begin to fail.
135// ct_build_path concatenates dir+stem+suffix with no separator, so each trailing slash is load-bearing.
136const NXCX_MODES_DIR_1: *u8 = "knowledge/"
137const NXCX_MODES_DIR_2: *u8 = "buildroot/knowledge/"
138const NXCX_MODES_STEM: *u8 = "lang_modes"
139const NXCX_MODES_SUF: *u8 = ".conf"
140func nxcx_is_mode_flag(a: *u8) -> i64 {
141 if a[0] != (45 as u8) { return 0 }
142 if a[1] != (45 as u8) { return 0 }
143 if a[2] != (109 as u8) { return 0 }
144 if a[3] != (111 as u8) { return 0 }
145 if a[4] != (100 as u8) { return 0 }
146 if a[5] != (101 as u8) { return 0 }
147 if a[6] != (61 as u8) { return 0 }
148 return 1
149}
150func nxcx_streq(a: *u8, b: *u8) -> i64 {
151 var i: i64 = 0
152 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
153 if b[i] != (0 as u8) { return 0 }
154 return 1
155}
156// Returns a fresh argv: slot 0 = count, slots 1.. = the pointers; argv[0], then the mode's flag
157// tokens, then every original argument except the --mode= one. Exits with NX_COMPILE_X86_BAD_MODE
158// when the conf or the mode is absent.
159func nxcx_expand_mode(argc: i64, argv: *i64, mi: i64, marg: *u8) -> *i64 {
160 let name: *u8 = (marg as i64 + NXCX_MODE_PREFIX_LEN) as *u8
161 let ln: *i64 = sys_mmap(16) as *i64
162 let wh: *i64 = sys_mmap(16) as *i64
163 let cb: *u8 = ct_readall_2dir(NXCX_MODES_DIR_1, NXCX_MODES_DIR_2, NXCX_MODES_STEM, NXCX_MODES_SUF, ln, wh)
164 if (cb as i64) == 0 {
165 nxcx_log("nx_compile_x86: --mode= given but lang_modes.conf resolved in NEITHER tree root -- REFUSED\n" as *u8, 0)
166 nx_diag_puts(" tried, in order: knowledge/lang_modes.conf then buildroot/knowledge/lang_modes.conf, both relative to this process working directory.\n" as *u8)
167 nx_diag_puts(" why the build stopped: every mode is a declared row in lang_modes.conf; with the file unreadable the mode cannot be expanded into flags, and building the default under the mode name would be a mode wearing the wrong name.\n" as *u8)
168 nx_diag_puts(" fix: run from the serving root or from buildroot -- either resolves now -- or drop --mode to build the default.\n" as *u8)
169 sys_exit(NX_COMPILE_X86_BAD_MODE)
170 }
171 let n: i64 = ln[0]
172 // WHICH ROOT ANSWERED, ANNOUNCED ON THE SUCCESS PATH TOO. A resolver that returns bytes without saying
173 // where they came from reproduces the original defect one layer up, and the two knowledge trees are
174 // FORKED rather than copies -- so a silent pick between two lang_modes.conf files could expand a mode
175 // from the one nobody was looking at. CT_TREE_PRIMARY here means POSITION 1 (the bare path) and
176 // CT_TREE_SECONDARY POSITION 2: position, never a tree name, because this call passes its OWN dirs and
177 // the lib can only honestly report which ARGUMENT won. stderr, never stdout -- stdout carries the asm.
178 if wh[0] == CT_TREE_PRIMARY { nxcx_log("nx_compile_x86: --mode= conf resolved at knowledge/lang_modes.conf\n" as *u8, 0) }
179 if wh[0] == CT_TREE_SECONDARY { nxcx_log("nx_compile_x86: --mode= conf resolved at buildroot/knowledge/lang_modes.conf\n" as *u8, 0) }
180 let out: *i64 = sys_mmap((argc + NXCX_MODE_MAX_TOKENS + 4) * 8) as *i64
181 var found: i64 = 0
182 var p: i64 = 0
183 while p < n {
184 var e: i64 = p
185 while e < n { if cb[e] == (10 as u8) { break } e = e + 1 }
186 cb[e] = 0 as u8
187 let line: *u8 = (cb as i64 + p) as *u8
188 p = e + 1
189 // mode|<name>|<flags>|<intent>
190 if line[0] == (109 as u8) { if line[1] == (111 as u8) { if line[2] == (100 as u8) { if line[3] == (101 as u8) { if line[4] == (124 as u8) {
191 var q: i64 = 5
192 while line[q] != (0 as u8) { if line[q] == (124 as u8) { break } q = q + 1 }
193 if line[q] == (124 as u8) {
194 line[q] = 0 as u8
195 if nxcx_streq((line as i64 + 5) as *u8, name) == 1 {
196 found = 1
197 var flags: i64 = q + 1
198 var fe: i64 = flags
199 while line[fe] != (0 as u8) { if line[fe] == (124 as u8) { break } fe = fe + 1 }
200 line[fe] = 0 as u8
201 var cnt: i64 = 0
202 out[1] = argv[0]
203 cnt = 1
204 // tokens split on spaces, each NUL-terminated in place
205 var t: i64 = flags
206 while t < fe {
207 while t < fe { if line[t] == (32 as u8) { t = t + 1 } else { break } }
208 if t < fe {
209 let ts: i64 = t
210 while t < fe { if line[t] == (32 as u8) { break } t = t + 1 }
211 line[t] = 0 as u8
212 if cnt < argc + NXCX_MODE_MAX_TOKENS { out[1 + cnt] = (line as i64) + ts; cnt = cnt + 1 }
213 t = t + 1
214 }
215 }
216 var k: i64 = 1
217 while k < argc {
218 if k != mi { out[1 + cnt] = argv[k]; cnt = cnt + 1 }
219 k = k + 1
220 }
221 out[0] = cnt
222 p = n
223 }
224 }
225 } } } } }
226 }
227 if found == 0 {
228 nxcx_log("nx_compile_x86: --mode=" as *u8, 23)
229 var nl: i64 = 0
230 while name[nl] != (0 as u8) { nl = nl + 1 }
231 sys_write(2, name, nl)
232 nxcx_log(" is not declared in knowledge/lang_modes.conf -- REFUSED (declare the mode row; a silent default is a mode wearing the wrong name)\n" as *u8, 129)
233 nx_diag_puts(" why the build stopped: every mode is a declared row expanded into flags; an undeclared name would silently build the default while claiming the mode you asked for.\n" as *u8)
234 nx_diag_puts(" fix: use a mode declared in knowledge/lang_modes.conf (the rows there are the whole list), or add a row for the new mode before building with it.\n" as *u8)
235 sys_exit(NX_COMPILE_X86_BAD_MODE)
236 }
237 return out
238}
239
240func main(argc: i64, argv: *i64) -> i64 {
241 nx_crash_guard()
242 // ---- 1. Acquire source ----
243 //
244 // If argv[1] is provided, treat it as a file path and run import
245 // expansion (mirrors nx_nxc.nx). Else read raw source from stdin
246 // (no import expansion -- caller pre-expands).
247 var in_buf: *u8 = 0 as *u8
248 var in_n: i64 = 0
249 // Walk argv skipping flags so callers can mix `--target x86_64`
250 // (ignored by this binary -- it only emits x86_64) with a path
251 // argument, matching nxc2.exe's CLI shape. First arg not
252 // starting with '-' is the source path; absent it, read stdin.
253 var path_addr: i64 = 0
254 var no_guard: i64 = 0
255 var no_dce: i64 = 0
256 var ir_dump_flag: i64 = 0
257 // --mode=<name> expands into its declared bundle BEFORE the loop; the loop then sees only flags it already knows
258 var argc_eff: i64 = argc
259 var argv_eff: *i64 = argv
260 var mscan: i64 = 1
261 while mscan < argc {
262 let marg: *u8 = argv[mscan] as *u8
263 if nxcx_is_mode_flag(marg) == 1 {
264 let ex: *i64 = nxcx_expand_mode(argc, argv, mscan, marg)
265 argc_eff = ex[0]
266 argv_eff = (ex as i64 + 8) as *i64
267 mscan = argc
268 }
269 mscan = mscan + 1
270 }
271 var i: i64 = 1
272 while i < argc_eff {
273 let arg: *u8 = argv_eff[i] as *u8
274 if arg[0] == 0x2D { // '-' flag
275 // -g : emit .file/.loc debug-line directives. DEFAULT OFF, deliberately -- see the
276 // switch's note in nx_linemap.nx. The line MAP stays on regardless, because it costs
277 // nothing in the binary and powers per-file diagnostics; only the DIRECTIVES are gated.
278 if arg[1] == 0x67 { if arg[2] == 0 { lm_set_debug(1) } }
279 // --target NAME consumes the next argv slot too.
280 if arg[1] == 0x2D { // '--' long flag
281 if arg[2] == 0x74 { // 't' (target)
282 i = i + 2
283 continue
284 }
285 // --no-crash-guard : opt OUT of the default-on crash guard (v12 rung 1b-ii) --
286 // the minimal-binary lane (the 168 B return-42 demo) and any caller that must
287 // not carry the ~5 KB guard closure.
288 // --no-dce : opt OUT of whole-program reachability pruning (B1, 2026-08-18) --
289 // the A/B lever for the equivalence proof and for any suspected miscompile
290 // ("does it reproduce with --no-dce?"). Both spell "--no-", so the 6th byte
291 // decides: 'c'rash-guard vs 'd'ce.
292 if arg[2] == 0x6E { // 'n' ("--no-...")
293 if arg[5] == 0x63 { no_guard = 1 } // 'c'
294 if arg[5] == 0x64 { no_dce = 1 } // 'd'
295 }
296 // --ptrprov : LN3 raw-pointer provenance, the DECLARED MODE (default OFF).
297 // A `let`-bound sys_mmap(<const>) pointer's indexing is bounds-checked like a
298 // typed array (read AND write legs), and a provably-out-of-range constant index
299 // is refused at compile time. Default builds are byte-identical by construction;
300 // the per-class ratchet flips the default only after a clean corpus census.
301 if arg[2] == 0x70 { nx_ptrprov_set(1) } // 'p' ("--ptrprov")
302 // --chkarith : LN1 checked integer arithmetic, the DECLARED MODE (default OFF).
303 // Every i64 `+ - *` traps (exit NX_TRAP_OVERFLOW) on overflow instead of wrapping,
304 // and a constant overflow is refused at compile time; __wrap_add/sub/mul are
305 // exempt. Default builds are byte-identical by construction (nothing is emitted
306 // unless the flag is on); the per-class ratchet flips the default after a clean
307 // corpus census, the same contract as --ptrprov.
308 if arg[2] == 0x63 { nx_chkarith_set(1) } // 'c' ("--chkarith")
309 // --ir-dump : dump every function's POST-OPT IR to stderr (the nx_ir_dump line
310 // format), for THIS compile only. The older /tmp/nx_ir_dump marker below still
311 // works but is a host-global switch that flips every concurrent compile on the box;
312 // a per-invocation flag is what a gate can use without side effects on its neighbours
313 // (LN8's wiring gate reads the result kind of an xor-self instruction from it).
314 if arg[2] == 0x69 { ir_dump_flag = 1 } // 'i' ("--ir-dump")
315 // --optenforce : LN2 option / null enforcement, the DECLARED MODE (default OFF).
316 // A dereference of a pointer-typed local not proven non-null on its path (by
317 // `if p != 0 {`, an early-exit `if p == 0 { return }`, `while p != 0 {` or
318 // nx_assert_ptr) is REFUSED at parse time naming the local. Default builds are
319 // byte-identical by construction; the per-class ratchet flips the default after a
320 // clean corpus census, the same contract as --ptrprov and --chkarith.
321 // --ownership : LN4 + LN5 ownership moves and use-after-free, the DECLARED MODE
322 // (default OFF). A use of a named owned buffer after `__move(p)` transferred it, or
323 // after `sys_munmap(p, n)` released it, is REFUSED at parse time naming the local AND
324 // the line that ended its life; a double move and a double free fall out of the same
325 // rule. Emits no IR under EITHER mode, so default builds are byte-identical by
326 // construction; the per-class ratchet flips the default after a clean corpus census,
327 // the same contract as --ptrprov, --chkarith and --optenforce.
328 // BOTH LONG FLAGS BEGINNING "--o" NOW DISAMBIGUATE ON BYTE 3, and that is why this
329 // line changed shape: `if arg[2] == 0x6F` alone matched EVERY "--o..." spelling, so
330 // --ownership would silently have switched --optenforce on as well and entangled two
331 // independent modes. 'p' = --optenforce, 'w' = --ownership.
332 if arg[2] == 0x6F {
333 if arg[3] == 0x70 { nx_optenforce_set(1) } // 'p' ("--optenforce")
334 if arg[3] == 0x77 { nx_ownership_set(1) } // 'w' ("--ownership")
335 }
336 // --bckelide : LN7 sound bounds-check elision, the DECLARED MODE (default OFF).
337 // A bounds check whose (index SSA value id, constant length) pair was ALREADY
338 // checked at a point that DOMINATES it is made unconditional; a check that is
339 // not PROVABLY dominated is kept, and the FIRST check of every pair is always
340 // kept, so no access is ever left unchecked on its first reach.
341 // The exit_group syscall number travels WITH the flag rather than being copied
342 // into nx_bck_elide.nx: that pass has to recognise the trap sequence
343 // emit_bounds_check_v emits, so a second spelling of the number could drift out
344 // of agreement with the very sequence it exists to match -- and the failure
345 // mode of that drift is a pass that silently matches nothing, which reads as
346 // "there was nothing to elide" rather than as a defect.
347 // Default builds are byte-identical by construction (nothing is rewritten
348 // unless the flag is on), the same contract as --ptrprov, --chkarith,
349 // --optenforce and --ownership.
350 if arg[2] == 0x62 { nx_bckelide_set(1, NX_RV64_SYS_EXIT_GROUP) } // 'b' ("--bckelide")
351 // --sendcheck : LN6 data-race typing, the DECLARED MODE (default OFF). A per-task
352 // context handed to nx_pool_submit as `p as i64` must point at a struct marked `send`;
353 // an unmarked struct is REFUSED at parse time naming it, an untyped buffer or a plain
354 // integer is abstained (the check judges only what the type system can see). Emits no
355 // IR under either mode, so default builds are byte-identical by construction; the
356 // per-class ratchet flips the default after the corpus census is marked.
357 if arg[2] == 0x73 { nx_sendcheck_set(1) } // 's' ("--sendcheck")
358 }
359 i = i + 1
360 continue
361 }
362 // FIRST non-flag argument is the source path. DO NOT clobber the loop cursor to exit:
363 // `i = argc` here silently DISCARDED every flag that appeared AFTER the path, so
364 // `nx_compile_x86 file.nx --no-dce` parsed the path and then never saw the flag -- it
365 // returned rc 0 and quietly did the OPPOSITE of what the caller asked. That is the
366 // estate's own cursor-clobber hazard, and this block's own comment explicitly promises
367 // callers may MIX flags with a path argument. MEASURED 2026-08-25 on nishi_gui.nx: flag
368 // AFTER path -> `live=414 pruned=254` (flag ignored); flag BEFORE path -> `live=668
369 // pruned=0 (--no-dce)`. Same compiler, same source; argv ORDER was the only difference.
370 // It cost a month: the Windows daily driver's asm went stale because every rebuild
371 // segfaulted, and the `--no-crash-guard` that fixes it was being dropped for sitting
372 // after the path. Affects EVERY flag here: -g --no-dce --no-crash-guard --ptrprov
373 // --chkarith --ir-dump --optenforce.
374 // Keeping the FIRST path and continuing the scan is a strict superset of the old
375 // behaviour: single-path invocations are byte-identical, a second non-flag arg is still
376 // ignored, and flags in any position now bind. Bite-proven both ways plus a negative
377 // control (no flag -> DCE still prunes 254), and flag-before == flag-after byte-identical.
378 if path_addr == 0 { path_addr = arg as i64 }
379 i = i + 1
380 }
381 if path_addr != 0 {
382 let path: *u8 = path_addr as *u8
383 let expand_buf: *u8 = sys_mmap(NX_COMPILE_X86_EXPAND_CAP)
384 let ctx: *ExpandCtx = expand_ctx_new(expand_buf,
385 NX_COMPILE_X86_EXPAND_CAP)
386 // PER-FILE DIAGNOSTICS (2026-08-06): ask the expander to record which file
387 // each expanded line came from. ONLY this path can -- the stdin path below
388 // receives source that somebody else already flattened, so it has nothing to
389 // map and correctly keeps the old expanded-line output rather than inventing one.
390 let lmap: *LineMap = expand_ctx_enable_linemap(ctx)
391 let rc: i64 = expand_imports(ctx, path)
392 if rc < 0 {
393 nxcx_log("nx_compile_x86: expand_imports failed\n" as *u8, 40)
394 nx_diag_puts(" why the build stopped: an import line named a file that could not be read from any import root, so the unit cannot be assembled -- the line that failed is printed above by expand_imports.\n" as *u8)
395 nx_diag_puts(" fix: check the import path spelling and that the file exists under runtime/, hub/, wiki/, bin/, kernel/ or _hdl_build/ relative to the tree root you are building from; run the build from that root.\n" as *u8)
396 return 10 - rc
397 }
398 // DEFAULT-ON CRASH GUARD (v12 rung 1b-ii, 2026-08-13): append the self-contained
399 // nx_crash.nx through the SAME expander -- dedup by canonical path makes an explicit
400 // user import a no-op, and the linemap keeps attributing its lines to nx_crash.nx.
401 // Appending AFTER the user's expansion leaves every user line number untouched;
402 // parse_function injects the arming call at main entry when nx_cg_armed() is set.
403 // --no-crash-guard opts out; a missing guard source degrades LOUDLY to an unguarded
404 // build rather than failing the compile (the guard must never block shipping).
405 // The stdin path below is NOT injected (caller pre-expands; declared limit).
406 if no_guard == 0 {
407 let grc: i64 = expand_imports(ctx, "runtime/nx_crash.nx" as *u8)
408 if grc >= 0 { nx_cg_arm(1) }
409 if grc < 0 {
410 nxcx_log("nx_compile_x86: crash-guard source not found; building UNGUARDED\n" as *u8, 65)
411 nx_diag_puts(" why this matters: the crash handler is expanded after the user unit so a fault prints its address and signal instead of dying silently -- this binary will die silently.\n" as *u8)
412 nx_diag_puts(" fix: run the build from the tree root where runtime/nx_crash.nx resolves, or restore that file; the build continues so a missing guard never blocks a repair.\n" as *u8)
413 }
414 }
415 let op: *i64 = ctx.out_pos
416 let end: i64 = *op
417 expand_buf[end] = 0 as u8
418 in_buf = expand_buf
419 in_n = end
420 nx_diag_set_linemap(lmap)
421 }
422 if path_addr == 0 {
423 in_buf = sys_mmap(NX_COMPILE_X86_SRC_CAP)
424 in_n = nxcx_read_stdin(in_buf, NX_COMPILE_X86_SRC_CAP)
425 }
426 if in_n <= 0 {
427 nxcx_log("nx_compile_x86: empty input\n" as *u8, 28)
428 nx_diag_puts(" why the build stopped: the input file has no bytes, so there is nothing to compile.\n" as *u8)
429 nx_diag_puts(" fix: point the build at the source file -- an empty file usually means a copy or a write that never landed; check the path and the file size.\n" as *u8)
430 return NX_COMPILE_X86_EMPTY
431 }
432
433 // ---- 2. Lex ----
434 // DIAGNOSTICS: hand the parser the source so errors can show the offending line with a caret
435 // (additive -- an unset source means no snippet, never a wrong one).
436 nx_diag_set_source(in_buf, in_n)
437 // Token budget DERIVED from the unit itself (tokens <= bytes; lex_source re-derives
438 // from its post-macro-expansion length anyway) -- the fixed 262,144 guess this line
439 // carried until 2026-08-18 is what silently corrupted every unit larger than it.
440 let toks: *Tok = lex_source(in_buf, in_n + 2)
441 // LN24 (2026-09-02): a lexer desync is reported at the site that broke, BEFORE the parser sees a token
442 // stream that no longer means what the author wrote, in the same teaching voice as every other error
443 // here: where (file:line and a caret on the byte), why the build stopped, how it usually happens, fix.
444 // Fatal by design: nothing after the break is the program, and every later message would name the wrong
445 // file (measured: twenty errors at nx_crash.nx:36-99 for a NUL that sat in a gate source).
446 if lex_err_get_kind() != 0 {
447 nx_diag_at(lex_err_get_line())
448 if lex_err_get_kind() == 1 {
449 nx_diag_puts(": a control byte (decimal " as *u8); nx_put_dec_err(lex_err_get_byte())
450 nx_diag_puts(") sits in the source here, outside any string literal.\n" as *u8)
451 nx_diag_caret(lex_err_get_line(), lex_err_get_col())
452 nx_diag_puts(" why the build stopped: source is text. Only tab, newline and carriage return may appear outside a string literal; a control byte here is a byte no editor shows, so every token after it would be read from a file you cannot see, and the errors that followed would name the wrong place.\n" as *u8)
453 nx_diag_puts(" how it usually gets there: a patch tool or a shell heredoc wrote raw bytes (an escape that collapsed into the byte it named), or binary content was pasted as text.\n" as *u8)
454 nx_diag_puts(" fix: open the file in a hex view at this line and column, delete the control byte -- or, if the byte is meant, spell it as an escape inside a string literal -- then build again.\n" as *u8)
455 }
456 if lex_err_get_kind() == 2 {
457 nx_diag_puts(": the string literal that opens at this column is never closed.\n" as *u8)
458 nx_diag_caret(lex_err_get_line(), lex_err_get_col())
459 nx_diag_puts(" why the build stopped: the lexer reached the end of the unit, or a NUL byte inside the literal, without finding the closing quote, so every token after this point would have been swallowed into the string and the program would not mean what you wrote.\n" as *u8)
460 nx_diag_puts(" fix: put the closing quote where the literal should end. A quote, newline or NUL that belongs INSIDE the literal is written as an escape (backslash-quote, backslash-n, backslash-zero). If you did not put a NUL here, a tool wrote raw bytes into the file -- check it in a hex view.\n" as *u8)
461 }
462 if lex_err_get_kind() == 3 {
463 nx_diag_puts(": this integer literal does not fit in 64 bits.\n" as *u8)
464 nx_diag_caret(lex_err_get_line(), lex_err_get_col())
465 nx_diag_puts(" why the build stopped: 64 bits hold at most 18446744073709551615 (a decimal in [2^63, 2^64) is taken as the unsigned pattern). The digits here need a 65th bit, and a literal that silently wrapped to a different number would be a wrong answer compiled without complaint (that is what happened before 2026-09-03).\n" as *u8)
466 nx_diag_puts(" fix: if a floating value was meant, give it a decimal point or an exponent (1e20); if a bit pattern was meant, write it in hex (0xFFFFFFFFFFFFFFFF); a value wider than 64 bits needs two words.\n" as *u8)
467 }
468 if lex_err_get_kind() == 4 {
469 nx_diag_puts(": the exponent of this number has no digits.\n" as *u8)
470 nx_diag_caret(lex_err_get_line(), lex_err_get_col())
471 nx_diag_puts(" why the build stopped: after e or E the lexer expects an optional sign and then at least one digit (1e9, 2.5E-3, 6.02e+23); it found a sign with nothing after it.\n" as *u8)
472 nx_diag_puts(" fix: complete the exponent, or put a space or an operator between the number and the name that follows it.\n" as *u8)
473 }
474 return NX_COMPILE_X86_LEX_FAIL
475 }
476 // LN41 (2026-09-03): AN UNCLOSED BRACE IS REPORTED WHERE IT WAS OPENED.
477 // Measured: 40 programs in the tree refused with "I do not know the name K_MAGIC_65536", a name that IS
478 // declared -- on line 2 of nx_crash.nx, the file the builder APPENDS to every program. The real defect was
479 // a single unclosed `{` in the program itself: everything after it parses INSIDE a function, so the
480 // appended tail module-level `const` reads as an assignment and the cascade is attributed to a file the
481 // author never wrote. ★★★★★★A STRUCTURAL BREAK REPORTED AT THE PLACE IT SURFACES SENDS EVERY READER TO A
482 // FILE THAT IS NOT THE PROBLEM -- the token stream knows exactly where the brace was opened, so say that.
483 // Runs on the TOKEN STREAM (never on raw bytes), so braces inside strings and comments cannot confuse it.
484 var lc_depth: i64 = 0
485 var lc_open_line: i64 = 0
486 var lc_open_col: i64 = 0
487 var lc_have_open: i64 = 0
488 var lc_extra_line: i64 = 0
489 var lc_ti: i64 = 0
490 var lc_go: i64 = 1
491 while lc_go == 1 {
492 let lc_t: *Tok = tok_at(toks, lc_ti)
493 if lc_t.kind == TK_EOF { lc_go = 0 } else {
494 if lc_t.kind == TK_LBRACE {
495 // remember the OUTERMOST still-open brace: that is the one whose closer is missing.
496 if lc_depth == 0 { lc_open_line = lc_t.line; lc_open_col = lc_t.col; lc_have_open = 1 }
497 lc_depth = lc_depth + 1
498 }
499 if lc_t.kind == TK_RBRACE {
500 lc_depth = lc_depth - 1
501 if lc_depth < 0 { if lc_extra_line == 0 { lc_extra_line = lc_t.line } }
502 }
503 lc_ti = lc_ti + 1
504 }
505 }
506 if lc_depth > 0 {
507 if lc_have_open == 1 { nx_diag_at(lc_open_line) } else { nx_diag_at(1) }
508 nx_diag_puts(": this block is opened here and never closed.\n" as *u8)
509 if lc_have_open == 1 { nx_diag_caret(lc_open_line, lc_open_col) }
510 nx_diag_puts(" why the build stopped: the unit ends with " as *u8)
511 nx_put_dec_err(lc_depth)
512 nx_diag_puts(" more opening brace(s) than closing ones, so every declaration after this point is parsed INSIDE this block. The first error you would otherwise see is a module-level const or func in a LATER file -- often the crash handler the builder appends to every program -- reported as if that file were wrong. It is not: this brace is.\n" as *u8)
513 nx_diag_puts(" fix: close this block. If it looks closed, a brace inside a string or comment is not a brace -- this check reads the token stream, so the count above is the real one.\n" as *u8)
514 return NX_COMPILE_X86_LEX_FAIL
515 }
516 if lc_depth < 0 {
517 nx_diag_at(lc_extra_line)
518 nx_diag_puts(": this closing brace has no matching open.\n" as *u8)
519 nx_diag_puts(" why the build stopped: a stray closer ends a block that was never started, so everything after it is parsed at the wrong nesting level and the errors surface far from here.\n" as *u8)
520 nx_diag_puts(" fix: delete this brace, or add the opening one it was meant to close.\n" as *u8)
521 return NX_COMPILE_X86_LEX_FAIL
522 }
523 if toks == (0 as *Tok) {
524 nxcx_log("nx_compile_x86: lex_source failed\n" as *u8, 35)
525 nx_diag_puts(" why the build stopped: the tokenizer returned no token stream and recorded no desync, so it ran out of memory or was handed a unit larger than the pool it derived for it -- nothing after this point is the program.\n" as *u8)
526 nx_diag_puts(" fix: split the unit or shrink the file; if it repeats on a small file the tokenizer itself is broken -- rebuild the toolchain from the last banked compiler (knowledge/bank) and report it.\n" as *u8)
527 return NX_COMPILE_X86_LEX_FAIL
528 }
529
530 // ---- 3. Parse ----
531 let m: *Module = parse_module(toks, 0 as *Module)
532 if m == (0 as *Module) {
533 nxcx_log("nx_compile_x86: parse_module failed\n" as *u8, 37)
534 nx_diag_puts(" why the build stopped: the parser gave up before it could build a module -- the errors printed above are the cause; this line only names the stage that stopped.\n" as *u8)
535 nx_diag_puts(" fix: read the FIRST error above (it names the file and line with a caret), fix that one, and build again -- later errors are usually its consequences.\n" as *u8)
536 return NX_COMPILE_X86_PARSE_FAIL
537 }
538 if m.n_functions <= 0 {
539 nxcx_log("nx_compile_x86: no functions parsed\n" as *u8, 37)
540 nx_diag_puts(" why the build stopped: a unit with no function has nothing to run -- main is the entry the emitted ELF starts at, and nothing here declared one.\n" as *u8)
541 nx_diag_puts(" fix: declare func main() -> i64 in the file you are compiling, or point the build at the file that has it (a library is compiled as part of the program that imports it, never alone).\n" as *u8)
542 return NX_COMPILE_X86_NO_FN
543 }
544
545 // ---- 3b. Post-parse IR validation (log-only, mirrors nx_nxc.nx) ----
546 //
547 // V4 (every block ends in a terminator) is the prevention gate for
548 // the unterminated-final-block class: parse_function used to leave a
549 // body's last open block with no OP_RETURN, and the emitter's
550 // creation-order layout made it FALL THROUGH into a sibling block
551 // (nx_ttt_evaluate draw-detect clobber, found 2026-06-09). Log-only
552 // until the validator earns abort authority, same policy as nx_nxc.
553 let v_post_parse: i64 = ir_validate_module(m, "post-parse" as *u8)
554 if v_post_parse > 0 {
555 nxcx_log("nx_compile_x86: post-parse validator found violations (see above)\n" as *u8, 66)
556 nx_diag_puts(" why this matters: the IR validator found a block shape the emitter does not promise to lay out correctly (an unterminated block once fell through into its sibling); today this is log-only and the build continues.\n" as *u8)
557 nx_diag_puts(" fix: read the violations above -- each names the function and block; simplify the construct that produced it (a body whose last statement is not a return or a branch is the usual cause) and report it if the shape looks legal.\n" as *u8)
558 }
559
560 // ---- 3c. Whole-program reachability (B1 of /compare/toolchain, 2026-08-18) ----
561 //
562 // Mark every function reachable from main (calls + address-taken, see opt_module_dce_mark);
563 // everything else is DEAD for this program and is neither optimised nor emitted. Measured
564 // before this rung: a trivial `main(){return 0}` importing nx_syscalls emitted 94 functions /
565 // 5,219 asm lines, and opt+emit were 48% of a typical gate's compile. The map is consulted
566 // by the opt loop below and by x86ctx_emit_module_live; nothing in the IR is rewritten.
567 // --no-dce marks everything live (the pre-B1 pipeline, byte-for-byte). The line on stderr is
568 // deliberate: a pass that only speaks on failure is indistinguishable from one that was
569 // never compiled in, and it doubles as the "which compiler am I running" probe.
570 let dce_live: *u8 = sys_mmap(m.n_functions + 16)
571 var dce_pruned: i64 = 0
572 if no_dce == 0 {
573 dce_pruned = opt_module_dce_mark(m, dce_live)
574 }
575 if no_dce == 1 {
576 var dli: i64 = 0
577 while dli < m.n_functions { dce_live[dli] = 1 as u8; dli = dli + 1 }
578 }
579 nxcx_log("nx_dce: functions=" as *u8, 18)
580 nx_put_dec_err(m.n_functions)
581 nxcx_log(" live=" as *u8, 6)
582 nx_put_dec_err(m.n_functions - dce_pruned)
583 nxcx_log(" pruned=" as *u8, 8)
584 nx_put_dec_err(dce_pruned)
585 if no_dce == 1 { nxcx_log(" (--no-dce)" as *u8, 11) }
586 nxcx_log("
587" as *u8, 1)
588
589 // ---- 4. Run opt on each function ----
590 //
591 // Enabled: the curated opt_run fixpoint (mem2reg + sccp + const-fold +
592 // copyprop + cse + gvn + licm + alloca_const + dce + thread_jumps +
593 // tail_call + strength_reduce + dse + block_merge + reassoc + sweep).
594 // mem2reg promotes alloca'd loop/state vars to SSA -> the dominant
595 // crypto-loop reload cost. Gated by the self-host gauntlet (byte-stable
596 // 2nd-gen fixpoint + differential KATs + register-survival KAT).
597 var fi: i64 = 0
598 while fi < m.n_functions {
599 let fn_base: i64 = m.functions as i64
600 let f: *Function = (fn_base + fi * 176) as *Function
601 if dce_live[fi] == 1 { opt_run(f) }
602 fi = fi + 1
603 }
604
605 // ---- 4a. Single-block leaf inlining (module pass) ----
606 //
607 // Reuses the shared opt_inline_module organ (nx_opt.nx), run HERE -- after
608 // per-function opt so callee bodies are clean SSA; the older nxc.nx front-
609 // ends run it PRE-opt where it almost never fires. When it fires we re-run
610 // opt_run so copyprop/const-fold/DCE clean the spliced bodies in the
611 // caller's context. Closes the second half of the racing-bench spectral
612 // gap (the ~400K uninlined eval_A calls gcc inlines).
613 //
614 // OPT-IN (/tmp/nx_inline_on): opt_inline_module returns 0 by default while
615 // an nx_cc miscompile it exposes (an instr-walk in inline_call/
616 // opt_inline_module processing only its first element; root cause not yet
617 // identified) is outstanding, so this is a no-op on the normal path. See
618 // opt_inline_module's comment + the memory note.
619 let n_inlined: i64 = opt_inline_module(m)
620 if n_inlined > 0 {
621 var fj: i64 = 0
622 while fj < m.n_functions {
623 let fn_base2: i64 = m.functions as i64
624 let f2: *Function = (fn_base2 + fj * 176) as *Function
625 if dce_live[fj] == 1 { opt_run(f2) }
626 fj = fj + 1
627 }
628 }
629
630 // ---- 4b. (Optional) IR dump for bootstrap-divergence diagnosis ----
631 //
632 // If /tmp/nx_ir_dump is present, dump every function's IR to
633 // stderr in a byte-comparable format. Run the same source
634 // through qemu-RV64 + native lanes, diff the traces, pinpoint
635 // the diverging instruction. 3-step recipe:
636 // 1. touch /tmp/nx_ir_dump
637 // 2. cat repro.nx | qemu-riscv64-static _offc/nx_compile_x86.elf > /dev/null 2> /tmp/rv64.trace
638 // 3. cat repro.nx | _offc/nx_compile_x86_native.elf > /dev/null 2> /tmp/native.trace
639 // diff -u /tmp/rv64.trace /tmp/native.trace | head -30
640 let ird_marker: *u8 = "/tmp/nx_ir_dump"
641 let ird_len_p: *i64 = sys_mmap(8) as *i64
642 let ird_buf: *u8 = sys_read_file(ird_marker, ird_len_p)
643 var ird_on: i64 = ir_dump_flag
644 if (ird_buf as i64) != 0 { ird_on = 1 }
645 if ird_on == 1 {
646 var fi: i64 = 0
647 while fi < m.n_functions {
648 let fn_base: i64 = m.functions as i64
649 let f: *Function = (fn_base + fi * 176) as *Function
650 nx_ir_dump_function(f, "post-parse" as *u8)
651 fi = fi + 1
652 }
653 }
654
655 // ---- 5. Emit x86_64 asm via the IR-driven backend ----
656 let o: *OutBuf = out_new(NX_COMPILE_X86_OUT_CAP)
657 out_str(o, "# Generated by nx_compile_x86.nx via nx_x86_64_ctx\n")
658 out_str(o, " .att_syntax prefix\n")
659 x86ctx_emit_module_live(m, o, dce_live)
660
661 // ---- 6. Write asm to stdout ----
662 sys_write(1, o.buf, o.pos)
663 return NX_COMPILE_X86_OK
664}