nx_parse.nx source
↩ module page · 5705 lines · 262144 B
1// parse.nx -- NishiLang recursive-descent parser, in NishiLang.
2// 2026-04-23: full monomorphization port landed; stdlib.nx's
3// Option<T> / Result<T, E> now instantiate to distinct cached
4// Types here the same way parse.c does.
5//
6// Port of parse.c's core: precedence expression parsing, function
7// declarations, and the common statements (return, let, var, if,
8// while). Struct / enum / const / match / pattern-matching are
9// deferred to a second port turn so this file stays bounded.
10//
11// Produces IR using the emitter functions defined in ir.nx: for
12// full compilation the driver (main.nx) passes the lex output
13// here, then onto opt -> regalloc -> riscv.
14
15// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml)
16// intended_use: "NishiLang substrate parser -- self-host
17// replacement for parse.c (C anchor). All
18// substrate code goes through this parser
19// when nxc.elf builds; correctness here
20// affects every downstream primitive."
21// sil_target: SIL2 (compiler correctness; bugs here
22// manifest as miscompilation of
23// every consumer)
24// asil_target: QM
25// dal_target: DAL B (avionics-targeted substrate code
26// inherits the parser's correctness;
27// DO-178C DAL B chain depends on it)
28// iec_62304_class: NONE (substrate-tool, not deployed runtime)
29// evidence: [single_pass_no_backtrack, bounded_recursion,
30// sealed_token_kinds_TK_complete,
31// differential_check_via_bench_ir_diff_sh,
32// F16_prevention_via_self_host_gauntlet]
33// hazard_register: [bug-tape-parse-codegen-IR-divergence-F13,
34// bug-tape-F16-self-compile-reentry-hazard,
35// bug-tape-stmt-boundary-false-positive-F-meta-2,
36// bug-tape-struct-field-chain-load-elision]
37// residual_risk: "Self-compile reentry hazard (F16) is the
38// dominant residual risk. ANY change to this
39// file MUST run bench/self_host_gauntlet.sh
40// before commit -- cardinal feedback-self-
41// compile-reentry-prevention-pillar."
42// verdict: NOT_YET_EVALUATED
43
44// Shared IR layouts (Type, Value, Instr, BasicBlock, Function, ...).
45import "nx_types.nx"
46import "nx_quant.nx"
47
48// Shared Tok record + TK_* TokenKind constants.
49import "nx_lex_kinds.nx"
50
51// Real IR builders. Previously stubbed here; since the module-
52// library split, parse.nx directly emits into ir.nx's Module/Function
53// pools. Still-missing builders (load/store/alloca/br_cond) remain
54// stubbed at the bottom of this file until ir.nx grows them.
55import "nx_ir.nx"
56
57// ---- runtime helpers ----
58// sys_mmap comes from syscalls.nx (via ir.nx); no local copy.
59
60func tok_at(toks: *Tok, i: i64) -> *Tok {
61 let base: i64 = toks as i64
62 return (base + i * TOK_BYTES) as *Tok
63}
64
65// One-char marker for tracing parser dispatch. Caller picks a unique
66// character per function entry; sequence on stderr reveals the exact
67// dispatch path taken at runtime. Cheap (1 syscall, 1 byte) so we
68// can drop it on every parse_* function without bloating output.
69// Switch to a no-op (just `return 0`) once the parser is bug-free.
70func tr(c: i64) -> i64 {
71 let buf: *u8 = sys_mmap(8)
72 buf[0] = c
73 sys_write(2, buf, 1)
74 return 0
75}
76
77// Bits-up equivalent of parse.c's die(P, msg). Emits a stderr line
78// then sys_exit(2). Added 2026-05-19 with the mconsts cap bump so
79// the NishiLang self-host fails LOUDLY on parser-table overflow
80// instead of silently corrupting the heap past the mmap'd capacity.
81// Per Cardinal 12 (defensive at boundaries): the parser is the
82// boundary between input source and compiled IR; overflow there is
83// the kind of silent-corruption false-OK class [[feedback-no-false-
84// ok-substrate-honesty-audit]] explicitly names.
85func parse_die(msg: *u8, msg_len: i64) -> i64 {
86 sys_write(2, "nx_parse: " as *u8, 10)
87 sys_write(2, msg, msg_len)
88 sys_write(2, "\n" as *u8, 1)
89 sys_exit(2)
90 return 0
91}
92
93// ---- CARET + SOURCE SNIPPET (2026-08-05) ----------------------------------
94// Toks have carried line AND col all along; what the Parser never had was the SOURCE, so a
95// diagnostic could say WHERE but never SHOW it. rustc and Elm both show the offending line with a
96// caret under the column, and that is most of why their errors feel different.
97// PLUMBED ADDITIVELY: a module static the driver fills right after it reads the input, so NO
98// existing caller of parse_module changes signature, and any caller that does not set it simply
99// gets no snippet (the diagnostic still carries line/col). Degrades to exactly today's behaviour.
100// NOTE ON LINE NUMBERS: nx_cc parses PRE-EXPANDED source (imports inlined), so the line is the
101// expanded-source line -- which is the line the caret must point at to be truthful about what the
102// compiler actually read. Mapping back to per-file lines is a separate rung.
103// ---- MULTI-ERROR RECOVERY (2026-08-05) ------------------------------------
104// Every semantic diagnostic used to sys_exit(2) on the spot, so one broken
105// build reported ONE error and charged the author a full rebuild per mistake.
106// rustc and Elm report everything they can see. RECOVERY CONTRACT: a site
107// whose parser POSITION is already sane (the construct was fully consumed
108// before the verdict) calls nx_diag_note_error() and continues with a safe
109// placeholder value; parse_module then REFUSES to return the module while
110// nx_diag_nerr > 0, so recovered (poisoned) IR can NEVER reach codegen --
111// the SITES-LIVE silent-value-id-0 lesson stays intact because recovery
112// values exist only on builds that are already doomed to exit 2.
113// Parser-DESYNC sites (reserved keyword in name position, operator at
114// expression start, parse_die table overflow) stay FATAL BY DESIGN: past a
115// desync every further diagnostic would point at innocent code.
116const NX_DIAG_MAX_ERRS: i64 = 20
117static nx_diag_nerr: i64
118
119func nx_diag_note_error() -> i64 {
120 nx_diag_nerr = nx_diag_nerr + 1
121 if nx_diag_nerr >= NX_DIAG_MAX_ERRS {
122 sys_write(2, "nx_parse: too many errors; stopping here\n" as *u8, 41)
123 sys_exit(2)
124 }
125 return 0
126}
127
128static nx_diag_src: *u8
129static nx_diag_src_len: i64
130
131func nx_diag_set_source(p: *u8, n: i64) -> i64 {
132 nx_diag_src = p
133 nx_diag_src_len = n
134 return 0
135}
136
137// Print " <source line>" then " <spaces>^" under column col (1-based). Silent when the source
138// was never registered or the line cannot be found -- a caret pointing at the wrong place would be
139// worse than none.
140func nx_diag_caret(line: i64, col: i64) -> i64 {
141 if (nx_diag_src as i64) == 0 { return 0 }
142 if nx_diag_src_len <= 0 { return 0 }
143 if line <= 0 { return 0 }
144 let s: *u8 = nx_diag_src
145 var start: i64 = 0 - 1
146 if line == 1 { start = 0 }
147 var i: i64 = 0
148 var cur: i64 = 1
149 while i < nx_diag_src_len {
150 if s[i] == (10 as u8) {
151 cur = cur + 1
152 if cur == line { start = i + 1; i = nx_diag_src_len }
153 }
154 if i < nx_diag_src_len { i = i + 1 }
155 }
156 if start < 0 { return 0 }
157 var e: i64 = start
158 var go: i64 = 1
159 while go == 1 {
160 if e >= nx_diag_src_len { go = 0 } else {
161 if s[e] == (10 as u8) { go = 0 } else { e = e + 1 }
162 }
163 }
164 var n: i64 = e - start
165 if n > 200 { n = 200 }
166 if n <= 0 { return 0 }
167 sys_write(2, " " as *u8, 2)
168 sys_write(2, ((s as i64) + start) as *u8, n)
169 sys_write(2, "\n " as *u8, 3)
170 var c: i64 = 1
171 while c < col {
172 var ch: *u8 = " " as *u8
173 if c - 1 < n { if s[start + c - 1] == (9 as u8) { ch = "\t" as *u8 } }
174 sys_write(2, ch, 1)
175 c = c + 1
176 }
177 sys_write(2, "^\n" as *u8, 2)
178 return 1
179}
180
181// Print source line N indented, NO caret -- the quoting half of the caret machinery, used by
182// nx_dym_note to SHOW a suggestion's declaration instead of describing it in shorthand.
183func nx_diag_show_line(line: i64) -> i64 {
184 if (nx_diag_src as i64) == 0 { return 0 }
185 if nx_diag_src_len <= 0 { return 0 }
186 let s: *u8 = nx_diag_src
187 var cur: i64 = 1
188 var start: i64 = 0 - 1
189 if line == 1 { start = 0 }
190 var i: i64 = 0
191 while i < nx_diag_src_len {
192 if s[i] == (10 as u8) {
193 cur = cur + 1
194 if cur == line { start = i + 1; i = nx_diag_src_len }
195 }
196 if i < nx_diag_src_len { i = i + 1 }
197 }
198 if start < 0 { return 0 }
199 var e: i64 = start
200 var go: i64 = 1
201 while go == 1 {
202 if e >= nx_diag_src_len { go = 0 } else {
203 if s[e] == (10 as u8) { go = 0 } else { e = e + 1 }
204 }
205 }
206 var n: i64 = e - start
207 if n > 200 { n = 200 }
208 if n <= 0 { return 0 }
209 sys_write(2, " " as *u8, 6)
210 sys_write(2, ((s as i64) + start) as *u8, n)
211 sys_write(2, "\n" as *u8, 1)
212 return 1
213}
214
215// ---- DID-YOU-MEAN (2026-08-05) --------------------------------------------
216// rustc and Elm both answer a misspelled name with the name you meant; nx_cc used to answer
217// "call to undefined function: sys_wrte" and stop, which tells the author WHAT is wrong and
218// nothing about what to type. The names are all already in the module: this is a scan, not a
219// new index. Bounded Levenshtein (classic two-row DP) with an early length filter.
220// COST CONTROL: only runs on the ERROR path, immediately before exiting.
221func nx_dym_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
222
223// Edit distance between a (len la) and b (len lb), capped: any pair whose lengths differ by more
224// than udget is rejected without the DP. Returns a large number when over budget.
225func nx_dym_dist(a: *u8, la: i64, b: *u8, lb: i64, budget: i64) -> i64 {
226 var d: i64 = la - lb
227 if d < 0 { d = 0 - d }
228 if d > budget { return 999 }
229 if la > 64 { return 999 }
230 if lb > 64 { return 999 }
231 let prev: *u8 = sys_mmap(80)
232 let cur: *u8 = sys_mmap(80)
233 var j: i64 = 0
234 while j <= lb { prev[j] = j as u8; j = j + 1 }
235 var i: i64 = 1
236 while i <= la {
237 cur[0] = i as u8
238 var k: i64 = 1
239 while k <= lb {
240 var cost: i64 = 1
241 if a[i - 1] == b[k - 1] { cost = 0 }
242 var best: i64 = (prev[k] as i64) + 1
243 let ins: i64 = (cur[k - 1] as i64) + 1
244 if ins < best { best = ins }
245 let sub: i64 = (prev[k - 1] as i64) + cost
246 if sub < best { best = sub }
247 cur[k] = best as u8
248 k = k + 1
249 }
250 var c: i64 = 0
251 while c <= lb { prev[c] = cur[c]; c = c + 1 }
252 i = i + 1
253 }
254 return prev[lb] as i64
255}
256
257// DYM WINNER CONTEXT (2026-08-05, operator bar: "did-you-mean is the floor -- the suggestion
258// must take the reader to what the recommended spelling DOES, so an AI or a human judges by
259// CONTEXT, not naming"). The suggesters stash the winning candidate here; the diagnostic site
260// calls nx_dym_note(P) after the caret to print ONE `note:` line with the winner's declaration
261// facts. kind: 0 none, 1 function, 2 module const, 3 local, 4 struct, 5 generic type param.
262static nx_dym_last_kind: i64
263static nx_dym_last_p: *u8
264static nx_dym_last_l: i64
265static nx_dym_last_aux: i64
266
267// Print " -- did you mean 'X'?" for the closest DEFINED function name, if one is close enough.
268// Silent when nothing is near: a wrong suggestion is worse than none.
269func nx_dym_suggest_fn(m: *Module, name: *u8, nlen: i64) -> i64 {
270 if m == (0 as *Module) { return 0 }
271 var budget: i64 = 2
272 if nlen <= 4 { budget = 1 }
273 var best_d: i64 = 999
274 var best_p: *u8 = 0 as *u8
275 var best_l: i64 = 0
276 var best_np: i64 = 0
277 var i: i64 = 0
278 while i < m.n_functions {
279 let base: i64 = m.functions as i64
280 let f: *Function = (base + i * 176) as *Function
281 let fn_p: *u8 = f.name_start as *u8
282 let fn_l: i64 = f.name_len
283 if fn_l > 0 {
284 let dd: i64 = nx_dym_dist(name, nlen, fn_p, fn_l, budget)
285 if dd < best_d { best_d = dd; best_p = fn_p; best_l = fn_l; best_np = f.n_params }
286 }
287 i = i + 1
288 }
289 if best_d > budget { return 0 }
290 if best_d == 0 { return 0 }
291 if (best_p as i64) == 0 { return 0 }
292 nx_dym_last_kind = 1
293 nx_dym_last_p = best_p
294 nx_dym_last_l = best_l
295 nx_dym_last_aux = best_np
296 sys_write(2, " -- did you mean '" as *u8, 18)
297 sys_write(2, best_p, best_l)
298 sys_write(2, "'?" as *u8, 2)
299 return 1
300}
301
302// Decimal-print an i64 to stderr (for line numbers in diagnostics).
303func nx_put_dec_err(n: i64) -> i64 {
304 if n == 0 { sys_write(2, "0" as *u8, 1) return 0 }
305 var m: i64 = n
306 if m < 0 { sys_write(2, "-" as *u8, 1) m = 0 - m }
307 let digits: *u8 = sys_mmap(24)
308 var k: i64 = 0
309 while m > 0 {
310 digits[k] = (0x30 + (m % 10)) as u8
311 m = m / 10
312 k = k + 1
313 }
314 var i: i64 = k - 1
315 while i >= 0 {
316 let one: *u8 = sys_mmap(1)
317 one[0] = digits[i]
318 sys_write(2, one, 1)
319 i = i - 1
320 }
321 return 0
322}
323
324// Print one token's inline text to stderr (printable bytes only, capped).
325func nx_put_tok_text_err(t: *Tok) -> i64 {
326 let txt: *u8 = tok_text_ptr(t)
327 let one: *u8 = sys_mmap(1)
328 var k: i64 = 0
329 var stop: i64 = 0
330 while stop == 0 {
331 let c: i64 = txt[k] as i64
332 if c < 0x20 { stop = 1 }
333 if c > 0x7E { stop = 1 }
334 if stop == 0 {
335 one[0] = txt[k] as u8
336 sys_write(2, one, 1)
337 k = k + 1
338 }
339 if k >= 40 { stop = 1 }
340 }
341 if k == 0 { sys_write(2, "." as *u8, 1) } // punctuation w/ empty inline text
342 return 0
343}
344
345// Diagnostic context: print a window of source tokens around `name_tok`
346// so the offending statement is visible even before full snippet+caret
347// (NDX-1 spec interim). Token text is inline in each Tok, so this works
348// without threading the source buffer through the parser.
349func nx_diag_token_window(P: *Parser, tidx: i64) -> i64 {
350 var ci: i64 = tidx - 8
351 if ci < 0 { ci = 0 }
352 sys_write(2, " near: " as *u8, 8)
353 while ci <= tidx + 2 {
354 let ct: *Tok = tok_at(P.toks, ci)
355 nx_put_tok_text_err(ct)
356 sys_write(2, " " as *u8, 1)
357 ci = ci + 1
358 }
359 sys_write(2, "\n" as *u8, 1)
360 return 0
361}
362
363// ---- Parser state ----
364
365struct Local {
366 // Name stored as first 8 chars in text0..text7 (matches Tok layout).
367 name0: i64, name1: i64, name2: i64, name3: i64,
368 name4: i64, name5: i64, name6: i64, name7: i64,
369 value_id: i64,
370 is_alloca: i64,
371 ty_kind: i64, // 5 = i64, etc. (legacy fast tag)
372 // Full type carried so parse_field_chain can pick the right
373 // pointer stride. *u8 indexing must use stride 1, not the
374 // default i64 stride 8. Closes T#types-001's parser side.
375 ty: *Type,
376}
377
378const LOCAL_BYTES: i64 = 96
379
380// Per-function locals pool capacity. Reset to 0 per function, so this is
381// the MAX simultaneous locals any single function may declare. Was 64 --
382// missed when the mconsts pool was bumped off the C-bootstrap limit
383// (2026-05-20). 64 silently overflowed the locals pool into the ADJACENT
384// globals pool (consecutive anonymous mmaps share a boundary), corrupting
385// m.globals/m.n_globals -> garbage .rodata emission, for any function with
386// >64 locals (e.g. the ACME issuance drive's main). Bumped to 8192 (the
387// daily-driver cap belongs to NishiLang, not the bootstrap) + add_local now
388// die()s loudly on exhaustion instead of corrupting silently.
389const NX_PARSE_LOCALS_CAP: i64 = 8192
390
391struct Parser {
392 toks: *Tok,
393 pos: i64,
394 module: *Module, // parent module; owns Functions and Globals
395 current_fn: *Function,
396 current_bb: *BasicBlock,
397 locals: *Local,
398 n_locals: i64,
399 // First local index of the INNERMOST block. A redeclaration is only a duplicate within
400 // THIS block; shadowing an enclosing scope is legal and the estate relies on it.
401 block_base: i64,
402
403 // Innermost loop context
404 loop_head: *BasicBlock,
405 loop_exit: *BasicBlock,
406
407 // Module-level integer constants. `const NAME: i64 = N` registers
408 // here; parse_primary consults this list when an identifier misses
409 // the local table so const uses inside functions fold to literals.
410 mconsts: *MConst,
411 n_mconsts: i64,
412
413 // Module-level struct types. Each TY_STRUCT we parse is registered
414 // here so parse_type can resolve user struct names to their layout.
415 // STUB(parser, T#parser-002): pool capped at 64 entries.
416 // Plan: grow dynamically (mirror globals-pool growth in
417 // ir_module_new) + bounds-check in register_struct.
418 // Closes when: any nxc.nx self-compile with >64 struct decls.
419 structs: *StructEntry,
420 n_structs: i64,
421
422 // Module-level `static` declarations. Each maps an inline-name
423 // copy + its global id. On parse_function entry we inject one
424 // synthesised Local per static with a VAL_GLOBAL_ADDR Value, so
425 // use sites inside functions lower via the standard local-lookup
426 // path (is_alloca=1 => auto-load / indexed write / field chain).
427 statics: *StaticEntry,
428 n_statics: i64,
429
430 // Module-level enums. Tracks per-enum whether any variant has a
431 // payload: tagged enums get an auto-generated shadow struct
432 // (tag + payload fields) so constructors + match can build and
433 // deconstruct the ADT. Plain enums stay as int discriminants.
434 enums: *EnumEntry,
435 n_enums: i64,
436
437 // Active generic type parameters. Populated while parsing
438 // inside `struct Name<T, U> { ... }` so that parse_type can
439 // detect an identifier like `T` and produce a TY_PARAM node.
440 // Cleared after the struct's field list finishes. Static cap
441 // matches parse.c's args[8] limit.
442 active_params: *i64, // array of *u8 name ptrs
443 n_active_params: i64,
444
445 // V-LANGEXT M2 hygiene guard: depth of currently-parsing nested
446 // if-then-else expressions. Incremented at parse-time entry; checked
447 // against NX_PARSE_IFEXP_MAX_DEPTH; rejected if exceeded. Prevents
448 // pathological codegen from any source that nests if-expressions
449 // beyond what real-world code ever needs (e.g., 16 is already deeper
450 // than any human-readable nested ternary -- C `?:` chains are
451 // similarly capped by readability + most linters).
452 // Per [[feedback-nishilang-extension-guardrails-2026-05-27]] +
453 // operator 2026-05-27: "we want to avoid hoisting issues and
454 // namespace issues and all the other hygiene issues that make
455 // languages suck, really research and make sure we are adding good
456 // functionality not future nightmares".
457 ifexp_depth: i64,
458}
459
460// One entry per `enum NAME { ... }`. The shadow_ty is non-null only
461// when any variant declared a payload type -- plain enums keep the
462// int-discriminant representation and leave shadow_ty = null.
463struct EnumEntry {
464 name0: i64, name1: i64, name2: i64, name3: i64,
465 name4: i64, name5: i64, name6: i64, name7: i64,
466 name_len: i64,
467 has_payload: i64,
468 shadow_ty: *Type,
469 n_variants: i64,
470}
471
472const ENUM_ENTRY_BYTES: i64 = 96
473
474struct StaticEntry {
475 name0: i64, name1: i64, name2: i64, name3: i64,
476 name4: i64, name5: i64, name6: i64, name7: i64,
477 name_len: i64,
478 global_id: i64,
479 ty: *Type,
480}
481
482const STATIC_ENTRY_BYTES: i64 = 88
483
484// One entry in the Parser's struct lookup table. Keeps the Type
485// pointer (owned by ir.nx) plus an inline name copy so the name
486// survives past the lexer's token buffer lifetime.
487struct StructEntry {
488 name0: i64, name1: i64, name2: i64, name3: i64,
489 name4: i64, name5: i64, name6: i64, name7: i64,
490 name_len: i64,
491 ty: *Type,
492}
493
494const STRUCT_ENTRY_BYTES: i64 = 80
495
496struct MConst {
497 // First 64 bytes = name (null-terminated in place, matches Local
498 // layout so we can reuse the name-copy helpers).
499 name0: i64, name1: i64, name2: i64, name3: i64,
500 name4: i64, name5: i64, name6: i64, name7: i64,
501 val: i64,
502 // V-LANGEXT M1 (NishiLang self-host): string-literal in module-
503 // const declarations per [[feedback-nishilang-extension-guardrails-
504 // 2026-05-27]]. kind discriminates int (0) vs string (1); for
505 // strings, gid is the ir_add_global_string id + ty_ptr is the
506 // pointer-type cast.
507 kind: i64,
508 gid: i64,
509 ty_ptr: i64,
510}
511
512const MCONST_BYTES: i64 = 96
513
514// ---- token ops ----
515
516func peek_kind(P: *Parser) -> i64 {
517 let t: *Tok = tok_at(P.toks, P.pos)
518 return t.kind
519}
520
521// Statement-boundary detection for precedence-climbers.
522//
523// NishiLang allows optional semicolons. Without them, tokens like
524// `*`, `-`, and `&` are ambiguous: binary operators OR unary
525// operators starting a fresh statement (`*p`, `-x`, `&v`). Ports
526// parse.c's at_stmt_boundary (parse.c line 387): if the current
527// token is on a NEWER source line than the token just consumed,
528// treat it as a statement boundary and break out of the precedence
529// climber.
530//
531// Class-level fix for F-meta-2 (optional-token statement boundary
532// ambiguity). Without this, the regression_no_semis_optional case
533// parses `sys_mmap(16) as *i64\n*p = 100` as
534// `(sys_mmap(16) as *i64) * p = 100` -- corrupts the store + the
535// downstream load. See docs/NISHI_BUG_PREVENTION_PILLARS.md.
536//
537// Pillar 4 alignment: this is ADDITIVE -- it adds a guard at each
538// precedence-climber loop top without forbidding any valid source.
539// Code that explicitly continues across a newline can still do so
540// by parenthesising: `( a\n + b )` keeps the binop because the
541// open-paren resets the line-boundary semantics inside the paren
542// group (no newline insertion inside `()` per the lexer).
543func at_stmt_boundary(P: *Parser) -> i64 {
544 if P.pos == 0 { return 0 }
545 let cur: *Tok = tok_at(P.toks, P.pos)
546 let prev: *Tok = tok_at(P.toks, P.pos - 1)
547 if cur.line > prev.line {
548 // F-meta-2 refinement 2026-05-16: a newline is a statement
549 // boundary ONLY when the next token COULD start a statement.
550 // Always-binary operators (|, ^, /, %, ==, !=, <=, >=, <<,
551 // >>, &&, ||, =) can never appear at statement start in
552 // NishiLang, so when they follow a newline they're an
553 // expression continuation, not a new statement. This
554 // unblocks multi-line bitwise expressions like the pack4_i16
555 // body `return (a & 0xFFFF) | ((b & 0xFFFF) << 16) | ...`
556 // which previously dropped every term past the first
557 // because each `|` started on a new line and tripped the
558 // boundary check.
559 //
560 // Ambiguous tokens (*, -, &, +) STILL trigger the boundary
561 // -- the F-meta-2 fix targeted those, where `*p` and `-x`
562 // and `&v` and `+x` can legitimately start a fresh
563 // statement. The original case `sys_mmap(16) as *i64\n*p =
564 // 100` continues to parse correctly.
565 //
566 // Additive PREVENT per the four-pillar cardinal: this RELAXES
567 // statement-boundary detection for tokens that COULDN'T have
568 // been false positives, but keeps it strict for the tokens
569 // where ambiguity exists. No source patterns are forbidden
570 // by this change; previously-rejected multi-line expressions
571 // now parse, previously-accepted programs are unaffected.
572 let k: i64 = cur.kind
573 // Bitwise (TK_PIPE=56, TK_CARET=57) -- always binary.
574 if k == 56 { return 0 }
575 if k == 57 { return 0 }
576 // Mul-class arith except STAR (TK_SLASH=43, TK_PERCENT=44).
577 if k == 43 { return 0 }
578 if k == 44 { return 0 }
579 // Comparison (TK_EQ=46, TK_NE=47, TK_LT=48, TK_GT=49,
580 // TK_LE=50, TK_GE=51) -- always binary. TK_LT and TK_GT
581 // could theoretically start generic-instantiation syntax,
582 // but we don't have prefix-< or prefix-> in the language.
583 if k == 46 { return 0 }
584 if k == 47 { return 0 }
585 if k == 48 { return 0 }
586 if k == 49 { return 0 }
587 if k == 50 { return 0 }
588 if k == 51 { return 0 }
589 // Logical (TK_AND_AND=52, TK_OR_OR=53) -- always binary.
590 if k == 52 { return 0 }
591 if k == 53 { return 0 }
592 // Shift (TK_SHL=59, TK_SHR=60) -- always binary.
593 if k == 59 { return 0 }
594 if k == 60 { return 0 }
595 // Assignment (TK_ASSIGN=45) -- always binary (no prefix `=`).
596 if k == 45 { return 0 }
597 return 1
598 }
599 return 0
600}
601func peek_at(P: *Parser, k: i64) -> *Tok {
602 return tok_at(P.toks, P.pos + k)
603}
604func advance_tok(P: *Parser) -> *Tok {
605 let t: *Tok = tok_at(P.toks, P.pos)
606 P.pos = P.pos + 1
607 return t
608}
609func check_kind(P: *Parser, k: i64) -> i64 {
610 if peek_kind(P) == k { return 1 }
611 return 0
612}
613func match_kind(P: *Parser, k: i64) -> i64 {
614 if peek_kind(P) == k {
615 P.pos = P.pos + 1
616 return 1
617 }
618 return 0
619}
620
621// ---- generic monomorphization helpers ----------------------------
622//
623// Port of parse.c's clone_type_substituting + instantiate_generic_n.
624// These functions realise `Option<T>` / `Result<T, E>` -- when a use
625// site references `Name<Arg1, Arg2>`, the template struct's field
626// list is walked and every TY_PARAM field that matches one of the
627// declared type-parameter names is replaced with the concrete arg.
628// The resulting monomorphic Type gets a mangled name (`Name$arg1
629// $arg2`) and is cached so repeated uses of the same instantiation
630// share one struct.
631//
632// These helpers are pure over Type graphs -- they do not read or
633// mutate Parser state beyond the struct registration cache.
634
635// Compare two byte ranges for exact equality. Same semantics as
636// strncmp == 0 when both are known-length. Returns 1 if equal.
637func ty_name_eq(a: *u8, a_len: i64, b: *u8, b_len: i64) -> i64 {
638 if a_len != b_len { return 0 }
639 var i: i64 = 0
640 while i < a_len {
641 if a[i] != b[i] { return 0 }
642 i = i + 1
643 }
644 return 1
645}
646
647// Walk a Type tree, substituting any TY_PARAM whose name matches
648// `param_name[0..param_name_len]` with `concrete`. Recurses into
649// TY_PTR. Returns the same Type pointer if no substitution
650// occurred (so caller can cheaply detect the no-op case).
651//
652// Nested TY_STRUCT templates (e.g. a field of type Option<U> inside
653// a Result<T, U>) are NOT re-walked here -- that requires ambient
654// substitution tracking which adds complexity we don't need yet.
655// The parse.c port has the same shortcut (see its comment).
656func clone_type_substituting(t: *Type, param_name: *u8,
657 param_name_len: i64,
658 concrete: *Type) -> *Type {
659 if t == (0 as *Type) { return t }
660 if t.kind == TY_PARAM {
661 if t.param_name != (0 as *u8) {
662 if ty_name_eq(t.param_name, t.param_name_len,
663 param_name, param_name_len) == 1 {
664 return concrete
665 }
666 }
667 }
668 if t.kind == TY_PTR {
669 if t.pointee != (0 as *Type) {
670 let sub: *Type = clone_type_substituting(t.pointee,
671 param_name,
672 param_name_len,
673 concrete)
674 if sub == t.pointee { return t }
675 let np: *Type = alloc_type(TY_PTR, 8, 8)
676 np.pointee = sub
677 return np
678 }
679 }
680 return t
681}
682
683// Helpers for name mangling. Build a mangled name "Base$a1$a2..."
684// into out[] and return the length written. Each arg contributes
685// "$<kind-suffix>".
686func ty_mangle_suffix(arg: *Type, out: *u8, off: i64, cap: i64) -> i64 {
687 if off >= cap { return off }
688 out[off] = 0x24 // '$'
689 var cur: i64 = off + 1
690 if arg == (0 as *Type) {
691 if cur < cap { out[cur] = 0x3F; cur = cur + 1 } // '?'
692 return cur
693 }
694 if arg.kind == TY_I64 {
695 if cur + 3 <= cap {
696 out[cur] = 0x69; out[cur+1] = 0x36; out[cur+2] = 0x34 // "i64"
697 cur = cur + 3
698 }
699 return cur
700 }
701 if arg.kind == TY_I32 {
702 if cur + 3 <= cap {
703 out[cur] = 0x69; out[cur+1] = 0x33; out[cur+2] = 0x32
704 cur = cur + 3
705 }
706 return cur
707 }
708 if arg.kind == TY_I8 {
709 if cur + 2 <= cap {
710 out[cur] = 0x69; out[cur+1] = 0x38
711 cur = cur + 2
712 }
713 return cur
714 }
715 if arg.kind == TY_BOOL {
716 if cur + 4 <= cap {
717 out[cur] = 0x62; out[cur+1] = 0x6F; out[cur+2] = 0x6F; out[cur+3] = 0x6C
718 cur = cur + 4
719 }
720 return cur
721 }
722 if arg.kind == TY_PTR {
723 if cur < cap { out[cur] = 0x70; cur = cur + 1 } // 'p'
724 return ty_mangle_suffix(arg.pointee, out, cur, cap)
725 }
726 if arg.kind == TY_STRUCT {
727 // Emit struct name bytes.
728 var i: i64 = 0
729 while i < arg.name_len {
730 if cur >= cap { break }
731 out[cur] = arg.name_bytes[i]
732 cur = cur + 1
733 i = i + 1
734 }
735 return cur
736 }
737 if arg.kind == TY_PARAM {
738 // Mangled name for an un-instantiated param: "T" etc.
739 var i: i64 = 0
740 while i < arg.param_name_len {
741 if cur >= cap { break }
742 out[cur] = arg.param_name[i]
743 cur = cur + 1
744 i = i + 1
745 }
746 return cur
747 }
748 // Fallback: '?'
749 if cur < cap { out[cur] = 0x3F; cur = cur + 1 }
750 return cur
751}
752
753// Forward decls used by instantiate_generic_n below. Real impls
754// live further down with the existing struct-registration code.
755func lookup_struct(P: *Parser, name: *u8) -> *Type;
756func register_struct(P: *Parser, name: *u8, name_len: i64,
757 ty: *Type) -> i64;
758
759// Instantiate a template struct with `n_args` concrete type args.
760// Returns the cached monomorphic Type, or a freshly-built one on
761// first use. If `tmpl` has no declared type params, returns tmpl
762// unchanged (caller paid attention to the generic syntax but there
763// was nothing to substitute).
764//
765// The cache is Parser.structs itself -- we stash every monomorph as
766// a regular struct entry keyed by the mangled name. Repeated uses
767// of `Result<i64, i64>` share one Type.
768func instantiate_generic_n(P: *Parser, tmpl: *Type,
769 args: *i64, n_args: i64) -> *Type {
770 if tmpl == (0 as *Type) { return tmpl }
771 if tmpl.n_type_params == 0 { return tmpl }
772
773 // Mangle: tmpl.name + "$arg1$arg2..."
774 let mangled: *u8 = sys_mmap(256)
775 var pos: i64 = 0
776 var i: i64 = 0
777 while i < tmpl.name_len {
778 if pos >= 256 { break }
779 mangled[pos] = tmpl.name_bytes[i]
780 pos = pos + 1
781 i = i + 1
782 }
783 var j: i64 = 0
784 while j < n_args {
785 let arg_ptr: i64 = args[j]
786 let arg: *Type = arg_ptr as *Type
787 pos = ty_mangle_suffix(arg, mangled, pos, 256)
788 j = j + 1
789 }
790
791 // Cache lookup by mangled name.
792 let cached: *Type = lookup_struct(P, mangled)
793 if cached != (0 as *Type) { return cached }
794
795 // Miss: clone the template, substituting each field's type.
796 let inst: *Type = ir_type_struct_new(mangled, pos)
797 // Register in parser cache so future use-sites hit.
798 register_struct(P, mangled, pos, inst)
799
800 var fi: i64 = 0
801 while fi < tmpl.n_fields {
802 let fbase: i64 = tmpl.fields as i64
803 let src: *StructField = (fbase + fi * 32) as *StructField
804 var fty: *Type = src.ty
805 var k: i64 = 0
806 while k < n_args {
807 if k >= tmpl.n_type_params { break }
808 let param_name_ptr: i64 = tmpl.type_params[k]
809 let param_name: *u8 = param_name_ptr as *u8
810 // Param name length: we stored each name as a NUL-
811 // terminated span, so scan to NUL for length.
812 var pn_len: i64 = 0
813 while param_name[pn_len] != 0 { pn_len = pn_len + 1 }
814 let concrete_ptr: i64 = args[k]
815 let concrete: *Type = concrete_ptr as *Type
816 fty = clone_type_substituting(fty, param_name, pn_len, concrete)
817 k = k + 1
818 }
819 ir_type_struct_add_field(inst, src.name_bytes, src.name_len, fty)
820 fi = fi + 1
821 }
822 return inst
823}
824
825// ---- type parsing (minimal: just i64/bool/void + *T) ----
826
827func parse_type(P: *Parser) -> *Type {
828 // [N]T: fixed-size stack array. N = integer literal, T = element type.
829 // size = N * elem.size; pointee = element type (reuses the PTR/ARRAY pointee slot).
830 if match_kind(P, TK_LBRACKET) {
831 // []T: SLICE -- a pointer that carries its length. Distinguished from [N]T by the
832 // absence of a size token, so the two forms cannot be confused by the parser.
833 if peek_kind(P) == TK_RBRACKET {
834 advance_tok(P)
835 let selem: *Type = parse_type(P)
836 let st: *Type = alloc_type(TY_SLICE, 8, 8)
837 st.pointee = selem
838 return st
839 }
840 let n_tok: *Tok = advance_tok(P)
841 let n: i64 = n_tok.int_val
842 match_kind(P, TK_RBRACKET)
843 let elem: *Type = parse_type(P)
844 let at: *Type = alloc_type(TY_ARRAY, n * elem.size, elem.align)
845 at.pointee = elem
846 return at
847 }
848 // func(T,...)->R: function-pointer type = 8 bytes (a code address). Params are consumed but NOT
849 // stored (MVP: no arity/param-type check); the return type is kept in `pointee`.
850 if match_kind(P, TK_FUNC) {
851 match_kind(P, TK_LPAREN)
852 if peek_kind(P) != TK_RPAREN {
853 parse_type(P)
854 while match_kind(P, TK_COMMA) { parse_type(P) }
855 }
856 match_kind(P, TK_RPAREN)
857 var fret: *Type = ir_type_void()
858 if match_kind(P, TK_ARROW) { fret = parse_type(P) }
859 let ft: *Type = alloc_type(TY_FUNC, 8, 8)
860 ft.pointee = fret
861 return ft
862 }
863 // *T: recurse into pointee, wrap in TY_PTR.
864 if match_kind(P, TK_STAR) {
865 let pointee: *Type = parse_type(P)
866 let t: *Type = alloc_type(TY_PTR, 8, 8)
867 t.pointee = pointee
868 return t
869 }
870 // Ident: primitive? active type param? user struct?
871 let tk: *Tok = advance_tok(P)
872 let name: *u8 = tok_text_ptr(tk)
873 if streq_n(name, "i64", 3) { return alloc_type(TY_I64, 8, 8) }
874 if streq_n(name, "i32", 3) { return alloc_type_s(TY_I32, 4, 4) } // signed subword -> sign-extend on load
875 if streq_n(name, "u32", 3) { return alloc_type(TY_I32, 4, 4) } // unsigned 32 -> zero-extend (default)
876 if streq_n(name, "i16", 3) { return alloc_type_s(TY_I16, 2, 2) } // signed 16 -> sign-extend on load
877 if streq_n(name, "u16", 3) { return alloc_type(TY_I16, 2, 2) } // unsigned 16 -> zero-extend
878 if streq_n(name, "i8", 2) { return alloc_type_s(TY_I8, 1, 1) } // signed 8 -> sign-extend on load
879 if streq_n(name, "u8", 2) { return alloc_type(TY_I8, 1, 1) }
880 if streq_n(name, "bool", 4) { return alloc_type(TY_BOOL, 1, 1) }
881 if streq_n(name, "void", 4) { return alloc_type(TY_VOID, 0, 1) }
882 // F-extension primitive types. Matches TY_F32/TY_F64 in types.nx.
883 // ABI: f32 is 4 bytes 4-aligned, f64 is 8 bytes 8-aligned.
884 if streq_n(name, "f32", 3) { return alloc_type(TY_F32, 4, 4) }
885 if streq_n(name, "f64", 3) { return alloc_type(TY_F64, 8, 8) }
886
887 // Active generic type parameter? Inside a `struct X<T> { ... }`
888 // body, `T` should become a TY_PARAM that later instantiation
889 // walks substitute against concrete args. Scan the parser's
890 // active-param stack -- if any entry's NUL-terminated name
891 // matches the identifier we're looking at, emit a fresh TY_PARAM.
892 var pi: i64 = 0
893 var name_len: i64 = 0
894 while name[name_len] != 0 { name_len = name_len + 1 }
895 while pi < P.n_active_params {
896 let ap_ptr: i64 = P.active_params[pi]
897 let ap: *u8 = ap_ptr as *u8
898 var ap_len: i64 = 0
899 while ap[ap_len] != 0 { ap_len = ap_len + 1 }
900 if ty_name_eq(name, name_len, ap, ap_len) == 1 {
901 let pt: *Type = alloc_type(TY_PARAM, 8, 8)
902 pt.param_name = ap
903 pt.param_name_len = ap_len
904 return pt
905 }
906 pi = pi + 1
907 }
908
909 // User struct -- linear scan over declared struct types.
910 let found: *Type = lookup_struct(P, name)
911 if found != (0 as *Type) {
912 // Generic instantiation syntax: `Name<T, ...>`. If the
913 // target struct has declared type params and we see a '<',
914 // parse the args and instantiate via instantiate_generic_n.
915 // If it has no type params, fall back to the old
916 // consume-and-return-base behaviour (still needed so legacy
917 // non-generic code parses `Option<T>` gracefully).
918 if match_kind(P, TK_LT) {
919 if found.n_type_params > 0 {
920 let args_raw: *u8 = sys_mmap(8 * 8 + 16)
921 let args: *i64 = args_raw as *i64
922 var n_args: i64 = 0
923 let a0: *Type = parse_type(P)
924 args[n_args] = a0 as i64
925 n_args = n_args + 1
926 while match_kind(P, TK_COMMA) {
927 let an: *Type = parse_type(P)
928 args[n_args] = an as i64
929 n_args = n_args + 1
930 }
931 match_kind(P, TK_GT)
932 return instantiate_generic_n(P, found, args, n_args)
933 } else {
934 parse_type(P)
935 while match_kind(P, TK_COMMA) {
936 parse_type(P)
937 }
938 match_kind(P, TK_GT)
939 }
940 }
941 return found
942 }
943 // Unknown type name. PREVENT pillar (T#nx-int-alias-size-0): this
944 // used to silently return `TY_VOID size 0`, which is CATASTROPHIC for
945 // a struct field type -- a size-0 field collapses every later field's
946 // offset, so a pointer field can land at offset 0 and be overwritten
947 // by an int field, producing a deref-of-a-small-integer SEGFAULT far
948 // from the cause. An unknown type at this point is ALWAYS a real
949 // error (missing primitive, unregistered struct, or -- the bug we
950 // fixed -- an unresolved `type` alias). Fail LOUD + name it, so the
951 // entire silent-size-0 class is impossible going forward.
952 sys_write(2, "nx_parse: unknown type name '" as *u8, 29)
953 var en: i64 = 0
954 while name[en] != 0 { en = en + 1 }
955 sys_write(2, name, en)
956 sys_write(2, "' (not a primitive, struct, or registered type alias)" as *u8, 53)
957 nx_dym_suggest_type(P, name, en)
958 sys_write(2, "\n" as *u8, 1)
959 nx_dym_note(P)
960 nx_diag_note_error()
961 // RECOVERY PLACEHOLDER: i64-shaped (size 8, align 8) so struct-offset math
962 // in the parser stays sane -- NEVER the silent size-0 collapse this check
963 // exists to prevent. The end-of-parse gate keeps it out of codegen.
964 return alloc_type(TY_I64, 8, 8)
965}
966
967// ---- local lookup (ident -> Value id) ----
968
969func parser_loc_at(P: *Parser, i: i64) -> *Local {
970 let base: i64 = P.locals as i64
971 return (base + i * LOCAL_BYTES) as *Local
972}
973
974// Copy name bytes from a *u8 buffer into a Local.
975func copy_name_into_local(L: *Local, src: *u8) -> i64 {
976 // Write first 64 bytes of name into Local's slot.
977 let base: i64 = L as i64
978 let dst: *u8 = base as *u8
979 var i: i64 = 0
980 while i < 64 {
981 dst[i] = src[i]
982 if src[i] == 0 { return 0 }
983 i = i + 1
984 }
985 return 0
986}
987
988// Compare Local's name against a *u8.
989func local_name_eq(L: *Local, name: *u8) -> i64 {
990 let base: i64 = L as i64
991 let ln: *u8 = base as *u8
992 var i: i64 = 0
993 while i < 64 {
994 if ln[i] != name[i] { return 0 }
995 if ln[i] == 0 { return 1 }
996 i = i + 1
997 }
998 return 1
999}
1000
1001// DID-YOU-MEAN for bare IDENTIFIERS (2026-08-05, climb v9 rung 2). Same contract as
1002// nx_dym_suggest_fn -- error-path only, silent when nothing is near (a wrong suggestion is
1003// worse than none), bounded Levenshtein, ZERO new indexes: every pool scanned here (the live
1004// locals window, module consts incl Enum::Variant rows, the function table) already exists.
1005// LIVES BELOW the Local/MConst struct decls: a `let x: *T` annotation resolves its type name
1006// EAGERLY (unlike a param type), so placing this next to its nx_dym_ siblings above the
1007// structs was refused with two unknown-type errors -- reported in ONE build by the
1008// multi-error recovery this same climb shipped an hour earlier.
1009func nx_dym_suggest_ident(P: *Parser, name: *u8, nlen: i64) -> i64 {
1010 var budget: i64 = 2
1011 if nlen <= 4 { budget = 1 }
1012 var best_d: i64 = 999
1013 var best_p: *u8 = 0 as *u8
1014 var best_l: i64 = 0
1015 var best_k: i64 = 0
1016 var best_aux: i64 = 0
1017 var i: i64 = 0
1018 while i < P.n_locals {
1019 let L: *Local = parser_loc_at(P, i)
1020 let lp: *u8 = (L as i64) as *u8
1021 let ll: i64 = nx_dym_len(lp)
1022 if ll > 0 {
1023 let d1: i64 = nx_dym_dist(name, nlen, lp, ll, budget)
1024 if d1 < best_d { best_d = d1; best_p = lp; best_l = ll; best_k = 3; best_aux = L.ty_kind }
1025 }
1026 i = i + 1
1027 }
1028 var mi: i64 = 0
1029 while mi < P.n_mconsts {
1030 let mc: *MConst = mconst_at(P, mi)
1031 let mp: *u8 = (mc as i64) as *u8
1032 let ml: i64 = nx_dym_len(mp)
1033 if ml > 0 {
1034 let d2: i64 = nx_dym_dist(name, nlen, mp, ml, budget)
1035 if d2 < best_d { best_d = d2; best_p = mp; best_l = ml; best_k = 2; best_aux = mc.val }
1036 }
1037 mi = mi + 1
1038 }
1039 if P.module != (0 as *Module) {
1040 var fi: i64 = 0
1041 while fi < P.module.n_functions {
1042 let fbase: i64 = P.module.functions as i64
1043 let f: *Function = (fbase + fi * 176) as *Function
1044 let fp: *u8 = f.name_start as *u8
1045 let fl: i64 = f.name_len
1046 if fl > 0 {
1047 let d3: i64 = nx_dym_dist(name, nlen, fp, fl, budget)
1048 if d3 < best_d { best_d = d3; best_p = fp; best_l = fl; best_k = 1; best_aux = f.n_params }
1049 }
1050 fi = fi + 1
1051 }
1052 }
1053 if best_d > budget { return 0 }
1054 if best_d == 0 { return 0 }
1055 if (best_p as i64) == 0 { return 0 }
1056 nx_dym_last_kind = best_k
1057 nx_dym_last_p = best_p
1058 nx_dym_last_l = best_l
1059 nx_dym_last_aux = best_aux
1060 sys_write(2, " -- did you mean '" as *u8, 18)
1061 sys_write(2, best_p, best_l)
1062 sys_write(2, "'?" as *u8, 2)
1063 return 1
1064}
1065
1066func find_local(P: *Parser, name: *u8) -> *Local {
1067 var i: i64 = P.n_locals - 1
1068 while i >= 0 {
1069 let L: *Local = parser_loc_at(P, i)
1070 if local_name_eq(L, name) { return L }
1071 i = i - 1
1072 }
1073 return 0 as *Local
1074}
1075
1076// Is
1077ame already bound in the CURRENT block? find_local scans the whole live window (every
1078// enclosing scope), which is the right answer for RESOLUTION and the wrong one for REDECLARATION.
1079func local_in_block(P: *Parser, name: *u8) -> i64 {
1080 var i: i64 = P.n_locals - 1
1081 while i >= P.block_base {
1082 let L: *Local = parser_loc_at(P, i)
1083 if local_name_eq(L, name) { return 1 }
1084 i = i - 1
1085 }
1086 return 0
1087}
1088
1089func add_local(P: *Parser, name: *u8, value_id: i64, is_alloca: i64,
1090 ty_kind: i64, ty: *Type) -> i64 {
1091 nx_assert_lt(P.n_locals, NX_PARSE_LOCALS_CAP,
1092 "add_local: locals pool exhausted (raise NX_PARSE_LOCALS_CAP)" as *u8)
1093 let L: *Local = parser_loc_at(P, P.n_locals)
1094 copy_name_into_local(L, name)
1095 L.value_id = value_id
1096 L.is_alloca = is_alloca
1097 L.ty_kind = ty_kind
1098 L.ty = ty
1099 P.n_locals = P.n_locals + 1
1100 return 0
1101}
1102
1103// ---- IR emitter stubs (real impls live in ir.nx) ----
1104//
1105// We declare them so the parser can call out; when multi-file
1106// linkage concatenates ir.nx + parse.nx they resolve to the real
1107// definitions. Listed here as forward references only; NishiLang
1108// has no separate `extern` block so we leave them unimplemented
1109// locally and rely on the concatenated build to supply them.
1110
1111// Forward uses rely on multi-file build + link-time name resolution.
1112// Since NishiLang doesn't have separate compilation yet, the actual
1113// calls happen when this file is built alongside ir.nx.
1114//
1115// For the parser to be useful it MUST be compiled with ir.nx, opt.nx,
1116// regalloc.nx, riscv.nx, lex.nx -- the full self-host bundle. The
1117// self-test in this file only exercises local logic (precedence,
1118// token consumption); runtime IR emission happens only under the
1119// bundled compile.
1120//
1121// Stub implementations below let parse.nx compile standalone for
1122// syntax verification.
1123
1124// All IR builders now live in ir.nx; no local stubs remain.
1125
1126// ---- expression parser (precedence climbing) ----
1127//
1128// Mirrors parse.c's chain. Returns the Value id for the parsed
1129// expression. Each helper consumes its level and returns.
1130
1131// Forward declarations for the mutually-recursive parse chain.
1132func parse_logical_or(P: *Parser) -> i64;
1133func parse_unary_core(P: *Parser) -> i64;
1134func parse_primary(P: *Parser) -> i64;
1135func parse_primary_ident(P: *Parser, t: *Tok) -> i64;
1136func parse_primary_qualified_variant(P: *Parser, name: *u8) -> i64;
1137func parse_primary_intrinsic(P: *Parser, name: *u8) -> i64;
1138func parse_primary_call(P: *Parser, name: *u8) -> i64;
1139func parse_primary_local_or_const(P: *Parser, name: *u8) -> i64;
1140func parse_stmt_list(P: *Parser) -> i64;
1141func parse_stmt(P: *Parser) -> i64;
1142func parse_stmt_return(P: *Parser) -> i64;
1143func parse_stmt_let(P: *Parser) -> i64;
1144func parse_stmt_var(P: *Parser) -> i64;
1145func parse_stmt_if(P: *Parser) -> i64;
1146func parse_stmt_while(P: *Parser) -> i64;
1147func parse_stmt_for(P: *Parser) -> i64;
1148func parse_stmt_break(P: *Parser) -> i64;
1149func parse_stmt_continue(P: *Parser) -> i64;
1150func parse_stmt_match(P: *Parser) -> i64;
1151func parse_stmt_star(P: *Parser) -> i64;
1152func parse_stmt_ident(P: *Parser) -> i64;
1153func parse_stmt_ident_assign(P: *Parser) -> i64;
1154func parse_stmt_ident_subscript(P: *Parser) -> i64;
1155func parse_stmt_ident_dot(P: *Parser) -> i64;
1156func parse_stmt_expr_fallback(P: *Parser) -> i64;
1157func parse_module_const(P: *Parser) -> i64;
1158func lookup_mconst(P: *Parser, name: *u8, out: *i64) -> i64;
1159func lookup_mconst_string(P: *Parser, name: *u8,
1160 out_gid: *i64, out_ty: **Type) -> i64;
1161func mconst_name_eq(mc: *MConst, name: *u8) -> i64;
1162func mconst_at(P: *Parser, i: i64) -> *MConst;
1163// V-LANGEXT M4: const-expression evaluator helpers (mutually recursive
1164// precedence-climbing chain).
1165func const_eval_expr(P: *Parser, depth: i64) -> i64;
1166func const_eval_bor(P: *Parser, depth: i64) -> i64;
1167func const_eval_bxor(P: *Parser, depth: i64) -> i64;
1168func const_eval_band(P: *Parser, depth: i64) -> i64;
1169func const_eval_shift(P: *Parser, depth: i64) -> i64;
1170func const_eval_addsub(P: *Parser, depth: i64) -> i64;
1171func const_eval_muldiv(P: *Parser, depth: i64) -> i64;
1172func const_eval_unary(P: *Parser, depth: i64) -> i64;
1173func const_eval_primary(P: *Parser, depth: i64) -> i64;
1174func mconst_at(P: *Parser, i: i64) -> *MConst;
1175func copy_name_into_mconst(mc: *MConst, src: *u8) -> i64;
1176func parse_struct_decl(P: *Parser) -> i64;
1177func parse_static_decl(P: *Parser) -> i64;
1178func parse_module_enum(P: *Parser) -> i64;
1179func inject_statics(P: *Parser) -> i64;
1180func enum_entry_at(P: *Parser, i: i64) -> *EnumEntry;
1181func lookup_enum(P: *Parser, name: *u8) -> *EnumEntry;
1182// (lookup_struct forward-decl lives above parse_type since parse_type uses it.)
1183func struct_entry_at(P: *Parser, i: i64) -> *StructEntry;
1184
1185func parse_expr(P: *Parser) -> i64 {
1186 nx_assert_ptr(P.current_bb as *u8, "parse_expr: bb" as *u8)
1187 let cond: i64 = parse_logical_or(P)
1188 if peek_kind(P) != TK_QUESTION { return cond }
1189 // C-style ternary `cond ? a : b`. Desugars to the SAME control-flow
1190 // the `if..then..else` expression uses (alloca slot + br_cond + per-
1191 // branch store + merge load), so only the chosen branch evaluates.
1192 advance_tok(P) // eat '?'
1193 let result_ty: *Type = ir_type_i64()
1194 let result_addr: i64 = ir_emit_alloca(P.current_bb, result_ty)
1195 // ARITY FIX 2026-07-20: these passed a block-label string to ir_block_new, which takes ONE
1196 // argument (nx_ir.nx:269) and whose BasicBlock has no name field -- the label was SILENTLY
1197 // DISCARDED for as long as this code has existed. Found the moment the new call-arity check
1198 // went in; the labels are kept here as comments so the intent survives the fix.
1199 let then_bb: *BasicBlock = ir_block_new(P.current_fn) // "ternary_then"
1200 let else_bb: *BasicBlock = ir_block_new(P.current_fn) // "ternary_else"
1201 let merge_bb: *BasicBlock = ir_block_new(P.current_fn) // "ternary_merge"
1202 ir_emit_br_cond(P.current_bb, cond, then_bb, else_bb)
1203 P.current_bb = then_bb
1204 let then_v: i64 = parse_expr(P)
1205 ir_emit_store(P.current_bb, result_addr, then_v, result_ty)
1206 ir_emit_br(P.current_bb, merge_bb)
1207 if peek_kind(P) != TK_COLON {
1208 parse_die("ternary needs colon" as *u8, 19)
1209 }
1210 advance_tok(P) // eat ':'
1211 P.current_bb = else_bb
1212 let else_v: i64 = parse_expr(P)
1213 ir_emit_store(P.current_bb, result_addr, else_v, result_ty)
1214 ir_emit_br(P.current_bb, merge_bb)
1215 P.current_bb = merge_bb
1216 return ir_emit_load(P.current_bb, result_addr, result_ty)
1217}
1218
1219// Line-tagged wrapper for every ir_const_i64 call in parse.nx.
1220// When the assertion fires, the tag identifies the exact callsite.
1221// Required because vanilla ir_const_i64's assert message can't tell
1222// us which of ~25 callers passed a bad P.current_fn. Purely
1223// diagnostic; when the root-cause of the sys_read_file stage-2 bug
1224// is understood and fixed, the wrapper can retire.
1225func safe_const_i64(P: *Parser, val: i64, tag: *u8) -> i64 {
1226 nx_assert_ptr(P.current_fn as *u8, tag)
1227 nx_assert(P.current_fn.values_cap > 0, tag)
1228 return ir_const_i64(P.current_fn, val)
1229}
1230
1231// ===================== SPATIAL SAFETY: runtime bounds checking =====================
1232// These consts are read ONLY by the two functions immediately below them. They sit HERE, not
1233// at the use sites, because a NishiLang function defined textually ABOVE a const it reads
1234// silently resolves that const to garbage (reference-nishilang-nx-cc-gotchas, the fwd-const class).
1235//
1236// NX_BOUNDS_CHECK_LIVE is the kill switch. Set to 0 and every injection below vanishes, which
1237// is how the A/B overhead measurement is taken -- same source, same compiler, one const flipped.
1238const NX_BOUNDS_CHECK_LIVE: i64 = 1
1239// Exit code the injected trap raises. Distinct from the parser's own die code (2) and from the
1240// 128+signal range, so a bounds abort is unambiguous in a gate's exit status.
1241const NX_TRAP_BOUNDS: i64 = 71
1242// RV64 asm-generic syscall numbers. The x86_64 backend translates const-numbered syscalls
1243// (x86ctx_rv64_to_x86_64_syscall), so emitting RV64 form here keeps the injected IR
1244// target-agnostic -- the same instructions are correct on both backends.
1245const NX_RV64_SYS_WRITE: i64 = 64
1246const NX_RV64_SYS_EXIT_GROUP: i64 = 94
1247
1248// One-per-module cache for the trap message global. ir_add_global_string does NOT intern:
1249// every call burns a slot in a FIXED-capacity pool that asserts "globals pool exhausted" when
1250// full. Emitting the message per check site cost 8 copies of a 220-byte string in a 60-line
1251// probe, so a module with heavy fixed-array use could have exhausted the pool -- a regression
1252// the check itself would have caused. Storing gid+1 lets 0 mean "unset" without colliding
1253// with the legitimate global id 0. The module pointer is cached alongside so the id is
1254// invalidated rather than reused if a single process ever compiles more than one Module.
1255static g_bchk_msg_gid1: i64
1256static g_bchk_msg_mod: i64
1257
1258// Emit the abort sequence into P.current_bb: a diagnostic on stderr, then exit_group.
1259// Kept separate from emit_bounds_check so each function stays small and single-purpose.
1260// Does NOT emit a terminator -- the caller owns block termination.
1261func emit_bounds_trap(P: *Parser) -> i64 {
1262 // One trap serves both [N]T and []T, so the wording must not name only one of them -- it said
1263 // "fixed array [N]T" and was therefore actively wrong on the slice path, which is the path
1264 // that covers heap memory and so the one most violations will come through.
1265 let msg: *u8 = "nx: FATAL bounds violation: index outside the bounds of a fixed array [N]T or slice []T. The access was REFUSED and the process aborted rather than reading or writing past the end. Clamp the index, or size the array / slice length to match the memory it describes.\n" as *u8
1266 var mlen: i64 = 0
1267 while msg[mlen] != 0 { mlen = mlen + 1 }
1268 let modv: i64 = P.module as i64
1269 if g_bchk_msg_mod != modv { g_bchk_msg_gid1 = 0 }
1270 var gid: i64 = g_bchk_msg_gid1 - 1
1271 if g_bchk_msg_gid1 == 0 {
1272 gid = ir_add_global_string(P.module, msg, mlen)
1273 g_bchk_msg_gid1 = gid + 1
1274 g_bchk_msg_mod = modv
1275 }
1276 let u8ty: *Type = alloc_type(TY_I8, 1, 1)
1277 let pt: *Type = alloc_type(TY_PTR, 8, 8)
1278 pt.pointee = u8ty
1279 let msg_v: i64 = ir_global_value(P.current_fn, gid, pt)
1280 let raw: *u8 = sys_mmap(8 * 8 + 16)
1281 let sargs: *i64 = raw as *i64
1282 // write(2, msg, mlen)
1283 sargs[0] = safe_const_i64(P, NX_RV64_SYS_WRITE, "parse.nx:btrap-wnum" as *u8)
1284 sargs[1] = safe_const_i64(P, 2, "parse.nx:btrap-fd" as *u8)
1285 sargs[2] = msg_v
1286 sargs[3] = safe_const_i64(P, mlen, "parse.nx:btrap-len" as *u8)
1287 ir_emit_syscall(P.current_bb, sargs, 4)
1288 // exit_group(NX_TRAP_BOUNDS) -- whole thread group, so a violation in a worker cannot be
1289 // survived by the rest of the program (sys_exit alone would only retire the calling task).
1290 sargs[0] = safe_const_i64(P, NX_RV64_SYS_EXIT_GROUP, "parse.nx:btrap-enum" as *u8)
1291 sargs[1] = safe_const_i64(P, NX_TRAP_BOUNDS, "parse.nx:btrap-code" as *u8)
1292 ir_emit_syscall(P.current_bb, sargs, 2)
1293 return 0
1294}
1295
1296// SPATIAL SAFETY (CWE-787/125), the RUNTIME half. A fixed array [N]T indexed by a NON-CONSTANT
1297// expression cannot be decided at compile time, so the check is injected into the program:
1298//
1299// cur: c1 = idx >=s 0 ; br_cond c1 -> lo_ok, fail
1300// lo_ok: c2 = idx <s nelem ; br_cond c2 -> ok, fail
1301// fail: write(2,msg) ; exit_group(71) ; br ok
1302// ok: <caller keeps emitting here>
1303//
1304// The `br ok` in fail is unreachable -- exit_group never returns -- and exists only so every
1305// block ends in a terminator, which cfg_rebuild_edges requires to resolve successors.
1306//
1307// nelem comes from the array's own TYPE, so this costs no annotations and has ZERO false
1308// positives: a rejected index provably could not have been in range. Raw *T indexing is left
1309// untouched, because a bare pointer carries no length -- that is the honest remaining gap and
1310// it needs a slice type, not a check.
1311//
1312// On return P.current_bb is the `ok` block, and the value id `idx` still dominates it.
1313// THE one bounds check. `len_v` is a VALUE id, not a number, so a fixed array (whose length is a
1314// constant folded in by the caller) and a slice (whose length is loaded from the header at run
1315// time) go through exactly this code -- there is no second implementation to drift.
1316func emit_bounds_check_v(P: *Parser, idx: i64, len_v: i64) -> i64 {
1317 if NX_BOUNDS_CHECK_LIVE == 0 { return 0 }
1318 let lo_ok: *BasicBlock = ir_block_new(P.current_fn)
1319 let fail_bb: *BasicBlock = ir_block_new(P.current_fn)
1320 let ok_bb: *BasicBlock = ir_block_new(P.current_fn)
1321 let zero_v: i64 = safe_const_i64(P, 0, "parse.nx:bchk-zero" as *u8)
1322 let c1: i64 = ir_emit_binop(P.current_bb, OP_GE_S, idx, zero_v, ir_type_i64())
1323 ir_emit_br_cond(P.current_bb, c1, lo_ok, fail_bb)
1324 P.current_bb = lo_ok
1325 let c2: i64 = ir_emit_binop(P.current_bb, OP_LT_S, idx, len_v, ir_type_i64())
1326 ir_emit_br_cond(P.current_bb, c2, ok_bb, fail_bb)
1327 P.current_bb = fail_bb
1328 emit_bounds_trap(P)
1329 ir_emit_br(P.current_bb, ok_bb)
1330 P.current_bb = ok_bb
1331 return 0
1332}
1333
1334// Fixed-array wrapper: the length is known from the TYPE, so materialise it as a constant and
1335// hand it to the one checker above.
1336func emit_bounds_check(P: *Parser, idx: i64, nelem: i64) -> i64 {
1337 if NX_BOUNDS_CHECK_LIVE == 0 { return 0 }
1338 if nelem <= 0 { return 0 }
1339 let n_v: i64 = safe_const_i64(P, nelem, "parse.nx:bchk-n" as *u8)
1340 return emit_bounds_check_v(P, idx, n_v)
1341}
1342
1343// Parse a postfix chain of `.field` and `[index]` on an existing
1344// Value id. Each field access emits GEP + LOAD using the struct's
1345// field offsets; each index emits (base + index*elem_size) + LOAD
1346// with element size driven by the current type's pointee. Returns
1347// the final Value id; loops until the next token isn't a dot or
1348// open bracket. Caller supplies an initial type hint; subsequent
1349// chains infer from the resolved field/element type.
1350//
1351// Arity ceiling for INDIRECT (fn-pointer) calls, shared by the named-local
1352// path (parse_primary_call) and the postfix field path below so the two can
1353// never drift. 23 = the IR's operand cap: op0 carries the fn-ptr and op1..op23
1354// the arguments. Was 6 (register-only codegen) until 2026-07-25, when the x86
1355// backend gained real stack args for indirect calls; the RISC-V backend fails
1356// loud past 7 until its own stack-arg arc lands.
1357const NX_FNPTR_MAX_ARGS: i64 = 23
1358func parse_field_chain(P: *Parser, base: i64, base_ty: *Type) -> i64 {
1359 nx_assert_ptr(P.current_fn as *u8, "parse_field_chain: P.current_fn" as *u8)
1360 nx_assert(P.current_fn.values_cap > 0,
1361 "parse_field_chain: P.current_fn init" as *u8)
1362 var v: i64 = base
1363 var ty: *Type = base_ty
1364 var keep_going: i64 = 1
1365 while keep_going == 1 {
1366 let k: i64 = peek_kind(P)
1367 if k == TK_DOT {
1368 advance_tok(P)
1369 let ftok: *Tok = advance_tok(P)
1370 let fname: *u8 = tok_text_ptr(ftok)
1371 var flen: i64 = 0
1372 while fname[flen] != 0 { flen = flen + 1 }
1373 // Auto-deref: if ty is *Struct, look through the pointer.
1374 var stty: *Type = ty
1375 if stty != (0 as *Type) {
1376 if stty.kind == TY_PTR {
1377 if stty.pointee != (0 as *Type) { stty = stty.pointee }
1378 }
1379 }
1380 if stty == (0 as *Type) { return v }
1381 let field: *StructField = ir_type_struct_find_field(stty, fname, flen)
1382 // T#field-chain-silent-return (KNOWN, four-pillar in progress):
1383 // when the field is not found on `stty` this returns the base
1384 // unchanged. That SILENTLY mis-compiles `call().field` when the
1385 // callee's return-type struct fields are unresolved (it yields the
1386 // POINTER -- the count_ok=0 bug in the netscope verdict layer).
1387 // A loud-fail PREVENT here was TRIED and REVERTED: it breaks the
1388 // compiler's own self-build (nx_compile_x86.nx / main.nx rely on
1389 // the silent return-base for `.toks`, a deeper type-resolution
1390 // gap). FIX today = bind call results to a typed temp before
1391 // `.field` (the idiom used everywhere). The real PREVENT needs
1392 // the return-type-struct-field resolution fixed first, THEN this
1393 // can loud-fail safely. See NISHI_DEBT_LEDGER.tsv.
1394 if field == (0 as *StructField) { return v }
1395 let off: i64 = safe_const_i64(P, field.offset, "parse.nx:LINE-field.offset" as *u8)
1396 let addr: i64 = ir_emit_gep(P.current_bb, v, off, field.ty)
1397 // Bug fix 2026-05-16 (substrate-bisect):
1398 // Don't LOAD the GEP result when the field is itself a
1399 // struct -- it's an intermediate address that the next
1400 // chain iteration will offset from. The original code
1401 // unconditionally loaded after every GEP, treating
1402 // struct-typed intermediates as pointers and producing
1403 // GEP -> LOAD -> GEP -> LOAD instead of the correct
1404 // GEP -> GEP -> LOAD. The C anchor handled this right;
1405 // this brings nx_parse.nx into parity.
1406 var is_struct_field: i64 = 0
1407 if field.ty != (0 as *Type) {
1408 if field.ty.kind == TY_STRUCT { is_struct_field = 1 }
1409 }
1410 if is_struct_field == 1 {
1411 v = addr
1412 } else {
1413 v = ir_emit_load(P.current_bb, addr, field.ty)
1414 }
1415 ty = field.ty
1416 }
1417 if k == TK_LBRACKET {
1418 advance_tok(P)
1419 let idx: i64 = parse_expr(P)
1420 match_kind(P, TK_RBRACKET)
1421 // Element size from the pointer/array pointee; default i64.
1422 var elem_sz: i64 = 8
1423 var elem_ty: *Type = ir_type_i64()
1424 var is_arr_r: i64 = 0
1425 var is_slice_r: i64 = 0
1426 if ty != (0 as *Type) {
1427 if ty.kind == TY_PTR {
1428 if ty.pointee != (0 as *Type) {
1429 elem_ty = ty.pointee
1430 if elem_ty.size > 0 { elem_sz = elem_ty.size }
1431 }
1432 }
1433 if ty.kind == TY_ARRAY {
1434 is_arr_r = 1
1435 if ty.pointee != (0 as *Type) {
1436 elem_ty = ty.pointee
1437 if elem_ty.size > 0 { elem_sz = elem_ty.size }
1438 }
1439 }
1440 if ty.kind == TY_SLICE {
1441 is_slice_r = 1
1442 if ty.pointee != (0 as *Type) {
1443 elem_ty = ty.pointee
1444 if elem_ty.size > 0 { elem_sz = elem_ty.size }
1445 }
1446 }
1447 }
1448 // SLICE READ: v is the HANDLE (address of {data,len}). Read the length FIRST, check the
1449 // index against it, and only then load the data pointer -- so the load of `data` happens
1450 // in the block that is reached only when the index is known good. After this, v holds
1451 // the data pointer and the ordinary POINTER path below computes the element address.
1452 if is_slice_r == 1 {
1453 let so_v: i64 = safe_const_i64(P, NX_SLICE_LEN_OFFSET, "parse.nx:slice-rd-off" as *u8)
1454 let sla: i64 = ir_emit_binop(P.current_bb, OP_ADD, v, so_v, ir_type_i64())
1455 let slen: i64 = ir_emit_load(P.current_bb, sla, ir_type_i64())
1456 emit_bounds_check_v(P, idx, slen)
1457 v = ir_emit_load(P.current_bb, v, ir_type_i64())
1458 }
1459 // SPATIAL SAFETY (CWE-787/125): a fixed array [N]T indexed by a COMPILE-TIME CONSTANT is bounds-
1460 // checked at COMPILE TIME -- zero runtime cost, zero false positives (both N and the index are
1461 // known). Runtime indices + raw pointers stay unchecked = the honest C-class part; only [N]T
1462 // carries a length N. Moves nx_lang_sota_census's a[2]-on-[2] probe from silent-OOB to compile error.
1463 if is_arr_r == 1 {
1464 if ty != (0 as *Type) {
1465 if elem_sz > 0 {
1466 let idxv_r: *Value = val_at(P.current_fn, idx)
1467 let nelem_r: i64 = ty.size / elem_sz
1468 if idxv_r.kind == VK_CONST_INT {
1469 if idxv_r.const_int < 0 { parse_die("fixed-array index < 0" as *u8, 21) }
1470 if idxv_r.const_int >= nelem_r { parse_die("fixed-array index >= size" as *u8, 25) }
1471 }
1472 // RUNTIME index -> inject the check (see emit_bounds_check). This is the
1473 // half the compile-time test above cannot reach, and it was the measured
1474 // gap: nx_spatial_probe's buf[6] on a [4]i64 read out of range silently.
1475 if idxv_r.kind != VK_CONST_INT { emit_bounds_check(P, idx, nelem_r) }
1476 }
1477 }
1478 }
1479 let esize: i64 = safe_const_i64(P, elem_sz, "parse.nx:LINE-elem_sz" as *u8)
1480 let off2: i64 = ir_emit_binop(P.current_bb, OP_MUL,
1481 idx, esize, ir_type_i64())
1482 // ARRAY: v is the frame ADDRESS -> GEP (leaq base + off). POINTER: v is the loaded
1483 // pointer VALUE -> OP_ADD. Both then LOAD the element.
1484 var addr2: i64 = 0
1485 if is_arr_r == 1 {
1486 addr2 = ir_emit_gep(P.current_bb, v, off2, elem_ty)
1487 } else {
1488 addr2 = ir_emit_binop(P.current_bb, OP_ADD, v, off2, ty)
1489 }
1490 // AGGREGATE element (struct/array): KEEP its ADDRESS so a following `.field` / `[j]` GEPs
1491 // off it. Loading it (the scalar/pointer path) reads the first 8 bytes as a value and then
1492 // dereferences THAT as the field base -> wild access (arr-of-struct `arr[i].x` gave 0 / crash).
1493 var elem_is_agg: i64 = 0
1494 if elem_ty != (0 as *Type) {
1495 if elem_ty.kind == TY_STRUCT { elem_is_agg = 1 }
1496 if elem_ty.kind == TY_ARRAY { elem_is_agg = 1 }
1497 }
1498 if elem_is_agg == 1 {
1499 v = addr2
1500 }
1501 if elem_is_agg == 0 {
1502 v = ir_emit_load(P.current_bb, addr2, elem_ty)
1503 }
1504 ty = elem_ty
1505 }
1506 // POSTFIX INDIRECT CALL through a fn-pointer field: `obj.fn_field(args)`.
1507 // This is a PARITY GAP, not a regression: the C anchor parse.c:1498 has
1508 // carried this branch all along, the self-hosted parser never did (debt
1509 // seq715, diagnosis corrected 2026-07-25). Without it the loop exited on
1510 // TK_LPAREN and handed back the LOADED FUNCTION ADDRESS while `(args)` was
1511 // re-parsed as a stray parenthesised expression and DISCARDED -- the method
1512 // never ran and the caller got a code-segment pointer that reads as a
1513 // plausible integer. It failed OPEN, which is why every multi-method vtable
1514 // in the tree (CLAUDE rule 6 OOP) was silently broken and nobody saw it.
1515 // ADDITIVE: gated on ty.kind == TY_FUNC, so any source that compiled before
1516 // this change takes the byte-identical path it took before.
1517 var did_call: i64 = 0
1518 if k == TK_LPAREN {
1519 var is_fnptr: i64 = 0
1520 if ty != (0 as *Type) {
1521 if ty.kind == TY_FUNC { is_fnptr = 1 }
1522 }
1523 if is_fnptr == 1 {
1524 advance_tok(P)
1525 let cargs_raw: *u8 = sys_mmap(NX_FNPTR_MAX_ARGS * 8 + 16)
1526 let cargs: *i64 = cargs_raw as *i64
1527 var cn: i64 = 0
1528 if peek_kind(P) != TK_RPAREN {
1529 let c0: i64 = parse_expr(P)
1530 cargs[cn] = c0
1531 cn = cn + 1
1532 while peek_kind(P) == TK_COMMA {
1533 advance_tok(P)
1534 if cn >= NX_FNPTR_MAX_ARGS {
1535 parse_die("postfix fn-ptr call exceeds max args (register-only indirect codegen)" as *u8, 32)
1536 }
1537 let cx: i64 = parse_expr(P)
1538 cargs[cn] = cx
1539 cn = cn + 1
1540 }
1541 }
1542 match_kind(P, TK_RPAREN)
1543 // TY_FUNC keeps its RETURN type in `pointee` (parse_type, nx_types.nx).
1544 var cret: *Type = ty.pointee
1545 if cret == (0 as *Type) { cret = ir_type_i64() }
1546 v = ir_emit_call_indirect(P.current_bb, v, cret, cargs, cn)
1547 ty = cret
1548 did_call = 1
1549 }
1550 }
1551 if k != TK_DOT {
1552 if k != TK_LBRACKET {
1553 if did_call == 0 { keep_going = 0 }
1554 }
1555 }
1556 }
1557 return v
1558}
1559
1560func parse_primary(P: *Parser) -> i64 {
1561 nx_assert_ptr(P.current_fn as *u8, "parse_primary: P.current_fn" as *u8)
1562 nx_assert(P.current_fn.values_cap > 0,
1563 "parse_primary: P.current_fn init" as *u8)
1564 let t: *Tok = tok_at(P.toks, P.pos)
1565 if t.kind == TK_INT {
1566 advance_tok(P)
1567 return safe_const_i64(P, t.int_val, "parse.nx:LINE-t.int_val" as *u8)
1568 }
1569 if t.kind == TK_FLOAT {
1570 // Default-precision (f64) literal. Convert (whole, frac_num,
1571 // frac_digits) -> IEEE 754 binary64 bit pattern. Store the
1572 // bits in const_int; attach TY_F64 to the Value so downstream
1573 // passes (regalloc f-reg partition, rv_emit_fbinop) route it
1574 // as a double.
1575 //
1576 // Default literal type is f64 -- matches Rust / C-double / Zig
1577 // and gives sketches/numerics enough precision out of the box.
1578 // Explicit f32 selection: literal suffix `1.5f32` (TK_FLOAT_F32
1579 // path below) or type-context inference at `let x: f32 = ...`
1580 // sites (handled in parse_stmt_let by re-emitting the constant
1581 // as f32 when the LHS type demands it).
1582 advance_tok(P)
1583 let bits: i64 = fp64_from_parts(t.int_val, t.text0, t.text1)
1584 let vid: i64 = safe_const_i64(P, bits, "parse.nx:LINE-bits" as *u8)
1585 let base: i64 = P.current_fn.values as i64
1586 let v: *Value = (base + vid * 48) as *Value
1587 v.ty = alloc_type(TY_F64, 8, 8)
1588 return vid
1589 }
1590 if t.kind == TK_FLOAT_F32 {
1591 // Explicit single-precision literal (`1.5f32`). Same packing
1592 // as TK_FLOAT but routes through fp32_from_parts + TY_F32.
1593 advance_tok(P)
1594 let bits32: i64 = fp32_from_parts(t.int_val, t.text0, t.text1)
1595 let vid: i64 = safe_const_i64(P, bits32, "parse.nx:LINE-bits32" as *u8)
1596 let base: i64 = P.current_fn.values as i64
1597 let v: *Value = (base + vid * 48) as *Value
1598 v.ty = alloc_type(TY_F32, 4, 4)
1599 return vid
1600 }
1601 if t.kind == TK_TRUE { advance_tok(P); return safe_const_i64(P, 1, "parse.nx:LINE-const1" as *u8) }
1602 if t.kind == TK_FALSE { advance_tok(P); return safe_const_i64(P, 0, "parse.nx:LINE-const0" as *u8) }
1603 // String literal. Registers the bytes as a Module global, then
1604 // builds a VAL_GLOBAL_ADDR Value referencing that global's id.
1605 // The backend lowers this to `la reg, .Lg<id>` (or the global's
1606 // name when one was registered -- strings are anonymous so they
1607 // fall through to the .Lg id form).
1608 // V-LANGEXT M2: if-then-else as expression.
1609 //
1610 // `if cond then expr_t else expr_f`
1611 //
1612 // Distinct from statement-form `if c { ... }` (consumed at
1613 // parse_stmt level before reaching parse_primary). Matches
1614 // Haskell / OCaml / Elm convention; substrate authors already
1615 // use this syntax.
1616 //
1617 // HYGIENE GUARDS (per operator 2026-05-27 "we want to avoid
1618 // hoisting issues and namespace issues and all the other hygiene
1619 // issues that make languages suck, really research and make sure
1620 // we are adding good functionality not future nightmares"):
1621 //
1622 // G1 REQUIRED `then`: TK_THEN must follow cond; if absent the
1623 // parser dies clearly. This is the syntactic anchor that
1624 // disambiguates expression-form from statement-form (which
1625 // doesn't use TK_THEN at all).
1626 // G2 REQUIRED `else`: TK_ELSE must follow then-branch; no
1627 // orphan-if at expression position. Matches Haskell / Elm
1628 // (Rust without-else returns unit which doesn't compose at
1629 // expression position; we reject explicitly).
1630 // G3 DEPTH LIMIT: P.ifexp_depth incremented at entry; rejected
1631 // if exceeds NX_PARSE_IFEXP_MAX_DEPTH (16). Prevents any
1632 // pathological codegen path that could blow the stack at
1633 // compile or runtime. Real-world readable code never nests
1634 // ternary/if-expr beyond 4-5 levels; 16 is generous.
1635 // G4 TYPE-COMPATIBLE BRANCHES: both branches must produce
1636 // values whose ir types match (V1: same Type pointer or
1637 // same kind+size; rejects mismatch). Matches Rust / Zig /
1638 // Elm / Haskell. Prevents the JS-style implicit-coerce-to-
1639 // worst-common-type bugs.
1640 // G5 SINGLE COND EVAL: parse_expr(P) for cond runs ONCE before
1641 // the BB split. Side effects in cond happen exactly once.
1642 // G6 SINGLE BRANCH EVAL: ir_emit_br_cond emits a hard branch;
1643 // ONLY the chosen branch BB runs at runtime. No speculative
1644 // both-eval (rules out unintended side effects + duplicate
1645 // work). Lexical isolation: branches use fresh BBs so any
1646 // temporary IR values don't leak to merge BB (NishiLang
1647 // already lexically scoped).
1648 //
1649 if t.kind == TK_IF {
1650 advance_tok(P) // eat 'if'
1651 // G3: depth check on ENTRY (so the first if-expr-call counts).
1652 P.ifexp_depth = P.ifexp_depth + 1
1653 if P.ifexp_depth > 16 {
1654 parse_die("if-expression nested too deep (max 16); refactor with named locals" as *u8, 70)
1655 }
1656 let cond: i64 = parse_expr(P) // G5
1657 // G1
1658 if peek_kind(P) != TK_THEN {
1659 parse_die("expected 'then' after if-expression condition" as *u8, 46)
1660 }
1661 advance_tok(P) // eat 'then'
1662 // Allocate one result slot per if-expression (function-local
1663 // alloca). Pathological depth is bounded by G3 so per-function
1664 // alloca count is bounded.
1665 let result_ty: *Type = ir_type_i64()
1666 let result_addr: i64 = ir_emit_alloca(P.current_bb, result_ty)
1667
1668 let then_bb: *BasicBlock = ir_block_new(P.current_fn) // "ifexp_then" (label dropped; see ARITY FIX above)
1669 let else_bb: *BasicBlock = ir_block_new(P.current_fn) // "ifexp_else"
1670 let merge_bb: *BasicBlock = ir_block_new(P.current_fn) // "ifexp_merge"
1671
1672 ir_emit_br_cond(P.current_bb, cond, then_bb, else_bb) // G6
1673
1674 // then branch
1675 P.current_bb = then_bb
1676 let then_v: i64 = parse_expr(P)
1677 ir_emit_store(P.current_bb, result_addr, then_v, result_ty)
1678 ir_emit_br(P.current_bb, merge_bb)
1679
1680 // G2
1681 if peek_kind(P) != TK_ELSE {
1682 parse_die("if-expression requires 'else' branch (no orphan-if at expression position)" as *u8, 76)
1683 }
1684 advance_tok(P) // eat 'else'
1685
1686 // else branch
1687 P.current_bb = else_bb
1688 let else_v: i64 = parse_expr(P)
1689 ir_emit_store(P.current_bb, result_addr, else_v, result_ty)
1690 ir_emit_br(P.current_bb, merge_bb)
1691
1692 // merge: load result
1693 P.current_bb = merge_bb
1694 let result: i64 = ir_emit_load(P.current_bb, result_addr, result_ty)
1695
1696 // G4 type-check enforcement queued V+1 (needs IR-value->type
1697 // lookup wiring; current pipeline lacks a portable getter).
1698 // For V1 both branches are TREATED as i64 (the most common
1699 // case). Mismatched-type if-expr produces a warning at IR
1700 // validate (existing pass) but doesn't break compile.
1701
1702 P.ifexp_depth = P.ifexp_depth - 1
1703 return result
1704 }
1705
1706 if t.kind == TK_STRING {
1707 advance_tok(P)
1708 // Prefer the heap-grown str_data buffer (handles literals
1709 // longer than the 63-byte inline text[] cap). Fall back to
1710 // the inline text[] when str_data is NULL (legacy callers).
1711 // Same fix as nxc2/parse.c TK_STRING.
1712 var bytes: *u8 = 0 as *u8
1713 var blen: i64 = 0
1714 if t.str_data != 0 {
1715 bytes = t.str_data as *u8
1716 blen = t.str_len
1717 }
1718 if t.str_data == 0 {
1719 bytes = tok_text_ptr(t)
1720 while bytes[blen] != 0 { blen = blen + 1 }
1721 }
1722 let gid: i64 = ir_add_global_string(P.module, bytes, blen)
1723 // Type: pointer-to-u8 (string literals are *u8 in practice).
1724 let pt: *Type = alloc_type(TY_PTR, 8, 8)
1725 let u8ty: *Type = alloc_type(TY_I8, 1, 1)
1726 pt.pointee = u8ty
1727 // SAME ROOT DEFECT AS THE STRING-CONST PATH (seq1552): an inline string literal is also a
1728 // pointer value, so `"abc"[0]` must run the postfix chain or its `[0]` desyncs the parser.
1729 // Fixing only the const half would leave the class alive on the literal half. Gated on an
1730 // actual postfix token, so the ~every-string-in-the-corpus no-postfix case is untouched.
1731 let slv: i64 = ir_global_value(P.current_fn, gid, pt)
1732 if peek_kind(P) == TK_LBRACKET { return parse_field_chain(P, slv, pt) }
1733 if peek_kind(P) == TK_DOT { return parse_field_chain(P, slv, pt) }
1734 return slv
1735 }
1736 if t.kind == TK_LPAREN {
1737 advance_tok(P)
1738 let e: i64 = parse_expr(P)
1739 advance_tok(P) // expect ')'
1740 return e
1741 }
1742 // Ident -> call, local, or module const. Hoisted helper to
1743 // shrink parse_primary's stack frame (task #21 codegen workaround).
1744 if t.kind == TK_IDENT {
1745 return parse_primary_ident(P, t)
1746 }
1747 // PREVENT pillar (four-pillar), architecture-respecting denylist.
1748 // Die ONLY for tokens that can NEVER start a primary expression: the
1749 // operator range [TK_PLUS .. TK_SHR] (40..60) plus `@` `#` `=>`.
1750 // parse_unary consumes every prefix-unary operator (- ! ~ & *) BEFORE
1751 // calling parse_primary, so an operator reaching THIS fallthrough is
1752 // always a missing-operator desync (the `~` class that surfaced as
1753 // "address-of unknown local") or a parser regression -- never
1754 // legitimate. Every OTHER token (structural closers/separators `;`
1755 // `)` `]` `,` `}` `->` `:` `..`, EOF, ...) can legitimately land here
1756 // as an empty-expression terminator, where the historical `return 0`
1757 // is the contract real code depends on (empty statements, function-
1758 // pointer types, empty arg positions) -- preserve it. This kills the
1759 // silent-desync class WITHOUT fighting the parser's return-0 recovery.
1760 let tk: i64 = t.kind
1761 var bad: i64 = 0
1762 if tk >= TK_PLUS { if tk <= TK_SHR { bad = 1 } }
1763 if tk == TK_AT { bad = 1 }
1764 if tk == TK_HASH { bad = 1 }
1765 if tk == TK_FAT_ARROW { bad = 1 }
1766 if bad == 1 {
1767 sys_write(2, "nx_parse: unexpected operator token kind=" as *u8, 41)
1768 nx_put_dec_err(t.kind)
1769 sys_write(2, " at expression start (no primary; parser desync)\n" as *u8, 49)
1770 sys_exit(2)
1771 }
1772 return 0
1773}
1774
1775// ---- parse_primary TK_IDENT helpers (hoisted for stack-frame slim) ----
1776
1777func parse_primary_ident(P: *Parser, t: *Tok) -> i64 {
1778 advance_tok(P)
1779 let name: *u8 = tok_text_ptr(t)
1780 if peek_kind(P) == TK_COLON_COLON {
1781 return parse_primary_qualified_variant(P, name)
1782 }
1783 if name[0] == 0x5F {
1784 if name[1] == 0x5F {
1785 let r: i64 = parse_primary_intrinsic(P, name)
1786 if r >= 0 { return r }
1787 }
1788 }
1789 if peek_kind(P) == TK_LPAREN {
1790 return parse_primary_call(P, name)
1791 }
1792 return parse_primary_local_or_const(P, name)
1793}
1794
1795func parse_primary_qualified_variant(P: *Parser, name: *u8) -> i64 {
1796 advance_tok(P)
1797 let vtok: *Tok = advance_tok(P)
1798 let vname: *u8 = tok_text_ptr(vtok)
1799 let qbuf: *u8 = sys_mmap(80)
1800 var nl: i64 = 0
1801 while name[nl] != 0 { qbuf[nl] = name[nl]; nl = nl + 1 }
1802 qbuf[nl] = 0x3A; nl = nl + 1
1803 qbuf[nl] = 0x3A; nl = nl + 1
1804 var vl: i64 = 0
1805 while vname[vl] != 0 { qbuf[nl + vl] = vname[vl]; vl = vl + 1 }
1806 qbuf[nl + vl] = 0
1807
1808 let out_raw: *u8 = sys_mmap(16)
1809 let out: *i64 = out_raw as *i64
1810 *out = 0
1811 lookup_mconst(P, qbuf, out)
1812 let disc: i64 = *out
1813
1814 var has_pay: i64 = 0
1815 var pay_v: i64 = 0
1816 if peek_kind(P) == TK_LPAREN {
1817 advance_tok(P)
1818 if peek_kind(P) != TK_RPAREN {
1819 pay_v = parse_expr(P)
1820 has_pay = 1
1821 }
1822 match_kind(P, TK_RPAREN)
1823 }
1824
1825 let ee: *EnumEntry = lookup_enum(P, name)
1826 if ee != (0 as *EnumEntry) {
1827 if ee.has_payload == 1 {
1828 if ee.shadow_ty != (0 as *Type) {
1829 let addr: i64 = ir_emit_alloca(P.current_bb, ee.shadow_ty)
1830 let off0: i64 = safe_const_i64(P, 0, "parse.nx:variant-z" as *u8)
1831 let tag_addr: i64 = ir_emit_gep(P.current_bb, addr, off0, ir_type_i64())
1832 let disc_v: i64 = safe_const_i64(P, disc, "parse.nx:variant-d" as *u8)
1833 ir_emit_store(P.current_bb, tag_addr, disc_v, ir_type_i64())
1834 let off8: i64 = safe_const_i64(P, 8, "parse.nx:variant-8" as *u8)
1835 let pay_addr: i64 = ir_emit_gep(P.current_bb, addr, off8, ir_type_i64())
1836 var pv: i64 = pay_v
1837 if has_pay == 0 { pv = safe_const_i64(P, 0, "parse.nx:variant-p0" as *u8) }
1838 ir_emit_store(P.current_bb, pay_addr, pv, ir_type_i64())
1839 return addr
1840 }
1841 }
1842 }
1843 return safe_const_i64(P, disc, "parse.nx:variant-plain" as *u8)
1844}
1845
1846// Returns valueid >= 0 on match; -1 if name is not a recognised intrinsic.
1847func parse_primary_intrinsic(P: *Parser, name: *u8) -> i64 {
1848 if streq_n(name, "__wfi", 5) == 1 {
1849 if peek_kind(P) == TK_LPAREN {
1850 advance_tok(P)
1851 match_kind(P, TK_RPAREN)
1852 ir_emit_wfi(P.current_bb)
1853 return safe_const_i64(P, 0, "parse.nx:wfi-0" as *u8)
1854 }
1855 }
1856 if streq_n(name, "__fence", 7) == 1 {
1857 if peek_kind(P) == TK_LPAREN {
1858 advance_tok(P)
1859 match_kind(P, TK_RPAREN)
1860 ir_emit_fence(P.current_bb)
1861 return safe_const_i64(P, 0, "parse.nx:fence-0" as *u8)
1862 }
1863 }
1864 if streq_n(name, "__mret", 6) == 1 {
1865 if peek_kind(P) == TK_LPAREN {
1866 advance_tok(P)
1867 match_kind(P, TK_RPAREN)
1868 ir_emit_mret(P.current_bb)
1869 return safe_const_i64(P, 0, "parse.nx:mret-0" as *u8)
1870 }
1871 }
1872 if streq_n(name, "__csrr", 6) == 1 {
1873 if peek_kind(P) == TK_LPAREN {
1874 advance_tok(P)
1875 let csr_tok: *Tok = advance_tok(P)
1876 let csr_num: i64 = csr_tok.int_val
1877 match_kind(P, TK_RPAREN)
1878 return ir_emit_csr_read(P.current_bb, csr_num)
1879 }
1880 }
1881 if streq_n(name, "__csrw", 6) == 1 {
1882 if peek_kind(P) == TK_LPAREN {
1883 advance_tok(P)
1884 let csr_tok: *Tok = advance_tok(P)
1885 let csr_num: i64 = csr_tok.int_val
1886 match_kind(P, TK_COMMA)
1887 let val_v: i64 = parse_expr(P)
1888 match_kind(P, TK_RPAREN)
1889 ir_emit_csr_write(P.current_bb, csr_num, val_v)
1890 return safe_const_i64(P, 0, "parse.nx:csrw-0" as *u8)
1891 }
1892 }
1893 // __slice(ptr, len) -> the handle for a []T : bind a length to a pointer.
1894 // Emits a 2-word header {data, len} on the frame and yields its ADDRESS. The ELEMENT TYPE is
1895 // NOT inferred here -- it comes from the declared type at the binding site
1896 // (`let s: []i64 = __slice(p, n)`), which is what makes indexing know its stride. That keeps
1897 // this builtin free of expression-type tracking the parser does not have.
1898 // NOTE (v1 limitation, deliberate): without the `: []T` annotation the local is a plain i64
1899 // holding a header address, and `s[i]` degrades to today's unchecked pointer arithmetic
1900 // rather than becoming unsafe in some NEW way. Requiring the annotation is a compile-time
1901 // error worth adding once slices have users.
1902 // ORDER MATTERS: streq_n compares only the first N bytes, so the 7-byte test for "__slice"
1903 // also matches "__slice_len". The longer name MUST be tested first or every __slice_len call
1904 // is silently parsed as a __slice construction and returns a header address instead of a
1905 // length. Caught by witness W8 printing len=129311793545216.
1906 if streq_n(name, "__slice_len", 11) == 1 {
1907 if peek_kind(P) == TK_LPAREN {
1908 advance_tok(P)
1909 let sh_v: i64 = parse_expr(P)
1910 match_kind(P, TK_RPAREN)
1911 let lo_v: i64 = safe_const_i64(P, NX_SLICE_LEN_OFFSET, "parse.nx:slicelen-off" as *u8)
1912 let la: i64 = ir_emit_binop(P.current_bb, OP_ADD, sh_v, lo_v, ir_type_i64())
1913 return ir_emit_load(P.current_bb, la, ir_type_i64())
1914 }
1915 }
1916 if streq_n(name, "__slice", 7) == 1 {
1917 if peek_kind(P) == TK_LPAREN {
1918 advance_tok(P)
1919 let sp_v: i64 = parse_expr(P)
1920 match_kind(P, TK_COMMA)
1921 let sl_v: i64 = parse_expr(P)
1922 match_kind(P, TK_RPAREN)
1923 let hdr_ty: *Type = alloc_type(TY_ARRAY, NX_SLICE_HDR_BYTES, 8)
1924 hdr_ty.pointee = ir_type_i64()
1925 let hdr: i64 = ir_emit_alloca(P.current_bb, hdr_ty)
1926 // An alloca id used as a VALUE operand AUTO-LOADS (it yields the slot's CONTENTS, not
1927 // its address) -- the same trap the array-decay path documents. Take the address once
1928 // with OP_ADDR_OF and use only that. Getting this wrong made `ADD(hdr, 8)` compute
1929 // data+8, so __slice wrote the LENGTH into the caller's element 1 and returned the
1930 // DATA pointer as the handle; every access then aliased the payload. Measured by
1931 // nx_bchk_dbg_addr printing delta=0 between the handle and the data pointer.
1932 let hdr_pt: *Type = alloc_type(TY_PTR, 8, 8)
1933 hdr_pt.pointee = ir_type_i64()
1934 let hdr_a: i64 = ir_emit_unop(P.current_bb, OP_ADDR_OF, hdr, hdr_pt)
1935 ir_emit_store(P.current_bb, hdr_a, sp_v, ir_type_i64())
1936 let off_v: i64 = safe_const_i64(P, NX_SLICE_LEN_OFFSET, "parse.nx:slice-lenoff" as *u8)
1937 let lenaddr: i64 = ir_emit_binop(P.current_bb, OP_ADD, hdr_a, off_v, ir_type_i64())
1938 ir_emit_store(P.current_bb, lenaddr, sl_v, ir_type_i64())
1939 return hdr_a
1940 }
1941 }
1942 // (__slice_len is handled ABOVE __slice -- see the ORDER MATTERS note there.)
1943 if streq_n(name, "__syscall", 9) == 1 {
1944 if peek_kind(P) == TK_LPAREN {
1945 advance_tok(P)
1946 let sargs_raw: *u8 = sys_mmap(8 * 8 + 16)
1947 let sargs: *i64 = sargs_raw as *i64
1948 var sn: i64 = 0
1949 if peek_kind(P) != TK_RPAREN {
1950 sargs[sn] = parse_expr(P)
1951 sn = sn + 1
1952 while peek_kind(P) == TK_COMMA {
1953 advance_tok(P)
1954 if sn < 7 {
1955 sargs[sn] = parse_expr(P)
1956 sn = sn + 1
1957 } else {
1958 parse_expr(P)
1959 }
1960 }
1961 }
1962 match_kind(P, TK_RPAREN)
1963 return ir_emit_syscall(P.current_bb, sargs, sn)
1964 }
1965 }
1966 // Hardware f32 (IEEE-754 binary32) intrinsics. A float rides as the low 32 bits
1967 // of an i64 carrier (NishiLang has no f32 type); the x86 backend lowers these to
1968 // SSE scalar-single (cvtsi2ss/addss/mulss/divss/cvttss2si). __f32_from_i64(x) ->
1969 // float bits; __f32_to_i64(f) -> truncated int; __f32_add/mul/div(a,b) -> float bits.
1970 if streq_n(name, "__f32_from_i64", 14) == 1 {
1971 if peek_kind(P) == TK_LPAREN {
1972 advance_tok(P)
1973 let a_v: i64 = parse_expr(P)
1974 match_kind(P, TK_RPAREN)
1975 return ir_emit_f32_unop(P.current_bb, OP_FCAST_I_TO_F, a_v)
1976 }
1977 }
1978 if streq_n(name, "__f32_to_i64", 12) == 1 {
1979 if peek_kind(P) == TK_LPAREN {
1980 advance_tok(P)
1981 let a_v: i64 = parse_expr(P)
1982 match_kind(P, TK_RPAREN)
1983 return ir_emit_f32_unop(P.current_bb, OP_FCAST_F_TO_I, a_v)
1984 }
1985 }
1986 // Hardware f64 (IEEE-754 binary64) conversion + sqrt intrinsics. f64 literals
1987 // and f64 arithmetic already carry TY_F64; these bridge int<->f64 and add sqrt.
1988 // __f64_from_i64(x) -> f64 bits (cvtsi2sd); __f64_to_i64(f) -> truncated int
1989 // (cvttsd2si); __f64_sqrt(x) -> sqrt (sqrtsd). The backend picks double
1990 // precision from the TY_F64 result (or, for _to_i64, from the f64 operand).
1991 if streq_n(name, "__f64_from_i64", 14) == 1 {
1992 if peek_kind(P) == TK_LPAREN {
1993 advance_tok(P)
1994 let a_v: i64 = parse_expr(P)
1995 match_kind(P, TK_RPAREN)
1996 return ir_emit_f64_unop(P.current_bb, OP_FCAST_I_TO_F, a_v)
1997 }
1998 }
1999 if streq_n(name, "__f64_to_i64", 12) == 1 {
2000 if peek_kind(P) == TK_LPAREN {
2001 advance_tok(P)
2002 let a_v: i64 = parse_expr(P)
2003 match_kind(P, TK_RPAREN)
2004 return ir_emit_f32_unop(P.current_bb, OP_FCAST_F_TO_I, a_v)
2005 }
2006 }
2007 if streq_n(name, "__f64_sqrt", 10) == 1 {
2008 if peek_kind(P) == TK_LPAREN {
2009 advance_tok(P)
2010 let a_v: i64 = parse_expr(P)
2011 match_kind(P, TK_RPAREN)
2012 return ir_emit_f64_unop(P.current_bb, OP_FSQRT, a_v)
2013 }
2014 }
2015 // Hardware AES-NI: __aes128_enc_block(state_ptr, roundkeys_ptr) encrypts the 16-byte
2016 // block at state_ptr IN PLACE using the 11 expanded round keys (176 bytes) at
2017 // roundkeys_ptr. Lowered to movdqu + pxor + aesenc x9 + aesenclast (state in %xmm0).
2018 if streq_n(name, "__aes128_enc_block", 18) == 1 {
2019 if peek_kind(P) == TK_LPAREN {
2020 advance_tok(P)
2021 let a_v: i64 = parse_expr(P)
2022 match_kind(P, TK_COMMA)
2023 let b_v: i64 = parse_expr(P)
2024 match_kind(P, TK_RPAREN)
2025 return ir_emit_f32_binop(P.current_bb, OP_AES128_ENC_BLOCK, a_v, b_v)
2026 }
2027 }
2028 // Hardware SHA-NI: __sha256_ni_block(state_ptr, block_ptr, k_ptr) runs ONE full SHA-256 block
2029 // compression IN PLACE. state_ptr -> 8 contiguous u32 (working state a..h == h0..h7); block_ptr
2030 // -> 64 raw big-endian message bytes; k_ptr -> 64 contiguous u32 round constants K[0..63].
2031 // Lowered to the Intel SHA extension sequence (punpck/pshufd state arrange, pshufb byte-swap,
2032 // 16x sha256msg1/msg2 + 2x sha256rnds2). The software sha256_compress stays the oracle/fallback;
2033 // callers gate on __cpuid_ebx(7,0) bit-29 (SHA). 3 operands (op0/op1/op2), i64 result (0).
2034 if streq_n(name, "__sha256_ni_block", 17) == 1 {
2035 if peek_kind(P) == TK_LPAREN {
2036 advance_tok(P)
2037 let sa: i64 = parse_expr(P)
2038 match_kind(P, TK_COMMA)
2039 let sb: i64 = parse_expr(P)
2040 match_kind(P, TK_COMMA)
2041 let sk: i64 = parse_expr(P)
2042 match_kind(P, TK_RPAREN)
2043 return ir_emit_sha256_ni_block(P.current_bb, sa, sb, sk)
2044 }
2045 }
2046 // Q5_0 SSE unpack: __q5_unpack32(qhqs_ptr, out_i8_ptr, consts_ptr).
2047 if streq_n(name, "__q5_unpack32", 13) == 1 {
2048 if peek_kind(P) == TK_LPAREN {
2049 advance_tok(P)
2050 let u0: i64 = parse_expr(P)
2051 match_kind(P, TK_COMMA)
2052 let u1: i64 = parse_expr(P)
2053 match_kind(P, TK_COMMA)
2054 let u2: i64 = parse_expr(P)
2055 match_kind(P, TK_RPAREN)
2056 return ir_emit_q5unpack32(P.current_bb, u0, u1, u2)
2057 }
2058 }
2059 // Fused wide multiply: __mul256_wide(dst_ptr, a_ptr, b_ptr) computes the 512-bit product
2060 // of the 256-bit little-endian integers *a * *b into *dst (8 x u64) via the ADX/BMI2
2061 // mulx+adcx+adox dual-carry kernel. IN PLACE write to dst; i64 result (0).
2062 if streq_n(name, "__mul256_wide", 13) == 1 {
2063 if peek_kind(P) == TK_LPAREN {
2064 advance_tok(P)
2065 let md: i64 = parse_expr(P)
2066 match_kind(P, TK_COMMA)
2067 let ma: i64 = parse_expr(P)
2068 match_kind(P, TK_COMMA)
2069 let mb: i64 = parse_expr(P)
2070 match_kind(P, TK_RPAREN)
2071 return ir_emit_mul256_wide(P.current_bb, md, ma, mb)
2072 }
2073 }
2074 // Hardware CLMUL (PCLMULQDQ): __clmul_XY(p, q) carry-less-multiplies the X half of *p
2075 // by the Y half of *q (X,Y in {l,h}) and writes the 128-bit product back to *p in place.
2076 // The four variants are the half-products of a 128x128 GF(2) multiply -- the GHASH core.
2077 if streq_n(name, "__clmul_ll", 10) == 1 {
2078 if peek_kind(P) == TK_LPAREN {
2079 advance_tok(P)
2080 let a_v: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2081 let b_v: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2082 return ir_emit_f32_binop(P.current_bb, OP_CLMUL_LL, a_v, b_v)
2083 }
2084 }
2085 if streq_n(name, "__clmul_hh", 10) == 1 {
2086 if peek_kind(P) == TK_LPAREN {
2087 advance_tok(P)
2088 let a_v: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2089 let b_v: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2090 return ir_emit_f32_binop(P.current_bb, OP_CLMUL_HH, a_v, b_v)
2091 }
2092 }
2093 if streq_n(name, "__clmul_lh", 10) == 1 {
2094 if peek_kind(P) == TK_LPAREN {
2095 advance_tok(P)
2096 let a_v: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2097 let b_v: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2098 return ir_emit_f32_binop(P.current_bb, OP_CLMUL_LH, a_v, b_v)
2099 }
2100 }
2101 if streq_n(name, "__clmul_hl", 10) == 1 {
2102 if peek_kind(P) == TK_LPAREN {
2103 advance_tok(P)
2104 let a_v: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2105 let b_v: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2106 return ir_emit_f32_binop(P.current_bb, OP_CLMUL_HL, a_v, b_v)
2107 }
2108 }
2109 if streq_n(name, "__f32_add", 9) == 1 {
2110 if peek_kind(P) == TK_LPAREN {
2111 advance_tok(P)
2112 let a_v: i64 = parse_expr(P)
2113 match_kind(P, TK_COMMA)
2114 let b_v: i64 = parse_expr(P)
2115 match_kind(P, TK_RPAREN)
2116 return ir_emit_f32_binop(P.current_bb, OP_FADD, a_v, b_v)
2117 }
2118 }
2119 if streq_n(name, "__f32_mul", 9) == 1 {
2120 if peek_kind(P) == TK_LPAREN {
2121 advance_tok(P)
2122 let a_v: i64 = parse_expr(P)
2123 match_kind(P, TK_COMMA)
2124 let b_v: i64 = parse_expr(P)
2125 match_kind(P, TK_RPAREN)
2126 return ir_emit_f32_binop(P.current_bb, OP_FMUL, a_v, b_v)
2127 }
2128 }
2129 if streq_n(name, "__f32_div", 9) == 1 {
2130 if peek_kind(P) == TK_LPAREN {
2131 advance_tok(P)
2132 let a_v: i64 = parse_expr(P)
2133 match_kind(P, TK_COMMA)
2134 let b_v: i64 = parse_expr(P)
2135 match_kind(P, TK_RPAREN)
2136 return ir_emit_f32_binop(P.current_bb, OP_FDIV, a_v, b_v)
2137 }
2138 }
2139 // Widening SIMD dot product i16x16 -> i64 scalar. v0.0.1 shape:
2140 // takes two *i64 pointers (each addressing 4 packed i64 words =
2141 // 16 i16 lanes), does load-load-dot in one IR op. Self-host
2142 // SIMD surface first land -- bits-up from nothing. Codegen in
2143 // nx_riscv.nx lowers to vsetvli e16 m1 + vle16 + vwmul.vv +
2144 // vwredsum.vs.
2145 if streq_n(name, "__simd_vdot_i16_x16", 19) == 1 {
2146 if peek_kind(P) == TK_LPAREN {
2147 advance_tok(P)
2148 let a_v: i64 = parse_expr(P)
2149 match_kind(P, TK_COMMA)
2150 let b_v: i64 = parse_expr(P)
2151 match_kind(P, TK_RPAREN)
2152 return ir_emit_simd_vdot_i16_x16(P.current_bb, a_v, b_v)
2153 }
2154 }
2155 // Packed f32x4 dot product: __f32x4_dot(a_ptr, b_ptr) where each ptr addresses 4 CONTIGUOUS
2156 // 4-byte f32 -> their dot as an f32 scalar (i64-carried bits). x86 SSE movups+mulps + scalar
2157 // horizontal-sum (the compute-physics lever: 4 multiplies in one mulps vs 4 scalar mulss).
2158 if streq_n(name, "__f32x4_dot", 11) == 1 {
2159 if peek_kind(P) == TK_LPAREN {
2160 advance_tok(P)
2161 let fa_v: i64 = parse_expr(P)
2162 match_kind(P, TK_COMMA)
2163 let fb_v: i64 = parse_expr(P)
2164 match_kind(P, TK_RPAREN)
2165 return ir_emit_f32_binop(P.current_bb, OP_F32X4_DOT, fa_v, fb_v)
2166 }
2167 }
2168 // Q8_0/quantized dequant-dot lever: __f32_i8dot32(a:*i8[32], b:*f32[32])
2169 // -> f32 = dot of 32 sign-extended int8 with 32 f32 (SSE unrolled).
2170 // Monolithic Q8_0 row dot: __f32_q8row_dot(qbuf_row, a_row, nblocks).
2171 if streq_n(name, "__f32_q8row_dot", 15) == 1 {
2172 if peek_kind(P) == TK_LPAREN {
2173 advance_tok(P)
2174 let rq: i64 = parse_expr(P)
2175 match_kind(P, TK_COMMA)
2176 let ra: i64 = parse_expr(P)
2177 match_kind(P, TK_COMMA)
2178 let rn: i64 = parse_expr(P)
2179 match_kind(P, TK_RPAREN)
2180 return ir_emit_q8rowdot(P.current_bb, rq, ra, rn)
2181 }
2182 }
2183 // Deferred-hsum FMA block: __f32_i8fma32(a,b,d_bits,acc_ptr) -> acc += d*(a.b).
2184 if streq_n(name, "__f32_i8fma32", 13) == 1 {
2185 if peek_kind(P) == TK_LPAREN {
2186 advance_tok(P)
2187 let fa: i64 = parse_expr(P)
2188 match_kind(P, TK_COMMA)
2189 let fb: i64 = parse_expr(P)
2190 match_kind(P, TK_COMMA)
2191 let fd: i64 = parse_expr(P)
2192 match_kind(P, TK_COMMA)
2193 let fac: i64 = parse_expr(P)
2194 match_kind(P, TK_RPAREN)
2195 return ir_emit_i8fma32(P.current_bb, fa, fb, fd, fac)
2196 }
2197 }
2198 // AVX2 twin -- MUST be checked before __f32_i8dot32 (its 14-char name
2199 // shares the first 13 chars, so the 13-char streq_n would swallow it).
2200 if streq_n(name, "__f32_i8dot32a", 14) == 1 {
2201 if peek_kind(P) == TK_LPAREN {
2202 advance_tok(P)
2203 let aa_v: i64 = parse_expr(P)
2204 match_kind(P, TK_COMMA)
2205 let ab_v: i64 = parse_expr(P)
2206 match_kind(P, TK_RPAREN)
2207 return ir_emit_f32_binop(P.current_bb, OP_I8DOT32A, aa_v, ab_v)
2208 }
2209 }
2210 if streq_n(name, "__f32_i8dot32", 13) == 1 {
2211 if peek_kind(P) == TK_LPAREN {
2212 advance_tok(P)
2213 let ia_v: i64 = parse_expr(P)
2214 match_kind(P, TK_COMMA)
2215 let ib_v: i64 = parse_expr(P)
2216 match_kind(P, TK_RPAREN)
2217 return ir_emit_f32_binop(P.current_bb, OP_I8DOT32, ia_v, ib_v)
2218 }
2219 }
2220 // Packed f32x8 dot product: __f32x8_dot(a_ptr, b_ptr), each ptr -> 8 CONTIGUOUS 4-byte f32 ->
2221 // their dot as f32 scalar. x86 AVX2: vmovups+vmulps (8 lanes/instr) + vextractf128 + SSE hsum.
2222 if streq_n(name, "__f32x8_dot", 11) == 1 {
2223 if peek_kind(P) == TK_LPAREN {
2224 advance_tok(P)
2225 let ga_v: i64 = parse_expr(P)
2226 match_kind(P, TK_COMMA)
2227 let gb_v: i64 = parse_expr(P)
2228 match_kind(P, TK_RPAREN)
2229 return ir_emit_f32_binop(P.current_bb, OP_F32X8_DOT, ga_v, gb_v)
2230 }
2231 }
2232 // FMA vector-accumulate: __f32x8_fma(acc_ptr, a_ptr, b_ptr) -> *acc += a*b (8-wide fused, no hsum)
2233 if streq_n(name, "__f32x8_fma", 11) == 1 {
2234 if peek_kind(P) == TK_LPAREN {
2235 advance_tok(P)
2236 let fac: i64 = parse_expr(P)
2237 match_kind(P, TK_COMMA)
2238 let faa: i64 = parse_expr(P)
2239 match_kind(P, TK_COMMA)
2240 let fab: i64 = parse_expr(P)
2241 match_kind(P, TK_RPAREN)
2242 return ir_emit_f32x8_fma(P.current_bb, fac, faa, fab)
2243 }
2244 }
2245 // horizontal sum of an 8-wide accumulator: __f32x8_hsum(acc_ptr) -> f32 (called ONCE per dot)
2246 if streq_n(name, "__f32x8_hsum", 12) == 1 {
2247 if peek_kind(P) == TK_LPAREN {
2248 advance_tok(P)
2249 let fhc: i64 = parse_expr(P)
2250 match_kind(P, TK_RPAREN)
2251 return ir_emit_f32_unop(P.current_bb, OP_F32X8_HSUM, fhc)
2252 }
2253 }
2254 // NO-FLOAT integer dot accumulate: __i16x16_madd(acc_ptr, a_ptr, b_ptr) -> *acc(i32x8) += vpmaddwd(a,b)
2255 if streq_n(name, "__i16x16_madd", 13) == 1 {
2256 if peek_kind(P) == TK_LPAREN {
2257 advance_tok(P)
2258 let mac: i64 = parse_expr(P)
2259 match_kind(P, TK_COMMA)
2260 let maa: i64 = parse_expr(P)
2261 match_kind(P, TK_COMMA)
2262 let mab: i64 = parse_expr(P)
2263 match_kind(P, TK_RPAREN)
2264 return ir_emit_i16x16_madd(P.current_bb, mac, maa, mab)
2265 }
2266 }
2267 if streq_n(name, "__simd_vreduce_min_i16_x16", 26) == 1 {
2268 if peek_kind(P) == TK_LPAREN {
2269 advance_tok(P)
2270 let pmn: i64 = parse_expr(P)
2271 match_kind(P, TK_RPAREN)
2272 return ir_emit_simd_vreduce_min_i16_x16(P.current_bb, pmn)
2273 }
2274 }
2275 if streq_n(name, "__simd_vreduce_max_i16_x16", 26) == 1 {
2276 if peek_kind(P) == TK_LPAREN {
2277 advance_tok(P)
2278 let pmx: i64 = parse_expr(P)
2279 match_kind(P, TK_RPAREN)
2280 return ir_emit_simd_vreduce_max_i16_x16(P.current_bb, pmx)
2281 }
2282 }
2283 if streq_n(name, "__simd_vsadd_i16_x16", 20) == 1 {
2284 if peek_kind(P) == TK_LPAREN {
2285 advance_tok(P)
2286 let asa: i64 = parse_expr(P)
2287 match_kind(P, TK_COMMA)
2288 let bsa: i64 = parse_expr(P)
2289 match_kind(P, TK_COMMA)
2290 let osa: i64 = parse_expr(P)
2291 match_kind(P, TK_RPAREN)
2292 return ir_emit_simd_vsadd_i16_x16(P.current_bb, asa, bsa, osa)
2293 }
2294 }
2295 if streq_n(name, "__simd_vssub_i16_x16", 20) == 1 {
2296 if peek_kind(P) == TK_LPAREN {
2297 advance_tok(P)
2298 let ass: i64 = parse_expr(P)
2299 match_kind(P, TK_COMMA)
2300 let bss: i64 = parse_expr(P)
2301 match_kind(P, TK_COMMA)
2302 let oss: i64 = parse_expr(P)
2303 match_kind(P, TK_RPAREN)
2304 return ir_emit_simd_vssub_i16_x16(P.current_bb, ass, bss, oss)
2305 }
2306 }
2307 if streq_n(name, "__simd_vsaddu_i16_x16", 21) == 1 {
2308 if peek_kind(P) == TK_LPAREN {
2309 advance_tok(P)
2310 let asau: i64 = parse_expr(P)
2311 match_kind(P, TK_COMMA)
2312 let bsau: i64 = parse_expr(P)
2313 match_kind(P, TK_COMMA)
2314 let osau: i64 = parse_expr(P)
2315 match_kind(P, TK_RPAREN)
2316 return ir_emit_simd_vsaddu_i16_x16(P.current_bb, asau, bsau, osau)
2317 }
2318 }
2319 if streq_n(name, "__simd_vssubu_i16_x16", 21) == 1 {
2320 if peek_kind(P) == TK_LPAREN {
2321 advance_tok(P)
2322 let assu: i64 = parse_expr(P)
2323 match_kind(P, TK_COMMA)
2324 let bssu: i64 = parse_expr(P)
2325 match_kind(P, TK_COMMA)
2326 let ossu: i64 = parse_expr(P)
2327 match_kind(P, TK_RPAREN)
2328 return ir_emit_simd_vssubu_i16_x16(P.current_bb, assu, bssu, ossu)
2329 }
2330 }
2331 // Per-lane min/max/add/sub/mul: 25-char builtins (vXXX_lane).
2332 if streq_n(name, "__simd_vmin_lane_i16_x16", 24) == 1 {
2333 if peek_kind(P) == TK_LPAREN {
2334 advance_tok(P)
2335 let amn: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2336 let bmn: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2337 let omn: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2338 return ir_emit_simd_vmin_lane_i16_x16(P.current_bb, amn, bmn, omn)
2339 }
2340 }
2341 if streq_n(name, "__simd_vmax_lane_i16_x16", 24) == 1 {
2342 if peek_kind(P) == TK_LPAREN {
2343 advance_tok(P)
2344 let amx: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2345 let bmx: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2346 let omx: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2347 return ir_emit_simd_vmax_lane_i16_x16(P.current_bb, amx, bmx, omx)
2348 }
2349 }
2350 if streq_n(name, "__simd_vadd_lane_i16_x16", 24) == 1 {
2351 if peek_kind(P) == TK_LPAREN {
2352 advance_tok(P)
2353 let aad: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2354 let bad: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2355 let oad: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2356 return ir_emit_simd_vadd_lane_i16_x16(P.current_bb, aad, bad, oad)
2357 }
2358 }
2359 if streq_n(name, "__simd_vsub_lane_i16_x16", 24) == 1 {
2360 if peek_kind(P) == TK_LPAREN {
2361 advance_tok(P)
2362 let asb: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2363 let bsb: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2364 let osb: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2365 return ir_emit_simd_vsub_lane_i16_x16(P.current_bb, asb, bsb, osb)
2366 }
2367 }
2368 if streq_n(name, "__simd_vmul_lane_i16_x16", 24) == 1 {
2369 if peek_kind(P) == TK_LPAREN {
2370 advance_tok(P)
2371 let aml: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2372 let bml: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2373 let oml: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2374 return ir_emit_simd_vmul_lane_i16_x16(P.current_bb, aml, bml, oml)
2375 }
2376 }
2377 // 3-arg shifts: __simd_vsll/vsrl/vsra_i16_x16(*i64 src, i64 count, *i64 out)
2378 if streq_n(name, "__simd_vsll_i16_x16", 19) == 1 {
2379 if peek_kind(P) == TK_LPAREN {
2380 advance_tok(P)
2381 let asl: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2382 let csl: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2383 let osl: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2384 return ir_emit_simd_vsll_i16_x16(P.current_bb, asl, csl, osl)
2385 }
2386 }
2387 if streq_n(name, "__simd_vsrl_i16_x16", 19) == 1 {
2388 if peek_kind(P) == TK_LPAREN {
2389 advance_tok(P)
2390 let asr: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2391 let csr: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2392 let osr: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2393 return ir_emit_simd_vsrl_i16_x16(P.current_bb, asr, csr, osr)
2394 }
2395 }
2396 if streq_n(name, "__simd_vsra_i16_x16", 19) == 1 {
2397 if peek_kind(P) == TK_LPAREN {
2398 advance_tok(P)
2399 let asa2: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2400 let csa2: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2401 let osa2: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2402 return ir_emit_simd_vsra_i16_x16(P.current_bb, asa2, csa2, osa2)
2403 }
2404 }
2405 if streq_n(name, "__simd_vreduce_sum_i16_x16", 26) == 1 {
2406 if peek_kind(P) == TK_LPAREN {
2407 advance_tok(P)
2408 let prs: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2409 return ir_emit_simd_vreduce_sum_i16_x16(P.current_bb, prs)
2410 }
2411 }
2412 if streq_n(name, "__simd_vbroadcast_i16_x16", 25) == 1 {
2413 if peek_kind(P) == TK_LPAREN {
2414 advance_tok(P)
2415 let sbc: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2416 let obc: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2417 return ir_emit_simd_vbroadcast_i16_x16(P.current_bb, sbc, obc)
2418 }
2419 }
2420 // i8x32 set
2421 if streq_n(name, "__simd_vadd_i8_x32", 18) == 1 {
2422 if peek_kind(P) == TK_LPAREN {
2423 advance_tok(P)
2424 let a8a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2425 let b8a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2426 let o8a: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2427 return ir_emit_simd_vadd_i8_x32(P.current_bb, a8a, b8a, o8a)
2428 }
2429 }
2430 if streq_n(name, "__simd_vsub_i8_x32", 18) == 1 {
2431 if peek_kind(P) == TK_LPAREN {
2432 advance_tok(P)
2433 let a8s: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2434 let b8s: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2435 let o8s: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2436 return ir_emit_simd_vsub_i8_x32(P.current_bb, a8s, b8s, o8s)
2437 }
2438 }
2439 if streq_n(name, "__simd_vsadd_i8_x32", 19) == 1 {
2440 if peek_kind(P) == TK_LPAREN {
2441 advance_tok(P)
2442 let a8sa: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2443 let b8sa: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2444 let o8sa: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2445 return ir_emit_simd_vsadd_i8_x32(P.current_bb, a8sa, b8sa, o8sa)
2446 }
2447 }
2448 if streq_n(name, "__simd_vssub_i8_x32", 19) == 1 {
2449 if peek_kind(P) == TK_LPAREN {
2450 advance_tok(P)
2451 let a8ss: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2452 let b8ss: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2453 let o8ss: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2454 return ir_emit_simd_vssub_i8_x32(P.current_bb, a8ss, b8ss, o8ss)
2455 }
2456 }
2457 if streq_n(name, "__simd_vreduce_sum_i8_x32", 25) == 1 {
2458 if peek_kind(P) == TK_LPAREN {
2459 advance_tok(P)
2460 let p8r: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2461 return ir_emit_simd_vreduce_sum_i8_x32(P.current_bb, p8r)
2462 }
2463 }
2464 if streq_n(name, "__simd_vbroadcast_i8_x32", 24) == 1 {
2465 if peek_kind(P) == TK_LPAREN {
2466 advance_tok(P)
2467 let s8b: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2468 let o8b: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2469 return ir_emit_simd_vbroadcast_i8_x32(P.current_bb, s8b, o8b)
2470 }
2471 }
2472 // i32x8 set: names 17-24 chars long.
2473 if streq_n(name, "__simd_vadd_i32_x8", 18) == 1 {
2474 if peek_kind(P) == TK_LPAREN {
2475 advance_tok(P)
2476 let a32a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2477 let b32a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2478 let o32a: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2479 return ir_emit_simd_vadd_i32_x8(P.current_bb, a32a, b32a, o32a)
2480 }
2481 }
2482 if streq_n(name, "__simd_vsub_i32_x8", 18) == 1 {
2483 if peek_kind(P) == TK_LPAREN {
2484 advance_tok(P)
2485 let a32s: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2486 let b32s: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2487 let o32s: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2488 return ir_emit_simd_vsub_i32_x8(P.current_bb, a32s, b32s, o32s)
2489 }
2490 }
2491 if streq_n(name, "__simd_vmul_i32_x8", 18) == 1 {
2492 if peek_kind(P) == TK_LPAREN {
2493 advance_tok(P)
2494 let a32m: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2495 let b32m: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2496 let o32m: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2497 return ir_emit_simd_vmul_i32_x8(P.current_bb, a32m, b32m, o32m)
2498 }
2499 }
2500 if streq_n(name, "__simd_vsadd_i32_x8", 19) == 1 {
2501 if peek_kind(P) == TK_LPAREN {
2502 advance_tok(P)
2503 let a32sa: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2504 let b32sa: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2505 let o32sa: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2506 return ir_emit_simd_vsadd_i32_x8(P.current_bb, a32sa, b32sa, o32sa)
2507 }
2508 }
2509 if streq_n(name, "__simd_vssub_i32_x8", 19) == 1 {
2510 if peek_kind(P) == TK_LPAREN {
2511 advance_tok(P)
2512 let a32ss: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2513 let b32ss: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2514 let o32ss: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2515 return ir_emit_simd_vssub_i32_x8(P.current_bb, a32ss, b32ss, o32ss)
2516 }
2517 }
2518 if streq_n(name, "__simd_vreduce_sum_i32_x8", 25) == 1 {
2519 if peek_kind(P) == TK_LPAREN {
2520 advance_tok(P)
2521 let p32r: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2522 return ir_emit_simd_vreduce_sum_i32_x8(P.current_bb, p32r)
2523 }
2524 }
2525 if streq_n(name, "__simd_vbroadcast_i32_x8", 24) == 1 {
2526 if peek_kind(P) == TK_LPAREN {
2527 advance_tok(P)
2528 let s32b: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2529 let o32b: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2530 return ir_emit_simd_vbroadcast_i32_x8(P.current_bb, s32b, o32b)
2531 }
2532 }
2533 // i64x4 set
2534 if streq_n(name, "__simd_vadd_i64_x4", 18) == 1 {
2535 if peek_kind(P) == TK_LPAREN {
2536 advance_tok(P)
2537 let a64a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2538 let b64a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2539 let o64a: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2540 return ir_emit_simd_vadd_i64_x4(P.current_bb, a64a, b64a, o64a)
2541 }
2542 }
2543 if streq_n(name, "__simd_vsub_i64_x4", 18) == 1 {
2544 if peek_kind(P) == TK_LPAREN {
2545 advance_tok(P)
2546 let a64s: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2547 let b64s: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2548 let o64s: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2549 return ir_emit_simd_vsub_i64_x4(P.current_bb, a64s, b64s, o64s)
2550 }
2551 }
2552 if streq_n(name, "__simd_vmul_i64_x4", 18) == 1 {
2553 if peek_kind(P) == TK_LPAREN {
2554 advance_tok(P)
2555 let a64m: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2556 let b64m: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2557 let o64m: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2558 return ir_emit_simd_vmul_i64_x4(P.current_bb, a64m, b64m, o64m)
2559 }
2560 }
2561 if streq_n(name, "__simd_vsadd_i64_x4", 19) == 1 {
2562 if peek_kind(P) == TK_LPAREN {
2563 advance_tok(P)
2564 let a64sa: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2565 let b64sa: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2566 let o64sa: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2567 return ir_emit_simd_vsadd_i64_x4(P.current_bb, a64sa, b64sa, o64sa)
2568 }
2569 }
2570 if streq_n(name, "__simd_vssub_i64_x4", 19) == 1 {
2571 if peek_kind(P) == TK_LPAREN {
2572 advance_tok(P)
2573 let a64ss: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2574 let b64ss: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2575 let o64ss: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2576 return ir_emit_simd_vssub_i64_x4(P.current_bb, a64ss, b64ss, o64ss)
2577 }
2578 }
2579 if streq_n(name, "__simd_vreduce_sum_i64_x4", 25) == 1 {
2580 if peek_kind(P) == TK_LPAREN {
2581 advance_tok(P)
2582 let p64r: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2583 return ir_emit_simd_vreduce_sum_i64_x4(P.current_bb, p64r)
2584 }
2585 }
2586 if streq_n(name, "__simd_vbroadcast_i64_x4", 24) == 1 {
2587 if peek_kind(P) == TK_LPAREN {
2588 advance_tok(P)
2589 let s64b: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2590 let o64b: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2591 return ir_emit_simd_vbroadcast_i64_x4(P.current_bb, s64b, o64b)
2592 }
2593 }
2594 // Bit-rotate builtins -- C bootstrap parity (parse.c OP_ROTL64 /
2595 // OP_ROTR64). __rotl64(value, count) / __rotr64(value, count).
2596 // Lowered to x86 ROLQ/RORQ %cl (single instruction). Without
2597 // these the call fell through to an unresolved function and
2598 // silently returned op0 -- a no-op rotate that broke SHA-512.
2599 if streq_n(name, "__rotl64", 8) == 1 {
2600 if peek_kind(P) == TK_LPAREN {
2601 advance_tok(P)
2602 let rlv: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2603 let rln: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2604 return ir_emit_binop(P.current_bb, OP_ROTL64, rlv, rln, ir_type_i64())
2605 }
2606 }
2607 if streq_n(name, "__rotr64", 8) == 1 {
2608 if peek_kind(P) == TK_LPAREN {
2609 advance_tok(P)
2610 let rrv: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2611 let rrn: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2612 return ir_emit_binop(P.current_bb, OP_ROTR64, rrv, rrn, ir_type_i64())
2613 }
2614 }
2615 // Wide multiply -- G2 substrate. __umulhi64(a, b) = the HIGH 64 bits of the
2616 // unsigned 64x64 product (x86 mulq's rdx). Paired with normal `*` (the low
2617 // 64 bits) it yields the full 128-bit product, so crypto field arithmetic can
2618 // use 4x64-bit limbs (16 partial products) instead of 8x32 (64).
2619 if streq_n(name, "__umulhi64", 10) == 1 {
2620 if peek_kind(P) == TK_LPAREN {
2621 advance_tok(P)
2622 let uma: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2623 let umb: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2624 return ir_emit_binop(P.current_bb, OP_UMULHI, uma, umb, ir_type_i64())
2625 }
2626 }
2627 // Hardware CRC-32C (SSE4.2) -- __crc32_u64(crc, data) folds the 64-bit
2628 // `data` word into the running `crc` accumulator (x86 crc32q). Pure,
2629 // 2 i64 operands -> i64. Checksums, packet validation, hash fingerprints.
2630 if streq_n(name, "__crc32_u64", 11) == 1 {
2631 if peek_kind(P) == TK_LPAREN {
2632 advance_tok(P)
2633 let crca: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2634 let crcd: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2635 return ir_emit_binop(P.current_bb, OP_CRC32, crca, crcd, ir_type_i64())
2636 }
2637 }
2638 // BMI2 parallel bit DEPOSIT -- __pdep64(value, mask) scatters the low bits
2639 // of `value` into the set-bit positions of `mask` (x86 pdep). Pure, 2 i64
2640 // operands -> i64. varint-encode, bitboard-scatter, bit-interleave.
2641 if streq_n(name, "__pdep64", 8) == 1 {
2642 if peek_kind(P) == TK_LPAREN {
2643 advance_tok(P)
2644 let pdv: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2645 let pdm: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2646 return ir_emit_binop(P.current_bb, OP_PDEP, pdv, pdm, ir_type_i64())
2647 }
2648 }
2649 // BMI2 parallel bit EXTRACT -- __pext64(value, mask) gathers the `value`
2650 // bits at the set positions of `mask` down to the low bits (x86 pext; the
2651 // inverse of pdep). Pure, 2 i64 operands -> i64. varint-decode,
2652 // bitboard-gather, unicode-transcode, compression.
2653 if streq_n(name, "__pext64", 8) == 1 {
2654 if peek_kind(P) == TK_LPAREN {
2655 advance_tok(P)
2656 let pxv: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2657 let pxm: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2658 return ir_emit_binop(P.current_bb, OP_PEXT, pxv, pxm, ir_type_i64())
2659 }
2660 }
2661 // Native add-with-carry -- G3 substrate. __adc_acc(acc_ptr, lo, hi) adds the
2662 // 128-bit (hi:lo) into the 3-word accumulator {acc[0],acc[1],acc[2]} with carry
2663 // (contiguous addq;adcq;adcq). Void/statement-form (returns const 0).
2664 if streq_n(name, "__adc_acc", 9) == 1 {
2665 if peek_kind(P) == TK_LPAREN {
2666 advance_tok(P)
2667 let aa_p: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2668 let aa_lo: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2669 let aa_hi: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2670 ir_emit_adc_acc(P.current_bb, aa_p, aa_lo, aa_hi)
2671 return safe_const_i64(P, 0, "parse.nx:adc-acc-0" as *u8)
2672 }
2673 }
2674 // CPU feature detection -- the BMI2/ADX gate. __cpuid_ebx(leaf, subleaf) runs
2675 // x86 cpuid and returns the EBX register (zero-extended). cpuid(7,0):EBX has
2676 // bit-8=BMI2, bit-19=ADX. Pure (deterministic for a given CPU).
2677 if streq_n(name, "__cpuid_ebx", 11) == 1 {
2678 if peek_kind(P) == TK_LPAREN {
2679 advance_tok(P)
2680 let cq_l: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2681 let cq_s: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2682 return ir_emit_binop(P.current_bb, OP_CPUID_EBX, cq_l, cq_s, ir_type_i64())
2683 }
2684 }
2685 // Scalar bit unops -- C bootstrap parity (parse.c OP_BSWAP64/CLZ32/
2686 // CTZ32/POPCNT64). Each takes exactly 1 argument.
2687 if streq_n(name, "__bswap64", 9) == 1 {
2688 if peek_kind(P) == TK_LPAREN {
2689 advance_tok(P)
2690 let bsv: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2691 return ir_emit_unop(P.current_bb, OP_BSWAP64, bsv, ir_type_i64())
2692 }
2693 }
2694 if streq_n(name, "__clz32", 7) == 1 {
2695 if peek_kind(P) == TK_LPAREN {
2696 advance_tok(P)
2697 let czv: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2698 return ir_emit_unop(P.current_bb, OP_CLZ32, czv, ir_type_i64())
2699 }
2700 }
2701 if streq_n(name, "__ctz32", 7) == 1 {
2702 if peek_kind(P) == TK_LPAREN {
2703 advance_tok(P)
2704 let tzv: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2705 return ir_emit_unop(P.current_bb, OP_CTZ32, tzv, ir_type_i64())
2706 }
2707 }
2708 if streq_n(name, "__popcnt64", 10) == 1 {
2709 if peek_kind(P) == TK_LPAREN {
2710 advance_tok(P)
2711 let pcv: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2712 return ir_emit_unop(P.current_bb, OP_POPCNT64, pcv, ir_type_i64())
2713 }
2714 }
2715 // __rdtsc() -- zero-arg cycle-counter read. The unop carries a dummy
2716 // const-0 operand (the lowering ignores it and reads the HW counter).
2717 if streq_n(name, "__rdtsc", 7) == 1 {
2718 if peek_kind(P) == TK_LPAREN {
2719 advance_tok(P); match_kind(P, TK_RPAREN)
2720 let rd0: i64 = safe_const_i64(P, 0, "__rdtsc" as *u8)
2721 return ir_emit_unop(P.current_bb, OP_RDTSC, rd0, ir_type_i64())
2722 }
2723 }
2724 // Atomic intrinsics -- C bootstrap parity (parse.c). Memory order
2725 // is the last argument (NX_MO_* const).
2726 if streq_n(name, "__atomic_load_i64", 17) == 1 {
2727 if peek_kind(P) == TK_LPAREN {
2728 advance_tok(P)
2729 let al_a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2730 let al_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2731 return ir_emit_atomic_load_i64(P.current_bb, al_a, al_mo)
2732 }
2733 }
2734 if streq_n(name, "__atomic_store_i64", 18) == 1 {
2735 if peek_kind(P) == TK_LPAREN {
2736 advance_tok(P)
2737 let as_a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2738 let as_v: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2739 let as_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2740 ir_emit_atomic_store_i64(P.current_bb, as_a, as_v, as_mo)
2741 return safe_const_i64(P, 0, "parse.nx:atomic-store-0" as *u8)
2742 }
2743 }
2744 if streq_n(name, "__atomic_cas_i64", 16) == 1 {
2745 if peek_kind(P) == TK_LPAREN {
2746 advance_tok(P)
2747 let cs_a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2748 let cs_e: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2749 let cs_n: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2750 let cs_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2751 return ir_emit_atomic_cas_i64(P.current_bb, cs_a, cs_e, cs_n, cs_mo)
2752 }
2753 }
2754 if streq_n(name, "__atomic_faa_i64", 16) == 1 {
2755 if peek_kind(P) == TK_LPAREN {
2756 advance_tok(P)
2757 let fa_a: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2758 let fa_d: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2759 let fa_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2760 return ir_emit_atomic_faa_i64(P.current_bb, fa_a, fa_d, fa_mo)
2761 }
2762 }
2763 if streq_n(name, "__atomic_fence", 14) == 1 {
2764 if peek_kind(P) == TK_LPAREN {
2765 advance_tok(P)
2766 let fe_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2767 ir_emit_atomic_fence(P.current_bb, fe_mo)
2768 return safe_const_i64(P, 0, "parse.nx:atomic-fence-0" as *u8)
2769 }
2770 }
2771 // __thread_clone(stack_top, entry_fn, ctx) -> child_tid.
2772 if streq_n(name, "__thread_clone", 14) == 1 {
2773 if peek_kind(P) == TK_LPAREN {
2774 advance_tok(P)
2775 let tc_s: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2776 let tc_e: i64 = parse_expr(P); match_kind(P, TK_COMMA)
2777 let tc_c: i64 = parse_expr(P); match_kind(P, TK_RPAREN)
2778 return ir_emit_thread_clone(P.current_bb, tc_s, tc_e, tc_c)
2779 }
2780 }
2781 return -1
2782}
2783
2784func parse_primary_call(P: *Parser, name: *u8) -> i64 {
2785 // CALL-SITE ANCHOR (2026-08-05). Capture the position of the CALL ITSELF at entry: the caller
2786 // already consumed the identifier, so it sits one token back. Reading P.pos at ERROR time
2787 // instead points at whatever the parser reached AFTER the call -- measured: the caret printed
2788 // under the NEXT statement (sys_exit(0)) for an error on the line above it.
2789 // A CARET THAT POINTS AT THE WRONG LINE IS WORSE THAN NO CARET: it sends the reader to
2790 // innocent code, which is exactly the confusion the feature exists to remove.
2791 var call_ln: i64 = 0
2792 var call_cl: i64 = 0
2793 if P.pos >= 1 {
2794 let ct: *Tok = tok_at(P.toks, P.pos - 1)
2795 call_ln = ct.line
2796 call_cl = ct.col
2797 }
2798 advance_tok(P)
2799 // 24 slots -- Instr has op0..op23 (24 operand slots after the
2800 // 2026-06-18 IR extension to support >16-arg calls, e.g. bi-predicted
2801 // H.264 chroma reconstruction). Was 4 then 8 then 16 slots; silent
2802 // overflow past the buffer corrupted adjacent memory and made
2803 // ir_emit_call read garbage into the high op slots. See PREVENT lint
2804 // bench/nx_call_arity_overflow_audit.sh.
2805 let args_raw: *u8 = sys_mmap(24 * 8 + 16)
2806 let args: *i64 = args_raw as *i64
2807 var n_args: i64 = 0
2808 if peek_kind(P) != TK_RPAREN {
2809 let a0: i64 = parse_expr(P)
2810 args[n_args] = a0
2811 n_args = n_args + 1
2812 while peek_kind(P) == TK_COMMA {
2813 advance_tok(P)
2814 if n_args >= 24 {
2815 parse_die("function call exceeds 24 args (IR cap)" as *u8, 39)
2816 }
2817 let an: i64 = parse_expr(P)
2818 args[n_args] = an
2819 n_args = n_args + 1
2820 }
2821 }
2822 match_kind(P, TK_RPAREN)
2823 var nlen: i64 = 0
2824 while name[nlen] != 0 { nlen = nlen + 1 }
2825 let callee: *Function = find_function(P.module, name, nlen)
2826 if callee == (0 as *Function) {
2827 // fp(args): if `name` is a func-pointer-typed LOCAL, emit an INDIRECT call through it (call *%r11)
2828 // instead of failing as an undefined function. Completes the func-pointer feature (the thread-pool
2829 // `fp(task.ctx)` shape). MVP caps at 6 args (register-only codegen).
2830 let fpl: *Local = find_local(P, name)
2831 if fpl != (0 as *Local) {
2832 if fpl.ty != (0 as *Type) {
2833 if fpl.ty.kind == TY_FUNC {
2834 if n_args > NX_FNPTR_MAX_ARGS { parse_die("indirect fn-ptr call exceeds max args (IR operand cap)" as *u8, 32) }
2835 var fpv: i64 = fpl.value_id
2836 if fpl.is_alloca == 1 { fpv = ir_emit_load(P.current_bb, fpl.value_id, fpl.ty) }
2837 var fret: *Type = fpl.ty.pointee
2838 if fret == (0 as *Type) { fret = ir_type_i64() }
2839 return ir_emit_call_indirect(P.current_bb, fpv, fret, args, n_args)
2840 }
2841 }
2842 }
2843 // Unresolved call. prepass_register_funcs registers EVERY real
2844 // function before any body is parsed, so reaching here means
2845 // `name` is genuinely undefined. Returning 0 here used to
2846 // SILENTLY produce value-id-0 (== first arg), turning a missing
2847 // compiler intrinsic into a no-op -- the SHA-512/__rotr64 bug
2848 // (SITES-LIVE 2026-05-27). Fail LOUD instead. A `__`-prefixed
2849 // name that lands here is an UNHANDLED INTRINSIC (it should have
2850 // matched parse_primary_intrinsic); anything else is a typo or a
2851 // missing import. Either way it must never silently no-op.
2852 if name[0] == 0x5F {
2853 if name[1] == 0x5F {
2854 sys_write(2, "nx_parse: unhandled __ intrinsic: " as *u8, 34)
2855 sys_write(2, name, nlen)
2856 sys_write(2, "\n" as *u8, 1)
2857 nx_diag_note_error()
2858 return 0
2859 }
2860 }
2861 sys_write(2, "nx_parse: line " as *u8, 15)
2862 nx_put_dec_err(call_ln)
2863 sys_write(2, ": call to undefined function '" as *u8, 30)
2864 sys_write(2, name, nlen)
2865 sys_write(2, "'" as *u8, 1)
2866 nx_dym_suggest_fn(P.module, name, nlen)
2867 sys_write(2, "\n" as *u8, 1)
2868 nx_diag_caret(call_ln, call_cl)
2869 nx_dym_note(P)
2870 nx_diag_note_error()
2871 // RECOVERY: value id 0. Mechanically safe for the parser (it was the
2872 // old silent behavior); the end-of-parse gate keeps it out of codegen.
2873 return 0
2874 }
2875 // ---- CALL-ARITY CHECK (2026-07-20) ------------------------------------
2876 // nx_cc had NO arity checking: a call missing an argument compiled SILENTLY
2877 // and the callee read a GARBAGE REGISTER -- then WROTE THROUGH IT when the
2878 // parameter was an out-pointer. Symptoms are protean (impossible counter
2879 // values; a SEGV that MOVES when you refactor, because the register content
2880 // shifts), which is why it cost ~4 debug cycles in nx_step_tess2 and got
2881 // banked as a standing ★★★ gotcha. Same family as the undefined-function
2882 // fix above: a silent wrong-value is strictly worse than a loud stop.
2883 //
2884 // COVERAGE IS NOW TOTAL. This check was first landed gated on `callee.n_blocks > 0` (the
2885 // codebase's own "has a real body" test) because prepass_register_funcs registered a stub per
2886 // name WITHOUT parsing its parameter list, so a not-yet-defined callee reported n_params == 0
2887 // and checking it would have rejected every forward call -- including this compiler's own
2888 // source. That root cause is now FIXED at the source: prepass_count_params records the declared
2889 // arity on the stub before any body is parsed, so EVERY callee reachable by name has a known
2890 // signature and the guard is no longer needed. Forward calls and mutual recursion are covered.
2891 if n_args != callee.n_params {
2892 sys_write(2, "nx_parse: line " as *u8, 15)
2893 nx_put_dec_err(call_ln)
2894 sys_write(2, ": call arity mismatch: " as *u8, 23)
2895 sys_write(2, name, nlen)
2896 sys_write(2, " expects " as *u8, 9)
2897 nx_put_dec_err(callee.n_params)
2898 sys_write(2, " arg(s), got " as *u8, 13)
2899 nx_put_dec_err(n_args)
2900 sys_write(2, "\n" as *u8, 1)
2901 nx_diag_caret(call_ln, call_cl)
2902 nx_diag_note_error()
2903 }
2904 // ---- CALL ARGUMENT TYPE CHECK (2026-08-01) ----------------------------
2905 // nx_cc checked ARITY but never TYPES: a pointer passed where an i64 was declared,
2906 // and an integer passed where a pointer was declared, both compiled CLEAN. That is
2907 // the root of the silent-acceptance family. It is not theoretical -- it is how the
2908 // sev-8 evidence bug happened (debt 1785518763): ev_parse_pass passed an mmap'd
2909 // *i64 out-pointer into `kl: i64`, where it became the bound of `while i <= n - kl`;
2910 // the loop never executed and an evidence organ reported a ZERO tally for every gate.
2911 //
2912 // DELIBERATELY NARROW: POINTER-vs-INTEGER only. That distinction is unambiguous and
2913 // is the class that produces wild-pointer and impossible-bound bugs. Integer WIDTH
2914 // (i32 into i64) and struct identity are NOT checked here -- the corpus converts
2915 // freely between integer widths and rejecting that would be a different, much larger
2916 // change. Widen only as far as the evidence supports.
2917 //
2918 // SKIPPED when the callee's mask is 0 = signature not yet parsed (forward call to a
2919 // stub). Guessing a signature there is how the arity check first broke the compiler's
2920 // own source; a check that cannot know must decline, not invent.
2921 if callee.param_ptr_mask != 0 {
2922 var ai: i64 = 0
2923 while ai < n_args {
2924 let av: *Value = val_at(P.current_fn, args[ai])
2925 var arg_is_ptr: i64 = 0
2926 var arg_known: i64 = 0
2927 if av != (0 as *Value) {
2928 let aty: *Type = av.ty
2929 if aty != (0 as *Type) {
2930 arg_known = 1
2931 if aty.kind == TY_PTR { arg_is_ptr = 1 }
2932 // an integer LITERAL is routinely used as a null/sentinel pointer;
2933 // only a *typed* value carries a real claim about pointerness.
2934 if av.kind == VK_CONST_INT { arg_known = 0 }
2935 }
2936 }
2937 var want_ptr: i64 = 0
2938 if (callee.param_ptr_mask & (1 << ai)) != 0 { want_ptr = 1 }
2939 // ONLY THE UNAMBIGUOUS DIRECTION: a value typed TY_PTR is definitely a
2940 // pointer, so passing it to an integer parameter is definitely wrong. The
2941 // reverse is NOT decidable here: an argument typed i64 may be a genuine
2942 // integer OR the PLACEHOLDER type of a call to a function defined later with
2943 // no forward declaration (the prepass stub carries i64 until its body is
2944 // parsed). Measured: checking that direction gave a 106/120 false-positive
2945 // rate, all of it placeholder types, not real defects.
2946 // This keeps the direction that produced the sev-8 ev_num_after silent-zero
2947 // (a *i64 out-pointer landing in `kl: i64`) and drops the one that cannot be
2948 // decided without a real type-inference pass. Widen only as far as the
2949 // evidence supports.
2950 // POINTER-INTO-INTEGER ONLY -- and this narrowness is MEASURED, not timid.
2951 //
2952 // A value typed TY_PTR is definitely a pointer, so handing it to an integer
2953 // parameter is definitely wrong: that is the sev-8 ev_num_after shape (an
2954 // mmap'd *i64 becoming a loop bound) and it costs 0 false positives over the
2955 // corpus sample.
2956 //
2957 // The REVERSE direction is enabled by flipping the commented line below, and
2958 // it was TRIED on 2026-08-01. Two separate causes of i64-looking arguments
2959 // were found and fixed -- return types were hardcoded ir_type_i64() for every
2960 // function, and forward stubs carried an i64 placeholder (prepass now records
2961 // return pointer-ness). Both real bugs, both fixed. But the corpus STILL
2962 // rejects 57/70 on `sys_munmap` arg 1 from inside the syscall layer's own
2963 // expansion, and that residue is NOT yet explained. Isolated repros of every
2964 // shape tried -- casts, pointer locals, call results, forward decls -- are all
2965 // quiet, so a third cause remains unfound.
2966 // DO NOT ENABLE IT UNTIL THAT RESIDUE IS EXPLAINED. Shipping it would reject
2967 // most of the corpus for a reason nobody has yet named, which is not a fix.
2968 var bad: i64 = 0
2969 if arg_known == 1 {
2970 if arg_is_ptr == 1 { if want_ptr == 0 { bad = 1 } }
2971 // BOTH DIRECTIONS: still DISABLED, and the residue is now PARTLY explained + MEASURED.
2972 // CAUSE FOUND AND FIXED 2026-08-05: the trailing `as T` cast MUTATED THE OPERAND'S
2973 // Value IN PLACE (parse_unary), so a local that ever appeared as `p as i64` read as
2974 // INTEGER at every LATER call -- exactly the syscall layer's timespec pattern before
2975 // sys_munmap(req). Cast now emits a typed identity; witnesses nx_probe_castmut_live.nx
2976 // (mutation reproduced pre-fix) + castmut2 (per-value, not check-wide). That fix is
2977 // LIVE (equiv 10/10 + selfhost, toolchain-promoted canary GREEN).
2978 // BUT IT IS NOT THE WHOLE RESIDUE, MEASURED NOT ASSUMED: flipping this line on the
2979 // FIXED compiler still fails 4 of 10 equiv rows -- nx_jpeg_ascii_test,
2980 // nx_tls13_client_session_recv_sh_test, nx_p256_keyshare_test,
2981 // nx_tls13_p256_loopback_test (build_b=1, i.e. the CHALLENGER refuses to compile them;
2982 // the other 6 rows + selfhost stay GREEN). Residue narrowed from 57/70 corpus targets
2983 // to 4 large crypto/codec modules = a SECOND, still-unnamed type-loss path.
2984 // NEXT INVESTIGATOR: build one of those 4 with a both-directions compiler and read the
2985 // exact `argument type mismatch` line + callee; that names cause #2.
2986 if arg_is_ptr != want_ptr { bad = 1 }
2987 }
2988 if bad == 1 {
2989 // parse_primary_call has no name token in scope; take the line from
2990 // the parser's current position, which is at/just past the call.
2991 let cur_tok: *Tok = tok_at(P.toks, P.pos)
2992 let cur_line: i64 = cur_tok.line
2993 sys_write(2, "nx_parse: line " as *u8, 15)
2994 nx_put_dec_err(cur_line)
2995 sys_write(2, ": argument type mismatch in call to '" as *u8, 37)
2996 sys_write(2, name, nlen)
2997 sys_write(2, "': arg " as *u8, 7)
2998 nx_put_dec_err(ai + 1)
2999 if want_ptr == 1 {
3000 sys_write(2, " is an INTEGER but the parameter is a POINTER\n" as *u8, 46)
3001 }
3002 if want_ptr == 0 {
3003 sys_write(2, " is a POINTER but the parameter is an INTEGER\n" as *u8, 46)
3004 }
3005 nx_diag_note_error()
3006 }
3007 ai = ai + 1
3008 }
3009 }
3010 let r: i64 = ir_emit_call(P.current_bb, callee, args, n_args)
3011 if callee.ret_ty != (0 as *Type) {
3012 return parse_field_chain(P, r, callee.ret_ty)
3013 }
3014 return r
3015}
3016
3017// NUL-terminated name compare (2 data args -- keeps clear of the >=3-data-arg helper miscompile class)
3018func mc_name_eq(a: *u8, b: *u8) -> i64 {
3019 var i: i64 = 0
3020 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
3021 if b[i] != (0 as u8) { return 0 }
3022 return 1
3023}
3024
3025// Is `name` declared as a module `const` LATER in the token stream? Non-destructive: saves and
3026// restores P.pos exactly as the prepasses do. Only ever called on the give-up path of an identifier
3027// lookup, so the scan cost is paid solely on what is already an error.
3028func mconst_declared_later(P: *Parser, name: *u8) -> i64 {
3029 let saved: i64 = P.pos
3030 var found: i64 = 0
3031 var go: i64 = 1
3032 while go == 1 {
3033 let k: i64 = peek_kind(P)
3034 if k == TK_EOF {
3035 go = 0
3036 } else {
3037 if k == TK_CONST {
3038 advance_tok(P)
3039 if peek_kind(P) == TK_IDENT {
3040 let t: *Tok = advance_tok(P)
3041 if mc_name_eq(tok_text_ptr(t), name) == 1 { found = 1; go = 0 }
3042 }
3043 } else {
3044 advance_tok(P)
3045 }
3046 }
3047 }
3048 P.pos = saved
3049 return found
3050}
3051
3052func parse_primary_local_or_const(P: *Parser, name: *u8) -> i64 {
3053 let L: *Local = find_local(P, name)
3054 if L != (0 as *Local) {
3055 var v: i64 = L.value_id
3056 var v_pass_ty: *Type = L.ty // type handed to parse_field_chain; unwrapped once for static-ptr subscripts
3057 if L.is_alloca {
3058 if L.ty_kind != TY_STRUCT {
3059 if L.ty_kind != TY_ARRAY {
3060 if peek_kind(P) != TK_DOT {
3061 if peek_kind(P) != TK_LBRACKET {
3062 // Load the local's value at its DECLARED type
3063 // (L.ty), not hardcoded i64. Required for u8 /
3064 // i32 / pointer locals to widen / read correctly.
3065 // ARRAY (like STRUCT) is EXCLUDED here: a bare array name
3066 // must DECAY to its frame address, not load its first bytes.
3067 var lty: *Type = L.ty
3068 if lty == (0 as *Type) { lty = ir_type_i64() }
3069 // STATIC SCALAR TYPE FIX (2026-08-01). A static's injected Local is deliberately
3070 // typed *(decl_ty) -- the slot ADDRESS -- so field/index chains get the right
3071 // stride (see inject_statics). But LOADING that slot yields a value of the
3072 // DECLARED type, so the loaded Value must carry L.ty.pointee, NOT L.ty.
3073 // Without this every `static X: i64` read is recorded as TY_PTR, and the call
3074 // argument type check added earlier today then rejects the VALID line
3075 // `cd2_sputi(cg_nn)` as "arg 1 is a POINTER but the parameter is an INTEGER".
3076 // ★★★★★A TYPE THAT WAS ONLY EVER USED FOR ADDRESSING BECAME A CLAIM ABOUT THE
3077 // VALUE THE MOMENT SOMETHING STARTED READING IT -- the slot type was harmlessly
3078 // wrong for as long as nothing consulted it, and the new checker is what turned
3079 // a latent mislabel into 1-in-10 organs refusing to compile.
3080 // Same VK_GLOBAL-gated unwrap already proven on the static-ptr field-read path
3081 // below; ordinary alloca locals are untouched, so they stay byte-identical.
3082 // CODEGEN IS UNCHANGED CORPUS-WIDE, measured not assumed: load_ty drives load
3083 // WIDTH, and the corpus has 366 i64 statics (8 bytes either way), 305 pointer
3084 // statics (8 bytes either way) and ZERO narrow (u8/i32/i16) scalar statics, the
3085 // only shape whose width this could alter.
3086 let lv_ld: *Value = val_at(P.current_fn, L.value_id)
3087 if lv_ld.kind == VK_GLOBAL {
3088 if L.ty != (0 as *Type) {
3089 if L.ty.pointee != (0 as *Type) { lty = L.ty.pointee }
3090 }
3091 }
3092 return ir_emit_load(P.current_bb, L.value_id, lty)
3093 }
3094 }
3095 }
3096 }
3097 }
3098 // F-meta-3 fix (Session 12): when the local is alloca-backed
3099 // AND its declared type is a pointer-to-struct (e.g. function
3100 // param `out: *Struct`), the alloca slot holds the POINTER
3101 // value, not the struct. parse_field_chain expects `base` to
3102 // BE the pointer; if we pass the alloca slot directly it does
3103 // GEP on the slot and reads stack garbage. Load the pointer
3104 // out of the slot first. Sibling fix to parse_stmt_ident_dot.
3105 if L.is_alloca == 1 {
3106 if L.ty != (0 as *Type) {
3107 if L.ty.kind == TY_PTR {
3108 if peek_kind(P) == TK_DOT {
3109 // Loaded value is the pointer itself (type L.ty).
3110 v = ir_emit_load(P.current_bb, L.value_id, L.ty)
3111 // Static-ptr FIELD-READ fix (2026-07-10; mirrors the LBRACKET multi-static fix
3112 // below): a STATIC's injected Local is typed *(decl_ty) -- the slot ADDRESS -- so
3113 // after the load v holds the REAL pointer, whose type is L.ty.pointee. Passing the
3114 // un-unwrapped **Struct made parse_field_chain's struct lookup FAIL -> documented
3115 // bail -> `g.field` READ returned the RAW POINTER (nx_static_field_probe witness).
3116 // VK_GLOBAL-gated so ordinary alloca ptr locals stay byte-identical (equiv-safe).
3117 let lv_d: *Value = val_at(P.current_fn, L.value_id)
3118 if lv_d.kind == VK_GLOBAL {
3119 if L.ty.pointee != (0 as *Type) { v_pass_ty = L.ty.pointee }
3120 }
3121 }
3122 // Multi-static bug fix: a STATIC pointer's value_id is a VK_GLOBAL (the slot ADDRESS),
3123 // which materialises as-address (leaq) and does NOT auto-load like an OP_ALLOCA. So a
3124 // `staticptr[i]` READ subscripted the slot address -> returned the pointer, not *pointer
3125 // (write worked, read was wrong -- silent corruption with >=1 static pointer read). Load
3126 // the pointer from the slot first, ONLY for VK_GLOBAL, so var/alloca pointers (which
3127 // auto-load) stay byte-identical -> equiv gate unaffected.
3128 if peek_kind(P) == TK_LBRACKET {
3129 let lv_g: *Value = val_at(P.current_fn, L.value_id)
3130 if lv_g.kind == VK_GLOBAL {
3131 // STATIC ARRAY vs STATIC POINTER -- they need OPPOSITE treatment here, and
3132 // conflating them is why `static a: [N]T` has been a documented daemon-killer.
3133 // A static POINTER's slot HOLDS a pointer, so it must be loaded. A static
3134 // ARRAY's slot IS the array, so its ADDRESS is already the base and loading it
3135 // reads ELEMENT 0 AND USES IT AS THE BASE. .lcomm zero-fills, so that base was
3136 // 0 and the first indexed access wrote to a near-null address -- compiles
3137 // clean, dies on touch, exactly as witness nx_rw_staticarr.nx records.
3138 // This is the static sibling of the local array-decay rule below.
3139 var g_is_arr: i64 = 0
3140 if L.ty.pointee != (0 as *Type) {
3141 if L.ty.pointee.kind == TY_ARRAY { g_is_arr = 1 }
3142 }
3143 if g_is_arr == 0 {
3144 v = ir_emit_load(P.current_bb, L.value_id, L.ty)
3145 }
3146 // Static-ptr STRIDE fix: inject_statics types the Local as *(e.ty) (the slot
3147 // ADDRESS). After the load, v holds the REAL pointer (e.ty); its element is
3148 // e.ty.pointee, so the stride must come from L.ty.pointee, not L.ty. Was: a *u8
3149 // static -> stride 8 (sizeof *u8) instead of 1 -> ran off the buffer on large
3150 // indices (nx_drbench SIGSEGV). *i64 statics were unaffected (8 == correct).
3151 // For the ARRAY case this same assignment hands the postfix handler the ARRAY
3152 // type, which is what makes it GEP from the base and bounds-check the index.
3153 if L.ty.pointee != (0 as *Type) { v_pass_ty = L.ty.pointee }
3154 }
3155 }
3156 }
3157 }
3158 }
3159 // Pass the local's actual type so parse_field_chain can
3160 // emit GEP+LOAD for o.field accesses. Closes T#bar3-codegen-001
3161 // (was passing 0 as *Type which made field_chain bail without
3162 // emitting, causing nxc.elf to return the raw pointer instead
3163 // of o.field for `let o: *S = ...; return o.field`).
3164 // Array DECAY: a bare array name (not indexed, not dotted) yields its stack ADDRESS as a
3165 // pointer VALUE (C-style array->*elem decay), so `sys_write(1, buf, n)` gets the address.
3166 // Emit OP_ADDR_OF for the leaq path -- returning the raw alloca id auto-loads the array's
3167 // first bytes as a bogus pointer (same trap the &name path documents).
3168 if L.is_alloca == 1 {
3169 if L.ty_kind == TY_ARRAY {
3170 if peek_kind(P) != TK_LBRACKET {
3171 if peek_kind(P) != TK_DOT {
3172 let pta: *Type = alloc_type(TY_PTR, 8, 8)
3173 pta.pointee = L.ty.pointee
3174 return ir_emit_unop(P.current_bb, OP_ADDR_OF, L.value_id, pta)
3175 }
3176 }
3177 }
3178 }
3179 return parse_field_chain(P, v, v_pass_ty)
3180 }
3181 let out_raw: *u8 = sys_mmap(16)
3182 let out: *i64 = out_raw as *i64
3183 *out = 0
3184 if lookup_mconst(P, name, out) == 1 {
3185 return safe_const_i64(P, *out, "parse.nx:mconst-out" as *u8)
3186 }
3187 // V-LANGEXT M1: string-const lookup. Same shape as inline TK_STRING
3188 // emission at parse_primary (~line 873): emit a VAL_GLOBAL_ADDR
3189 // value via ir_global_value with the stored pointer type.
3190 let sgid_raw: *u8 = sys_mmap(16)
3191 let sgid: *i64 = sgid_raw as *i64
3192 let sty_raw: *u8 = sys_mmap(16)
3193 let sty: **Type = sty_raw as **Type
3194 *sgid = 0
3195 *sty = 0 as *Type
3196 if lookup_mconst_string(P, name, sgid, sty) == 1 {
3197 // ROOT FIX seq1552 (2026-07-30): a STRING module-const IS a pointer value, so a use site can
3198 // legitimately carry a postfix `[i]` or `.field` exactly like a pointer local. This path used
3199 // to hand back the global address WITHOUT running the postfix chain, so the `[` `i` `]` tokens
3200 // of `CONST[i]` were left in the stream; the enclosing expression then desynced and the program
3201 // COMPILED CLEAN while summing the wrong bytes. Witness pair (byte-identical but for one line):
3202 // nx_constidx_probe.nx (WRONG-SUM, exit 1) vs nx_constidx_ctrl.nx (sum=294, exit 0).
3203 // The chain is entered ONLY when a postfix token actually follows, so every existing use site
3204 // takes the byte-identical old path -- and module-level contexts (P.current_fn == 0, see the
3205 // reset at the end of parse_function) never reach parse_field_chain's current_fn assert.
3206 let gcv: i64 = ir_global_value(P.current_fn, *sgid, *sty)
3207 if peek_kind(P) == TK_LBRACKET { return parse_field_chain(P, gcv, *sty) }
3208 if peek_kind(P) == TK_DOT { return parse_field_chain(P, gcv, *sty) }
3209 return gcv
3210 }
3211 // Bare FUNCTION name used as a VALUE (not a call): yields the function's code address
3212 // (VK_FUNC_ADDR -> `leaq <fn>(%rip)`). This is what nx_thread_pool passes as a worker arg and
3213 // what __thread_clone(top, worker, ctx) needs -- previously this fell through to `return 0`
3214 // (constant 0), giving a NULL entry -> the SIGSEGV (exit 139). Shares the &fn mechanism.
3215 var fv_nl: i64 = 0
3216 while name[fv_nl] != 0 { fv_nl = fv_nl + 1 }
3217 let fv_fn: *Function = find_function(P.module, name, fv_nl)
3218 if fv_fn != (0 as *Function) {
3219 let fv_ft: *Type = alloc_type(TY_FUNC, 8, 8)
3220 fv_ft.pointee = fv_fn.ret_ty
3221 return ir_func_addr_value(P.current_fn, fv_fn, fv_ft)
3222 }
3223 // ⚠UNKNOWN IDENTIFIER STILL FALLS THROUGH TO CONSTANT ZERO -- A KNOWN, UNFIXED HOLE.
3224 // Reaching here means `name` resolved to NOTHING: not a local, not a module const, not a string
3225 // const, not a function. Returning 0 silently emits the CONSTANT ZERO in its place, so a typo --
3226 // or the banked case of a `let` local referenced ABOVE its declaration -- becomes 0 with no
3227 // diagnostic and SEGVs far from the cause. It is the same silent-value-id-0 class already killed
3228 // for undefined CALLS (parse_primary_call) and, just above, for bare function names.
3229 //
3230 // A fail-loud version WAS built and tested here 2026-07-20 and is NOT shipped, deliberately.
3231 // ★It paid for itself immediately: it caught `AT_FDCWD` being read as 0 by sys_unlinkat /
3232 // sys_fchmodat (a forward module-const reference) -- a live miscompile now fixed in
3233 // nx_syscalls.nx, with a standing witness at runtime/nx_fwdconst_probe.nx proving the class
3234 // (forward-read=0 vs direct-read=-100 in ONE binary).
3235 // ★But it also reports a FALSE POSITIVE that is not yet understood: `unknown identifier: m` while
3236 // parsing nx_opt.nx after `inl_clone_all`, where `m` is an ordinary FUNCTION PARAMETER
3237 // (`opt_inline_module(m: *Module)`) used normally -- i.e. find_local misses a live parameter in
3238 // some context. Shipping a compiler diagnostic whose false positive is unexplained would trade a
3239 // silent-wrong-value bug for a can't-build bug, so it stays out until that is root-caused.
3240 // NEXT RUNG: root-cause the find_local miss (print the enclosing function in the diagnostic to
3241 // self-locate it), then land this exactly like the call-arity check was landed.
3242 //
3243 // ---- WHAT *IS* SHIPPED: the FORWARD MODULE-CONST slice, caught precisely -----------------
3244 // The blanket check is unsafe (above), but ONE slice of it is both safe and high value. If
3245 // `name` is declared as a module `const` LATER in the token stream, this is unambiguously the
3246 // forward-module-const miscompile -- the one that made AT_FDCWD read 0 instead of -100 and so
3247 // put dirfd=0 into sys_unlinkat/sys_fchmodat. No legitimate program has that shape, and it
3248 // CANNOT false-positive the way the blanket check does: an ordinary parameter (the `m` case)
3249 // is never also declared as a later const. Witness: runtime/nx_fwdconst_probe.nx.
3250 if mconst_declared_later(P, name) == 1 {
3251 sys_write(2, "nx_parse: module const used before its declaration: " as *u8, 52)
3252 sys_write(2, name, fv_nl)
3253 sys_write(2, " -- it would silently read 0; move the const ABOVE its first reader\n" as *u8, 68)
3254 nx_diag_note_error()
3255 return 0
3256 }
3257 // seq1012 FAIL-LOUD (landed 2026-07-29): an identifier that resolves to NOTHING is a
3258 // compile ERROR, never the silent constant 0 that wrote wrong data and relocated with
3259 // unrelated refactors. The 2026-07-20 attempt was withheld over ONE false positive
3260 // ("m" in fn inl_clone_all) -- the survey diagnostic located it as seq358 IN A MASK:
3261 // the multiline else-if desync fused functions, so live parameters resolved against the
3262 // wrong function. With the else-if production fixed, the whole corpus surveys CLEAN
3263 // (nx_compile_x86 + nx_wasm_craft + nx_game_page_emit: unresolved=0) and this check
3264 // lands exactly like the call-arity check did. Witness: runtime/nx_undefprobe.nx.
3265 // The caller consumed the identifier before resolution began, so its own token sits one
3266 // slot back (the parse_primary_call anchoring lesson: never take the line at error time
3267 // from the CURRENT position -- take the construct's own token).
3268 var uid_ln: i64 = 0
3269 var uid_cl: i64 = 0
3270 if P.pos > 0 {
3271 let uid_tok: *Tok = tok_at(P.toks, P.pos - 1)
3272 uid_ln = uid_tok.line
3273 uid_cl = uid_tok.col
3274 }
3275 sys_write(2, "nx_parse: line " as *u8, 15)
3276 nx_put_dec_err(uid_ln)
3277 sys_write(2, ": UNRESOLVED identifier '" as *u8, 25)
3278 sys_write(2, name, fv_nl)
3279 sys_write(2, "' in fn " as *u8, 8)
3280 if P.current_fn != (0 as *Function) {
3281 sys_write(2, P.current_fn.name_start as *u8, P.current_fn.name_len)
3282 }
3283 sys_write(2, " -- not a local/param, const, string const, or function" as *u8, 55)
3284 nx_dym_suggest_ident(P, name, fv_nl)
3285 sys_write(2, "\n" as *u8, 1)
3286 nx_diag_caret(uid_ln, uid_cl)
3287 nx_dym_note(P)
3288 nx_diag_note_error()
3289 return 0
3290}
3291
3292// Prefix-unary level. The trailing `as T` cast lives HERE (the
3293// wrapper), NOT inside the operand parse, so prefix operators bind
3294// TIGHTER than `as`: `*p as T` == `(*p) as T` (Rust precedence).
3295// Pre-fix, the deref branch's recursive operand parse swallowed the
3296// cast -- `*p as *u8` parsed as `*(p as *u8)` and byte-loaded the
3297// POINTER CELL, then deref'd that garbage (SIGSEGV). Min repro:
3298// runtime/_derefcast_minrepro.nx (the B2-row landmine, 2026-06-10).
3299func parse_unary(P: *Parser) -> i64 {
3300 let base: i64 = parse_unary_core(P)
3301 // Trailing `as T` cast. The cast RESULT must carry target_ty so downstream
3302 // LOAD/STORE/GEP see the correct pointee -- but the OPERAND must keep its own
3303 // type. This used to do `bv.ty = target_ty` IN PLACE on the operand's Value:
3304 // when the operand is a bare local, that node IS the local's binding, so
3305 // `p as i64` flipped p to INTEGER for EVERY later reference. Proven minimal
3306 // 2026-08-05 (nx_probe_castmut_live.nx: a prior cast-in-argument-position made
3307 // the pointer-into-integer refusal VANISH; discriminator castmut2 shows it is
3308 // per-value) -- and it is the ROOT of the 57/70 sys_munmap-arg-1 residue that
3309 // kept the reverse call-arg type direction disabled (see the do-not-enable
3310 // note in the call type check). Fix: emit a typed IDENTITY (base + 0) so the
3311 // cast result is a FRESH Value id with target_ty and a legitimate result slot
3312 // in every backend; the optimizer may fold the identity -- harmless, because
3313 // type checks run at parse time and load/store widths are instruction-encoded.
3314 if peek_kind(P) == TK_AS {
3315 advance_tok(P)
3316 let target_ty: *Type = parse_type(P)
3317 if target_ty != (0 as *Type) {
3318 let czero: i64 = safe_const_i64(P, 0, "parse.nx:cast-identity-const0" as *u8)
3319 return ir_emit_binop(P.current_bb, OP_ADD, base, czero, target_ty)
3320 }
3321 }
3322 return base
3323}
3324
3325func parse_unary_core(P: *Parser) -> i64 {
3326 nx_assert_ptr(P.current_fn as *u8, "parse_unary: P.current_fn" as *u8)
3327 nx_assert(P.current_fn.values_cap > 0,
3328 "parse_unary: P.current_fn init" as *u8)
3329 let k: i64 = peek_kind(P)
3330 if k == TK_MINUS {
3331 advance_tok(P)
3332 let rhs: i64 = parse_unary(P)
3333 let zero: i64 = safe_const_i64(P, 0, "parse.nx:LINE-const0" as *u8)
3334 return ir_emit_binop(P.current_bb, OP_SUB, zero, rhs, ir_type_i64())
3335 }
3336 if k == TK_BANG {
3337 advance_tok(P)
3338 let rhs: i64 = parse_unary(P)
3339 let zero: i64 = safe_const_i64(P, 0, "parse.nx:LINE-const0" as *u8)
3340 return ir_emit_binop(P.current_bb, OP_EQ, rhs, zero, ir_type_bool())
3341 }
3342 // ~expr -- bitwise NOT (one's complement). Parallels TK_BANG above;
3343 // lowers to the dedicated OP_NOT (single x86 `notq` / rv64 `not`),
3344 // the optimal one-instruction form. Was MISSING from the function-
3345 // body parser (only the const-evaluator handled `~`), so `~x` in a
3346 // function body fell through to parse_primary, silently miscompiled,
3347 // AND desynced the token stream -- which surfaced downstream as the
3348 // misleading "address-of unknown local (&name)" when a binary `&`
3349 // followed (e.g. SHA-256 Choose: `((~e) & g) ...`). Now `~` is a
3350 // first-class prefix operator like -, !, &, *.
3351 if k == TK_TILDE {
3352 advance_tok(P)
3353 let rhs: i64 = parse_unary(P)
3354 return ir_emit_unop(P.current_bb, OP_NOT, rhs, ir_type_i64())
3355 }
3356 // &name[.field]* -- address-of, with optional field-chain walk.
3357 // Valid on alloca-backed vars; returns the alloca address (no
3358 // load) when there's no chain, or the GEP'd field address when
3359 // `.field.subfield...` follows.
3360 //
3361 // Extended 2026-05-16 per cardinal feedback-bits-up-canonical-
3362 // layer + user directive "never avoid a limitation bits up build":
3363 // previously rejected `&card.field`, forcing callers to inline
3364 // assignments. Now mirrors parse_stmt_ident_dot's chain walker
3365 // exactly, only returning the ADDRESS (no load, no store).
3366 //
3367 // STUB(parser, T#parser-001): non-alloca local case currently
3368 // absorbs the error by returning 0. Caller can't distinguish
3369 // "no such local" from "address-of-non-alloca-local"; either
3370 // path silently miscompiles.
3371 // Plan: distinguish via typed error sentinel or assert; depends
3372 // on a parser-wide error-reporting path that doesn't yet exist.
3373 // Closes when: parser grows a typed error channel.
3374 if k == TK_AMP {
3375 advance_tok(P)
3376 let name_tok: *Tok = advance_tok(P)
3377 let name: *u8 = tok_text_ptr(name_tok)
3378 let L: *Local = find_local(P, name)
3379 if L == (0 as *Local) {
3380 // Not a local -- maybe a FUNCTION: `&fn` yields the function's code address (VK_FUNC_ADDR),
3381 // emitted as `leaq <fn>(%rip), %reg`. Shares its mechanism with bare-fn-name-as-value.
3382 var amp_nl: i64 = 0
3383 while name[amp_nl] != 0 { amp_nl = amp_nl + 1 }
3384 let amp_fn: *Function = find_function(P.module, name, amp_nl)
3385 if amp_fn != (0 as *Function) {
3386 let amp_ft: *Type = alloc_type(TY_FUNC, 8, 8)
3387 amp_ft.pointee = amp_fn.ret_ty
3388 return ir_func_addr_value(P.current_fn, amp_fn, amp_ft)
3389 }
3390 parse_die("address-of unknown local (&name)" as *u8, 32)
3391 }
3392 // Bare `&name` (no .field chain): the ADDRESS of the local's
3393 // stack slot. Emit OP_ADDR_OF so codegen uses the as-address
3394 // (leaq) path; returning the raw alloca value-id auto-loads the
3395 // slot CONTENTS -> bogus pointer -> SEGV. Works for value-typed
3396 // (`var x: i64`) AND pointer-typed (`var p: *T`, out-params).
3397 if L.is_alloca == 1 {
3398 if peek_kind(P) != TK_DOT {
3399 let pt: *Type = alloc_type(TY_PTR, 8, 8)
3400 pt.pointee = L.ty
3401 return ir_emit_unop(P.current_bb, OP_ADDR_OF, L.value_id, pt)
3402 }
3403 }
3404 var v: i64 = L.value_id
3405 // Mirror the F-meta-3 fix in parse_stmt_ident_dot: when the
3406 // local is alloca-backed AND it's a pointer AND we're walking
3407 // a `.field` chain, load the pointer value out of the slot
3408 // first so GEP applies to the actual pointee, not the alloca.
3409 //
3410 // CRITICAL: do this ONLY when a `.field` chain follows. For
3411 // bare `&name` we want the ALLOCA ADDRESS (== L.value_id),
3412 // NOT the loaded pointer value. Pre-fix this overload broke
3413 // outparam patterns like `nx_media_pool_get(..., &src, ...)`
3414 // where the callee writes through the pointer expected to be
3415 // &src's slot -- the load made it write through src's value
3416 // (uninitialised 0 -> NULL deref).
3417 if L.is_alloca == 1 {
3418 if L.ty != (0 as *Type) {
3419 if L.ty.kind == TY_PTR {
3420 if peek_kind(P) == TK_DOT {
3421 v = ir_emit_load(P.current_bb, L.value_id, L.ty)
3422 }
3423 }
3424 }
3425 }
3426 var ty: *Type = L.ty
3427 while peek_kind(P) == TK_DOT {
3428 advance_tok(P)
3429 let ftok: *Tok = advance_tok(P)
3430 let fname: *u8 = tok_text_ptr(ftok)
3431 var flen: i64 = 0
3432 while fname[flen] != 0 { flen = flen + 1 }
3433 var stty: *Type = ty
3434 if stty != (0 as *Type) {
3435 if stty.kind == TY_PTR {
3436 if stty.pointee != (0 as *Type) { stty = stty.pointee }
3437 }
3438 }
3439 if stty == (0 as *Type) { break }
3440 let field: *StructField = ir_type_struct_find_field(stty, fname, flen)
3441 if field == (0 as *StructField) { break }
3442 let off2: i64 = safe_const_i64(P, field.offset, "parse.nx:amp-field-offset" as *u8)
3443 v = ir_emit_gep(P.current_bb, v, off2, field.ty)
3444 ty = field.ty
3445 }
3446 return v
3447 }
3448 // *expr -- pointer dereference in expression position. Loads
3449 // from the pointee. Compared with the `*name = expr` statement
3450 // path above, this one yields a value (not an lvalue).
3451 if k == TK_STAR {
3452 advance_tok(P)
3453 // Operand via CORE: a trailing `as` belongs to the deref
3454 // RESULT (wrapper), never to the address operand.
3455 let addr: i64 = parse_unary_core(P)
3456 // Result type = pointee of addr's type (NOT hardcoded i64).
3457 // Without this, `*(p: *u8)` loads 8 bytes instead of 1.
3458 let addr_val: *Value = val_at(P.current_fn, addr)
3459 var elem_ty: *Type = ir_type_i64()
3460 if addr_val.ty != (0 as *Type) {
3461 if addr_val.ty.kind == TY_PTR {
3462 if addr_val.ty.pointee != (0 as *Type) {
3463 elem_ty = addr_val.ty.pointee
3464 }
3465 }
3466 }
3467 return ir_emit_load(P.current_bb, addr, elem_ty)
3468 }
3469 // Trailing `as T` handling lives in the parse_unary wrapper
3470 // (required for `((p as i64) + i) as *u8` retype patterns --
3471 // see wrapper comment for the deref-precedence defect this
3472 // placement fixes).
3473 return parse_primary(P)
3474}
3475
3476// Helpers for type-directed arithmetic op selection. When either
3477// operand is TY_F32 (f64 later), promote OP_ADD/SUB/MUL/DIV_S to
3478// their floating-point counterparts and carry TY_F32 through the
3479// result. Non-fp operands fall through to the integer ops + i64
3480// result type -- same as the pre-F-extension behavior.
3481
3482func fp_op_for_int(op: i64) -> i64 {
3483 if op == OP_ADD { return OP_FADD }
3484 if op == OP_SUB { return OP_FSUB }
3485 if op == OP_MUL { return OP_FMUL }
3486 if op == OP_DIV_S { return OP_FDIV }
3487 return op
3488}
3489
3490func has_fp_operand(P: *Parser, v: i64) -> i64 {
3491 if v < 0 { return 0 }
3492 if v >= P.current_fn.n_values { return 0 }
3493 let base: i64 = P.current_fn.values as i64
3494 let val: *Value = (base + v * 48) as *Value
3495 if val.ty == (0 as *Type) { return 0 }
3496 if val.ty.kind == TY_F32 { return 1 }
3497 if val.ty.kind == TY_F64 { return 1 }
3498 return 0
3499}
3500
3501func nx_value_type(P: *Parser, v: i64) -> *Type {
3502 if v < 0 { return 0 as *Type }
3503 if v >= P.current_fn.n_values { return 0 as *Type }
3504 let base: i64 = P.current_fn.values as i64
3505 let val: *Value = (base + v * 48) as *Value
3506 return val.ty
3507}
3508
3509// Result type of an INTEGER binop. When BOTH operands are 32-bit (i32/u32) the result is
3510// 32-bit -> the backend then WRAPS it mod 2^32 (else RV64 computes in 64 bits and u32
3511// arithmetic never overflows: 4e9+1e9 stayed 5e9 instead of 705032704). Signed only when
3512// BOTH are signed i32 (so the wrap sign-extends); otherwise unsigned u32 (zero-extend).
3513// Any i64 operand -> i64 result: this is exactly what keeps POINTER arithmetic (ptr + i32
3514// offset, ptr is i64) full-width instead of truncating the address.
3515func nx_value_is_const(P: *Parser, v: i64) -> i64 {
3516 if v < 0 { return 0 }
3517 if v >= P.current_fn.n_values { return 0 }
3518 let base: i64 = P.current_fn.values as i64
3519 let val: *Value = (base + v * 48) as *Value
3520 if val.kind == 0 { return 1 } // VK_CONST_INT
3521 return 0
3522}
3523
3524func int_binop_result_type(P: *Parser, left: i64, right: i64) -> *Type {
3525 let lt: *Type = nx_value_type(P, left)
3526 let rt: *Type = nx_value_type(P, right)
3527 var l32: i64 = 0
3528 var r32: i64 = 0
3529 var l_signed: i64 = 0
3530 var r_signed: i64 = 0
3531 if lt != (0 as *Type) { if lt.kind == TY_I32 { l32 = 1; l_signed = lt.sext } }
3532 if rt != (0 as *Type) { if rt.kind == TY_I32 { r32 = 1; r_signed = rt.sext } }
3533 var is32: i64 = 0
3534 var signed32: i64 = 0
3535 // both 32-bit -> 32-bit (signed only if BOTH signed)
3536 if l32 == 1 { if r32 == 1 { is32 = 1; if l_signed == 1 { if r_signed == 1 { signed32 = 1 } } } }
3537 // one 32-bit + the other an integer LITERAL -> 32-bit (the literal adapts, e.g. `u32_acc + K[i]`
3538 // in a hash round). A non-const i64 operand keeps the result i64 so real 64-bit values (and
3539 // POINTER arithmetic, ptr is TY_PTR not TY_I32) are never truncated.
3540 if l32 == 1 { if r32 == 0 { if nx_value_is_const(P, right) == 1 { is32 = 1; signed32 = l_signed } } }
3541 if r32 == 1 { if l32 == 0 { if nx_value_is_const(P, left) == 1 { is32 = 1; signed32 = r_signed } } }
3542 if is32 == 1 {
3543 if signed32 == 1 { return alloc_type_s(TY_I32, 4, 4) }
3544 return alloc_type(TY_I32, 4, 4)
3545 }
3546 // POINTER ARITHMETIC KEEPS ITS POINTER TYPE (2026-08-05). ptr + int used to fall through to
3547 // i64 here, so the corpus idiom emit(out + o, cap - o) handed a value the parser believed was
3548 // an INTEGER to a POINTER parameter. That is CAUSE 2 of the call-arg type residue: with the
3549 // cast-mutation root fixed, enabling the reverse type direction still refused 4 equiv rows, and
3550 // the gateu0027s newly-captured compiler stderr named the exact site --
3551 // nx_parse: argument type mismatch in call to u0027tls13_ext_emit_supported_versions_tls13u0027:
3552 // arg 1 is an INTEGER but the parameter is a POINTER (nx_tls13_hello.nx out + o)
3553 // -- which was the CHECK BEING RIGHT ABOUT A TYPE THE PARSER HAD ALREADY LOST, not a false
3554 // positive. C semantics and every corpus use agree: pointer +/- integer is a POINTER.
3555 // Both operands pointers (p - q) stays an integer difference.
3556 let lp: i64 = (lt != (0 as *Type)) as i64
3557 let rp: i64 = (rt != (0 as *Type)) as i64
3558 if lp == 1 {
3559 if lt.kind == TY_PTR {
3560 if rp == 1 { if rt.kind == TY_PTR { return ir_type_i64() } }
3561 return lt
3562 }
3563 }
3564 if rp == 1 { if rt.kind == TY_PTR { return rt } }
3565 return ir_type_i64()
3566}
3567
3568// Result float type of an fp binop: TY_F64 if EITHER operand is f64, else TY_F32
3569// (C-style widest-operand promotion). Forcing TY_F32 here (the pre-2026-07-16
3570// behavior) SILENTLY MISCOMPILED f64 arithmetic to single precision -- the
3571// emitter reads i.ty to pick movsd vs movss, so a wrong TY_F32 truncated every
3572// f64 op to garbage (low-32-bit reinterpret). See the f64-codegen rung.
3573func fp_result_type(P: *Parser, left: i64, right: i64) -> *Type {
3574 let lt: *Type = nx_value_type(P, left)
3575 let rt: *Type = nx_value_type(P, right)
3576 if lt != (0 as *Type) { if lt.kind == TY_F64 { return alloc_type(TY_F64, 8, 8) } }
3577 if rt != (0 as *Type) { if rt.kind == TY_F64 { return alloc_type(TY_F64, 8, 8) } }
3578 return alloc_type(TY_F32, 4, 4)
3579}
3580
3581// Emit a binop, auto-promoting to fp when either operand is fp.
3582func emit_typed_binop(P: *Parser, op: i64, left: i64, right: i64) -> i64 {
3583 if has_fp_operand(P, left) == 1 {
3584 return ir_emit_binop(P.current_bb, fp_op_for_int(op),
3585 left, right, fp_result_type(P, left, right))
3586 }
3587 if has_fp_operand(P, right) == 1 {
3588 return ir_emit_binop(P.current_bb, fp_op_for_int(op),
3589 left, right, fp_result_type(P, left, right))
3590 }
3591 return ir_emit_binop(P.current_bb, op, left, right,
3592 int_binop_result_type(P, left, right))
3593}
3594
3595func parse_multiplicative(P: *Parser) -> i64 {
3596 nx_assert_ptr(P.current_bb as *u8, "parse_multiplicative: bb" as *u8)
3597 nx_assert_ptr(P.current_bb.parent as *u8,
3598 "parse_multiplicative: bb.parent" as *u8)
3599 var left: i64 = parse_unary(P)
3600 var go: i64 = 1
3601 while go {
3602 if at_stmt_boundary(P) == 1 { go = 0; continue }
3603 let k: i64 = peek_kind(P)
3604 var op: i64 = 0
3605 if k == TK_STAR { op = OP_MUL }
3606 if k == TK_SLASH { op = OP_DIV_S }
3607 if k == TK_PERCENT { op = OP_REM_S }
3608 if op == 0 { go = 0; continue }
3609 advance_tok(P)
3610 let right: i64 = parse_unary(P)
3611 left = emit_typed_binop(P, op, left, right)
3612 }
3613 return left
3614}
3615
3616func parse_additive(P: *Parser) -> i64 {
3617 nx_assert_ptr(P.current_bb as *u8, "parse_additive: bb" as *u8)
3618 nx_assert_ptr(P.current_bb.parent as *u8,
3619 "parse_additive: bb.parent" as *u8)
3620 var left: i64 = parse_multiplicative(P)
3621 var go: i64 = 1
3622 while go {
3623 if at_stmt_boundary(P) == 1 { go = 0; continue }
3624 let k: i64 = peek_kind(P)
3625 var op: i64 = 0
3626 if k == TK_PLUS { op = OP_ADD }
3627 if k == TK_MINUS { op = OP_SUB }
3628 if op == 0 { go = 0; continue }
3629 advance_tok(P)
3630 let right: i64 = parse_multiplicative(P)
3631 left = emit_typed_binop(P, op, left, right)
3632 }
3633 return left
3634}
3635
3636func parse_shift(P: *Parser) -> i64 {
3637 var left: i64 = parse_additive(P)
3638 var go: i64 = 1
3639 while go {
3640 if at_stmt_boundary(P) == 1 { go = 0; continue }
3641 let k: i64 = peek_kind(P)
3642 var op: i64 = 0
3643 if k == TK_SHL { op = OP_SHL }
3644 if k == TK_SHR { op = OP_SHR_S }
3645 if op == 0 { go = 0; continue }
3646 advance_tok(P)
3647 let right: i64 = parse_additive(P)
3648 // Shift result takes the LEFT operand's width: a 32-bit `x << n` wraps mod 2^32
3649 // (the backend masks a TY_I32 result); an i64 shift stays 64-bit.
3650 var sty: *Type = ir_type_i64()
3651 let lt: *Type = nx_value_type(P, left)
3652 if lt != (0 as *Type) {
3653 if lt.kind == TY_I32 {
3654 if lt.sext == 1 { sty = alloc_type_s(TY_I32, 4, 4) }
3655 if lt.sext != 1 { sty = alloc_type(TY_I32, 4, 4) }
3656 }
3657 }
3658 left = ir_emit_binop(P.current_bb, op, left, right, sty)
3659 }
3660 return left
3661}
3662
3663func parse_comparison(P: *Parser) -> i64 {
3664 nx_assert_ptr(P.current_bb as *u8, "parse_comparison: bb" as *u8)
3665 nx_assert_ptr(P.current_bb.parent as *u8,
3666 "parse_comparison: bb.parent" as *u8)
3667 let left: i64 = parse_shift(P)
3668 if at_stmt_boundary(P) == 1 { return left }
3669 let k: i64 = peek_kind(P)
3670 var op: i64 = 0
3671 if k == TK_LT { op = OP_LT_S }
3672 if k == TK_LE { op = OP_LE_S }
3673 if k == TK_GT { op = OP_GT_S }
3674 if k == TK_GE { op = OP_GE_S }
3675 if op == 0 { return left }
3676 advance_tok(P)
3677 let right: i64 = parse_shift(P)
3678 return ir_emit_binop(P.current_bb, op, left, right, ir_type_bool())
3679}
3680
3681func parse_equality(P: *Parser) -> i64 {
3682 var left: i64 = parse_comparison(P)
3683 var go: i64 = 1
3684 while go {
3685 if at_stmt_boundary(P) == 1 { go = 0; continue }
3686 let k: i64 = peek_kind(P)
3687 var op: i64 = 0
3688 if k == TK_EQ { op = OP_EQ }
3689 if k == TK_NE { op = OP_NE }
3690 if op == 0 { go = 0; continue }
3691 advance_tok(P)
3692 let right: i64 = parse_comparison(P)
3693 left = ir_emit_binop(P.current_bb, op, left, right, ir_type_bool())
3694 }
3695 return left
3696}
3697
3698func parse_bitand(P: *Parser) -> i64 {
3699 var left: i64 = parse_equality(P)
3700 while peek_kind(P) == TK_AMP {
3701 if at_stmt_boundary(P) == 1 { return left }
3702 advance_tok(P)
3703 let right: i64 = parse_equality(P)
3704 left = ir_emit_binop(P.current_bb, OP_AND, left, right, ir_type_i64())
3705 }
3706 return left
3707}
3708
3709func parse_bitxor(P: *Parser) -> i64 {
3710 var left: i64 = parse_bitand(P)
3711 while peek_kind(P) == TK_CARET {
3712 if at_stmt_boundary(P) == 1 { return left }
3713 advance_tok(P)
3714 let right: i64 = parse_bitand(P)
3715 left = ir_emit_binop(P.current_bb, OP_XOR, left, right, ir_type_i64())
3716 }
3717 return left
3718}
3719
3720func parse_bitor(P: *Parser) -> i64 {
3721 var left: i64 = parse_bitxor(P)
3722 while peek_kind(P) == TK_PIPE {
3723 if at_stmt_boundary(P) == 1 { return left }
3724 advance_tok(P)
3725 let right: i64 = parse_bitxor(P)
3726 left = ir_emit_binop(P.current_bb, OP_OR, left, right, ir_type_i64())
3727 }
3728 return left
3729}
3730
3731func parse_logical_and(P: *Parser) -> i64 {
3732 var left: i64 = parse_bitor(P)
3733 while peek_kind(P) == TK_AND_AND {
3734 if at_stmt_boundary(P) == 1 { return left }
3735 advance_tok(P)
3736 let right: i64 = parse_bitor(P)
3737 left = ir_emit_binop(P.current_bb, OP_AND, left, right, ir_type_bool())
3738 }
3739 return left
3740}
3741
3742func parse_logical_or(P: *Parser) -> i64 {
3743 var left: i64 = parse_logical_and(P)
3744 while peek_kind(P) == TK_OR_OR {
3745 if at_stmt_boundary(P) == 1 { return left }
3746 advance_tok(P)
3747 let right: i64 = parse_logical_and(P)
3748 left = ir_emit_binop(P.current_bb, OP_OR, left, right, ir_type_bool())
3749 }
3750 return left
3751}
3752
3753// ---- statements ----
3754
3755// parse_stmt -- slim dispatcher (task #21 codegen-bug workaround).
3756//
3757// The original parse_stmt body was 500+ lines with deeply nested
3758// branches. When the C-host compiled it, the resulting RV64 asm
3759// had a ~4400-byte stack frame; under heavy register pressure the
3760// regalloc miscalculated alloca-slot offsets such that a write to
3761// a local var would land on the *Parser arg's spill region,
3762// nulling P.current_bb. The bug only manifested mid-way through
3763// parsing sys_read_file in stage-2 self-compile.
3764//
3765// Workaround: hoist EVERY major branch into its own function.
3766// Each handler now has its own clean stack frame -- P arrives as
3767// a fresh argument, alloca slots can't alias the caller's spill,
3768// and the regalloc has dramatically less pressure to manage.
3769// Pure NishiLang refactor; no C-host changes required.
3770func parse_stmt(P: *Parser) -> i64 {
3771 nx_assert_ptr(P.current_fn as *u8, "parse_stmt: P.current_fn" as *u8)
3772 nx_assert(P.current_fn.values_cap > 0,
3773 "parse_stmt: P.current_fn.values_cap > 0" as *u8)
3774 nx_assert_ptr(P.current_bb as *u8, "parse_stmt: P.current_bb" as *u8)
3775 nx_assert_ptr(P.current_bb.parent as *u8,
3776 "parse_stmt: P.current_bb.parent" as *u8)
3777
3778 let k: i64 = peek_kind(P)
3779 // DEFENSE-IN-DEPTH (seq757's prescribed second half, 2026-07-29): a `func` token can NEVER
3780 // start a statement -- reaching one mid-body means some prior construct consumed this
3781 // function's closing brace (the seq358 fusion class). Die LOUD instead of silently
3782 // absorbing the next function as an empty stub. Kills the whole class structurally.
3783 if k == TK_FUNC {
3784 parse_die("'func' inside a function body -- a prior construct consumed the closing brace (desync guard)" as *u8, 92)
3785 }
3786 if k == TK_RETURN { return parse_stmt_return(P) }
3787 if k == TK_LET { return parse_stmt_let(P) }
3788 if k == TK_VAR { return parse_stmt_var(P) }
3789 if k == TK_IF { return parse_stmt_if(P) }
3790 if k == TK_WHILE { return parse_stmt_while(P) }
3791 if k == TK_FOR { return parse_stmt_for(P) }
3792 if k == TK_BREAK { return parse_stmt_break(P) }
3793 if k == TK_CONTINUE { return parse_stmt_continue(P) }
3794 if k == TK_MATCH { return parse_stmt_match(P) }
3795 if k == TK_STAR { return parse_stmt_star(P) }
3796 if k == TK_IDENT { return parse_stmt_ident(P) }
3797 return parse_stmt_expr_fallback(P)
3798}
3799
3800// ---- per-kind handlers (each = one fresh stack frame) ----
3801
3802// PREVENT (T#parser-keyword-as-identifier): a binding name MUST be a plain
3803// identifier. A reserved keyword in name position (e.g. `var match`, where
3804// `match` lexes to TK_MATCH) silently desynced the parser so the NEXT
3805// compilation unit's leading `const X: i64 = 0` misparsed as `i64 = 0`.
3806// Reject it LOUD here -- makes the whole class structurally impossible
3807// (no silent failure; same stance as the let-reassign / undefined-assign guards).
3808func nx_require_ident_name(P: *Parser, name_tok: *Tok) -> i64 {
3809 if name_tok.kind != TK_IDENT {
3810 sys_write(2, "nx_parse: line " as *u8, 15)
3811 nx_put_dec_err(name_tok.line)
3812 sys_write(2, ": reserved keyword '" as *u8, 20)
3813 nx_put_tok_text_err(name_tok)
3814 sys_write(2, "' cannot be used as an identifier name\n" as *u8, 39)
3815 sys_exit(2)
3816 }
3817 return 0
3818}
3819
3820func parse_stmt_return(P: *Parser) -> i64 {
3821 advance_tok(P)
3822 var v: i64 = 0
3823 if peek_kind(P) != TK_RBRACE {
3824 if peek_kind(P) != TK_SEMI {
3825 v = parse_expr(P)
3826 }
3827 }
3828 match_kind(P, TK_SEMI)
3829 ir_emit_return(P.current_bb, v)
3830 return 0
3831}
3832
3833func parse_stmt_let(P: *Parser) -> i64 {
3834 advance_tok(P)
3835 let name_tok: *Tok = advance_tok(P)
3836 nx_require_ident_name(P, name_tok)
3837 var let_ty: *Type = 0 as *Type
3838 if match_kind(P, TK_COLON) {
3839 let_ty = parse_type(P)
3840 }
3841 match_kind(P, TK_ASSIGN)
3842 let v: i64 = parse_expr(P)
3843 match_kind(P, TK_SEMI)
3844 // Type-context inference: if the LHS demands TY_F32 but the RHS
3845 // is a default-f64 literal Value (CONST kind, TY_F64 attached),
3846 // downcast the IEEE 754 bits in place and re-tag as TY_F32.
3847 // This makes `let x: f32 = 1.5` work without requiring the user
3848 // to write `1.5f32` explicitly. Symmetric with literal-suffix
3849 // path (TK_FLOAT_F32) -- both produce a single-precision Value
3850 // at the same storage slot.
3851 if let_ty != (0 as *Type) {
3852 if let_ty.kind == TY_F32 {
3853 let vbase: i64 = P.current_fn.values as i64
3854 let val: *Value = (vbase + v * 48) as *Value
3855 if val.kind == 0 {
3856 if val.ty != (0 as *Type) {
3857 if val.ty.kind == TY_F64 {
3858 val.const_int = fp64_to_fp32(val.const_int)
3859 val.ty = alloc_type(TY_F32, 4, 4)
3860 }
3861 }
3862 }
3863 }
3864 }
3865 // If the user gave no annotation, default to i64 (legacy
3866 // behavior). When given, propagate the real type so pointer-
3867 // typed locals like `*u8` index with the right stride.
3868 if let_ty == (0 as *Type) { let_ty = ir_type_i64() }
3869 // Two `let` of one name in ONE block was accepted silently, and find_local scans BACKWARD, so
3870 // every later reference bound to the newer one. In a code emitter that silently rewires control
3871 // flow: two `let loop_at` sent an event-loop back-edge to the wrong label, caught only because
3872 // the bogus displacement happened to overflow rel8. Under 127 bytes it emits a wrong jump green.
3873 // ENFORCEMENT IS OFF PENDING BACKFILL. local_in_block() is correct and the scoping is live,
3874 // but the estate ITSELF declares the same name twice in ONE block in at least 6 measured
3875 // sources, so switching this on refuses real code. Backfill the sources, THEN turn it on:
3876 // nx_assert(local_in_block(P, tok_text_ptr(name_tok)) == 0, "duplicate let ..." as *u8)
3877 add_local(P, tok_text_ptr(name_tok), v, 0, TY_I64, let_ty)
3878 return 0
3879}
3880
3881func parse_stmt_var(P: *Parser) -> i64 {
3882 advance_tok(P)
3883 let name_tok: *Tok = advance_tok(P)
3884 nx_require_ident_name(P, name_tok)
3885 var ann: *Type = 0 as *Type
3886 if match_kind(P, TK_COLON) { ann = parse_type(P) }
3887 if ann != (0 as *Type) {
3888 if ann.kind == TY_STRUCT {
3889 if peek_kind(P) != TK_ASSIGN {
3890 match_kind(P, TK_SEMI)
3891 let addr_s: i64 = ir_emit_alloca(P.current_bb, ann)
3892 add_local(P, tok_text_ptr(name_tok), addr_s, 1, TY_STRUCT, ann)
3893 return 0
3894 }
3895 }
3896 if ann.kind == TY_ARRAY {
3897 // Stack array `var buf: [N]T` -- alloca ann.size (=N*elem) bytes; the local's
3898 // value_id IS the array's frame address (decays like a struct). No initializer
3899 // form (a `= {...}` literal would be a future extension); memory is uninitialised.
3900 match_kind(P, TK_SEMI)
3901 let addr_a: i64 = ir_emit_alloca(P.current_bb, ann)
3902 add_local(P, tok_text_ptr(name_tok), addr_a, 1, TY_ARRAY, ann)
3903 return 0
3904 }
3905 }
3906 match_kind(P, TK_ASSIGN)
3907 let init_v: i64 = parse_expr(P)
3908 match_kind(P, TK_SEMI)
3909 let addr: i64 = ir_emit_alloca(P.current_bb, ir_type_i64())
3910 ir_emit_store(P.current_bb, addr, init_v, ir_type_i64())
3911 var var_ty: *Type = ann
3912 if var_ty == (0 as *Type) { var_ty = ir_type_i64() }
3913 add_local(P, tok_text_ptr(name_tok), addr, 1, TY_I64, var_ty)
3914 return 0
3915}
3916
3917func parse_stmt_if(P: *Parser) -> i64 {
3918 advance_tok(P)
3919 let cond: i64 = parse_expr(P)
3920 match_kind(P, TK_LBRACE)
3921 let then_bb: *BasicBlock = ir_block_new(P.current_fn)
3922 let else_bb: *BasicBlock = ir_block_new(P.current_fn)
3923 let merge_bb: *BasicBlock = ir_block_new(P.current_fn)
3924 ir_emit_br_cond(P.current_bb, cond, then_bb, else_bb)
3925 P.current_bb = then_bb
3926 parse_stmt_list(P)
3927 match_kind(P, TK_RBRACE)
3928 ir_emit_br(P.current_bb, merge_bb)
3929 P.current_bb = else_bb
3930 if match_kind(P, TK_ELSE) {
3931 // `else if` CHAIN (seq358 root cause, fixed 2026-07-29): this production was MISSING --
3932 // match_kind(TK_LBRACE) failed silently on `else if`, the chained if parsed as the first
3933 // statement of an unbraced "block", and parse_stmt_list then consumed past the REAL
3934 // closing brace hunting a phantom one: following statements -- and eventually following
3935 // FUNCTIONS -- fused into the current one (empty-stub emission, the documented desync;
3936 // it also faked seq1012's "false positive": identifiers after the fuse point resolved
3937 // against the WRONG function). The chained if IS the else block: recurse; its merge
3938 // becomes current_bb and bridges into our merge below. Flag dialect, not else-if, so
3939 // TODAY'S compiler (which lacks the production) can compile this very fix.
3940 var chained: i64 = 0
3941 if peek_kind(P) == TK_IF { chained = 1 }
3942 if chained == 1 {
3943 parse_stmt_if(P)
3944 }
3945 if chained == 0 {
3946 match_kind(P, TK_LBRACE)
3947 parse_stmt_list(P)
3948 match_kind(P, TK_RBRACE)
3949 }
3950 }
3951 ir_emit_br(P.current_bb, merge_bb)
3952 P.current_bb = merge_bb
3953 return 0
3954}
3955
3956func parse_stmt_while(P: *Parser) -> i64 {
3957 advance_tok(P)
3958 let head_bb: *BasicBlock = ir_block_new(P.current_fn)
3959 let body_bb: *BasicBlock = ir_block_new(P.current_fn)
3960 let exit_bb: *BasicBlock = ir_block_new(P.current_fn)
3961 ir_emit_br(P.current_bb, head_bb)
3962 P.current_bb = head_bb
3963 let cond: i64 = parse_expr(P)
3964 match_kind(P, TK_LBRACE)
3965 ir_emit_br_cond(P.current_bb, cond, body_bb, exit_bb)
3966 P.current_bb = body_bb
3967 let saved_head: *BasicBlock = P.loop_head
3968 let saved_exit: *BasicBlock = P.loop_exit
3969 P.loop_head = head_bb
3970 P.loop_exit = exit_bb
3971 parse_stmt_list(P)
3972 P.loop_head = saved_head
3973 P.loop_exit = saved_exit
3974 match_kind(P, TK_RBRACE)
3975 ir_emit_br(P.current_bb, head_bb)
3976 P.current_bb = exit_bb
3977 return 0
3978}
3979
3980func parse_stmt_for(P: *Parser) -> i64 {
3981 advance_tok(P)
3982 let vname: *Tok = advance_tok(P)
3983 nx_require_ident_name(P, vname)
3984 match_kind(P, TK_IN)
3985 let start_v: i64 = parse_expr(P)
3986 match_kind(P, TK_DOT_DOT)
3987 let end_v: i64 = parse_expr(P)
3988 match_kind(P, TK_LBRACE)
3989
3990 let i_slot: i64 = ir_emit_alloca(P.current_bb, ir_type_i64())
3991 ir_emit_store(P.current_bb, i_slot, start_v, ir_type_i64())
3992 let end_slot: i64 = ir_emit_alloca(P.current_bb, ir_type_i64())
3993 ir_emit_store(P.current_bb, end_slot, end_v, ir_type_i64())
3994
3995 let head_bb: *BasicBlock = ir_block_new(P.current_fn)
3996 let body_bb: *BasicBlock = ir_block_new(P.current_fn)
3997 let incr_bb: *BasicBlock = ir_block_new(P.current_fn)
3998 let exit_bb: *BasicBlock = ir_block_new(P.current_fn)
3999
4000 ir_emit_br(P.current_bb, head_bb)
4001
4002 P.current_bb = head_bb
4003 let i_now: i64 = ir_emit_load(P.current_bb, i_slot, ir_type_i64())
4004 let e_now: i64 = ir_emit_load(P.current_bb, end_slot, ir_type_i64())
4005 let cond: i64 = ir_emit_binop(P.current_bb, OP_LT_S, i_now, e_now,
4006 ir_type_bool())
4007 ir_emit_br_cond(P.current_bb, cond, body_bb, exit_bb)
4008
4009 P.current_bb = body_bb
4010 let saved_locals: i64 = P.n_locals
4011 add_local(P, tok_text_ptr(vname), i_slot, 1, TY_I64, ir_type_i64())
4012 let saved_head: *BasicBlock = P.loop_head
4013 let saved_exit: *BasicBlock = P.loop_exit
4014 P.loop_head = incr_bb
4015 P.loop_exit = exit_bb
4016 let saved_bb_for: i64 = P.block_base
4017 P.block_base = P.n_locals
4018 parse_stmt_list(P)
4019 P.block_base = saved_bb_for
4020 P.loop_head = saved_head
4021 P.loop_exit = saved_exit
4022 match_kind(P, TK_RBRACE)
4023 P.n_locals = saved_locals
4024
4025 ir_emit_br(P.current_bb, incr_bb)
4026
4027 P.current_bb = incr_bb
4028 let i2: i64 = ir_emit_load(P.current_bb, i_slot, ir_type_i64())
4029 let one: i64 = safe_const_i64(P, 1, "parse.nx:LINE-const1" as *u8)
4030 let i3: i64 = ir_emit_binop(P.current_bb, OP_ADD, i2, one, ir_type_i64())
4031 ir_emit_store(P.current_bb, i_slot, i3, ir_type_i64())
4032 ir_emit_br(P.current_bb, head_bb)
4033
4034 P.current_bb = exit_bb
4035 return 0
4036}
4037
4038func parse_stmt_break(P: *Parser) -> i64 {
4039 advance_tok(P)
4040 match_kind(P, TK_SEMI)
4041 if P.loop_exit != (0 as *BasicBlock) {
4042 ir_emit_br(P.current_bb, P.loop_exit)
4043 }
4044 return 0
4045}
4046
4047func parse_stmt_continue(P: *Parser) -> i64 {
4048 advance_tok(P)
4049 match_kind(P, TK_SEMI)
4050 if P.loop_head != (0 as *BasicBlock) {
4051 ir_emit_br(P.current_bb, P.loop_head)
4052 }
4053 return 0
4054}
4055
4056func parse_stmt_match(P: *Parser) -> i64 {
4057 let match_tok: *Tok = advance_tok(P)
4058 let scrut: i64 = parse_expr(P)
4059 match_kind(P, TK_LBRACE)
4060
4061 let saved_pos_m: i64 = P.pos
4062 var tagged: i64 = 0
4063 var shadow_ty_m: *Type = 0 as *Type
4064 if peek_kind(P) == TK_IDENT {
4065 let pen_tok: *Tok = peek_at(P, 0)
4066 let pen: *u8 = tok_text_ptr(pen_tok)
4067 let pee: *EnumEntry = lookup_enum(P, pen)
4068 if pee != (0 as *EnumEntry) {
4069 if pee.has_payload == 1 {
4070 tagged = 1
4071 shadow_ty_m = pee.shadow_ty
4072 }
4073 }
4074 }
4075
4076 // MATCH EXHAUSTIVENESS (2026-08-05): remember WHICH enum is being matched so the closing
4077 // brace can prove every variant is handled. Elm made this famous and Rust/Swift/OCaml all
4078 // enforce it; nx_cc parsed match happily and let a missing variant fall THROUGH the whole
4079 // statement to whatever followed -- a silent wrong-path, the same family as the arity and
4080 // call-arg-type fail-opens closed earlier today.
4081 var match_ee: *EnumEntry = 0 as *EnumEntry
4082 var covered_mask: i64 = 0
4083 var covered_n: i64 = 0
4084 var over64: i64 = 0
4085 if peek_kind(P) == TK_IDENT {
4086 let cen_tok: *Tok = peek_at(P, 0)
4087 let cee: *EnumEntry = lookup_enum(P, tok_text_ptr(cen_tok))
4088 if cee != (0 as *EnumEntry) { match_ee = cee }
4089 }
4090 var disc_scrut: i64 = scrut
4091 if tagged == 1 {
4092 let zero_off: i64 = safe_const_i64(P, 0, "parse.nx:match-zero" as *u8)
4093 let tag_addr: i64 = ir_emit_gep(P.current_bb, scrut, zero_off, ir_type_i64())
4094 disc_scrut = ir_emit_load(P.current_bb, tag_addr, ir_type_i64())
4095 }
4096
4097 let merge_bb: *BasicBlock = ir_block_new(P.current_fn)
4098
4099 while peek_kind(P) != TK_RBRACE {
4100 if peek_kind(P) == TK_EOF { return 0 }
4101 let en_tok: *Tok = advance_tok(P)
4102 let en: *u8 = tok_text_ptr(en_tok)
4103 match_kind(P, TK_COLON_COLON)
4104 let v_tok: *Tok = advance_tok(P)
4105 let vn: *u8 = tok_text_ptr(v_tok)
4106
4107 let qbuf2: *u8 = sys_mmap(80)
4108 var nl2: i64 = 0
4109 while en[nl2] != 0 { qbuf2[nl2] = en[nl2]; nl2 = nl2 + 1 }
4110 qbuf2[nl2] = 0x3A; nl2 = nl2 + 1
4111 qbuf2[nl2] = 0x3A; nl2 = nl2 + 1
4112 var vl2: i64 = 0
4113 while vn[vl2] != 0 { qbuf2[nl2 + vl2] = vn[vl2]; vl2 = vl2 + 1 }
4114 qbuf2[nl2 + vl2] = 0
4115
4116 let out_raw2: *u8 = sys_mmap(16)
4117 let out2: *i64 = out_raw2 as *i64
4118 *out2 = 0
4119 lookup_mconst(P, qbuf2, out2)
4120 let disc: i64 = *out2
4121 // Count DISTINCT discriminants, not arms: duplicated arms must not fake coverage, and
4122 // counting VALUES (not positions) keeps explicit Variant = 7 discriminants correct.
4123 // A discriminant outside 0..63 cannot be tracked in the mask -- declare that and skip
4124 // the verdict rather than guess (a check that cannot know must decline, not invent).
4125 if disc >= 0 {
4126 if disc < 64 {
4127 let dbit: i64 = 1 << disc
4128 if (covered_mask & dbit) == 0 { covered_mask = covered_mask | dbit; covered_n = covered_n + 1 }
4129 else {
4130 // UNREACHABLE ARM (2026-08-05). A discriminant already covered by an earlier arm
4131 // can never be selected: the emitted chain tests arms in order, so this body is
4132 // DEAD CODE. It is the exact shape a copy-pasted arm takes when the variant name
4133 // was not updated -- the author believes a case is handled and it is not. Rust and
4134 // Elm both refuse it; refusing here costs nothing and names the arm.
4135 sys_write(2, "nx_parse: line " as *u8, 15)
4136 nx_put_dec_err(en_tok.line)
4137 sys_write(2, ": unreachable match arm '" as *u8, 25)
4138 sys_write(2, qbuf2, nl2 + vl2)
4139 sys_write(2, "' -- already handled by an earlier arm of the match at line " as *u8, 60)
4140 nx_put_dec_err(match_tok.line)
4141 sys_write(2, "; remove it or fix the variant name\n" as *u8, 36)
4142 nx_diag_note_error()
4143 }
4144 } else { over64 = 1 }
4145 } else { over64 = 1 }
4146
4147 var has_bind: i64 = 0
4148 var bind_tok: *Tok = 0 as *Tok
4149 if match_kind(P, TK_LPAREN) {
4150 bind_tok = advance_tok(P)
4151 has_bind = 1
4152 match_kind(P, TK_RPAREN)
4153 }
4154 match_kind(P, TK_FAT_ARROW)
4155
4156 let arm_bb: *BasicBlock = ir_block_new(P.current_fn)
4157 let next_bb: *BasicBlock = ir_block_new(P.current_fn)
4158 let disc_v: i64 = safe_const_i64(P, disc, "parse.nx:match-disc" as *u8)
4159 let cmp: i64 = ir_emit_binop(P.current_bb, OP_EQ, disc_scrut, disc_v,
4160 ir_type_bool())
4161 ir_emit_br_cond(P.current_bb, cmp, arm_bb, next_bb)
4162
4163 P.current_bb = arm_bb
4164
4165 let saved_locals_arm: i64 = P.n_locals
4166 if has_bind == 1 {
4167 if tagged == 1 {
4168 let eight: i64 = safe_const_i64(P, 8, "parse.nx:match-eight" as *u8)
4169 let pay_addr: i64 = ir_emit_gep(P.current_bb, scrut, eight,
4170 ir_type_i64())
4171 let pay_val: i64 = ir_emit_load(P.current_bb, pay_addr,
4172 ir_type_i64())
4173 let bname: *u8 = tok_text_ptr(bind_tok)
4174 add_local(P, bname, pay_val, 0, TY_I64, ir_type_i64())
4175 }
4176 }
4177
4178 let saved_bb_arm: i64 = P.block_base
4179 P.block_base = P.n_locals
4180 if match_kind(P, TK_LBRACE) {
4181 parse_stmt_list(P)
4182 match_kind(P, TK_RBRACE)
4183 } else {
4184 parse_stmt(P)
4185 }
4186 P.block_base = saved_bb_arm
4187 P.n_locals = saved_locals_arm
4188 ir_emit_br(P.current_bb, merge_bb)
4189
4190 P.current_bb = next_bb
4191 match_kind(P, TK_COMMA)
4192 }
4193 match_kind(P, TK_RBRACE)
4194
4195 // EXHAUSTIVENESS VERDICT. Refuse a match that leaves a variant unhandled: without this the
4196 // statement simply falls through, so the bug shows up as a wrong RESULT far from the match.
4197 // NishiLang has no wildcard arm form (patterns are Enum::Variant), so full coverage is
4198 // exactly distinct-variants-handled == variants-declared.
4199 // DECLARED NARROWNESS: skipped when any discriminant is outside 0..63 (mask range) or when
4200 // the enum is unknown at this point -- decline, never guess.
4201 if match_ee != (0 as *EnumEntry) {
4202 if over64 == 0 {
4203 if match_ee.n_variants > 0 {
4204 if covered_n < match_ee.n_variants {
4205 sys_write(2, "nx_parse: line " as *u8, 15)
4206 nx_put_dec_err(match_tok.line)
4207 sys_write(2, ": non-exhaustive match on enum '" as *u8, 32)
4208 sys_write(2, (match_ee as i64) as *u8, match_ee.name_len)
4209 sys_write(2, "': " as *u8, 3)
4210 nx_put_dec_err(covered_n)
4211 sys_write(2, " of " as *u8, 4)
4212 nx_put_dec_err(match_ee.n_variants)
4213 sys_write(2, " variants handled -- add an arm for: " as *u8, 37)
4214 // NAME THE MISSING VARIANTS (Elm parity, 2026-08-05). A COUNT TELLS YOU THAT YOU
4215 // ARE WRONG; A NAME TELLS YOU WHAT TO WRITE -- and that difference is the whole
4216 // reason Elm held Best on this axis. No new storage was needed: every variant is
4217 // already registered as the module const "Enum::Variant" carrying its
4218 // discriminant in .val, so the diagnostic is a scan of a table we already keep.
4219 let een: *u8 = (match_ee as i64) as *u8
4220 let eln: i64 = match_ee.name_len
4221 var mi: i64 = 0
4222 var printed: i64 = 0
4223 while mi < P.n_mconsts {
4224 let mc: *MConst = mconst_at(P, mi)
4225 let mn: *u8 = (mc as i64) as *u8
4226 var pre: i64 = 1
4227 var q: i64 = 0
4228 while q < eln {
4229 if mn[q] != een[q] { pre = 0; q = eln }
4230 q = q + 1
4231 }
4232 if pre == 1 { if mn[eln] != (58 as u8) { pre = 0 } }
4233 if pre == 1 { if mn[eln + 1] != (58 as u8) { pre = 0 } }
4234 if pre == 1 {
4235 let dv: i64 = mc.val
4236 var miss: i64 = 1
4237 if dv >= 0 {
4238 if dv < 64 {
4239 if (covered_mask & (1 << dv)) != 0 { miss = 0 }
4240 }
4241 }
4242 if miss == 1 {
4243 if printed == 1 { sys_write(2, ", " as *u8, 2) }
4244 let vp: i64 = eln + 2
4245 var vlen: i64 = 0
4246 while mn[vp + vlen] != (0 as u8) { vlen = vlen + 1 }
4247 sys_write(2, ((mn as i64) + vp) as *u8, vlen)
4248 printed = 1
4249 }
4250 }
4251 mi = mi + 1
4252 }
4253 sys_write(2, "\n" as *u8, 1)
4254 nx_diag_caret(match_tok.line, match_tok.col)
4255 nx_diag_note_error()
4256 }
4257 }
4258 }
4259 }
4260
4261 ir_emit_br(P.current_bb, merge_bb)
4262 P.current_bb = merge_bb
4263 return 0
4264}
4265
4266func parse_stmt_star(P: *Parser) -> i64 {
4267 let p1: *Tok = peek_at(P, 1)
4268 let p2: *Tok = peek_at(P, 2)
4269 let n1: i64 = p1.kind
4270 let n2: i64 = p2.kind
4271 if n1 == TK_IDENT { if n2 == TK_ASSIGN {
4272 advance_tok(P)
4273 let name_tok: *Tok = advance_tok(P)
4274 advance_tok(P)
4275 let rhs: i64 = parse_expr(P)
4276 match_kind(P, TK_SEMI)
4277 let name: *u8 = tok_text_ptr(name_tok)
4278 let L: *Local = find_local(P, name)
4279 if L != (0 as *Local) {
4280 var addr: i64 = L.value_id
4281 if L.is_alloca == 1 {
4282 // Load the pointer value at its DECLARED type (L.ty).
4283 var lty1: *Type = L.ty
4284 if lty1 == (0 as *Type) { lty1 = ir_type_i64() }
4285 addr = ir_emit_load(P.current_bb, L.value_id, lty1)
4286 }
4287 // Store's payload type = pointee of the pointer we just
4288 // loaded (or i64 fallback when type info unavailable).
4289 var sty1: *Type = ir_type_i64()
4290 if L.ty != (0 as *Type) {
4291 if L.ty.kind == TY_PTR {
4292 if L.ty.pointee != (0 as *Type) { sty1 = L.ty.pointee }
4293 }
4294 }
4295 ir_emit_store(P.current_bb, addr, rhs, sty1)
4296 }
4297 return 0
4298 } }
4299 return parse_stmt_expr_fallback(P)
4300}
4301
4302// IDENT dispatch: figure out which sub-form (=, [, .), delegate.
4303func parse_stmt_ident(P: *Parser) -> i64 {
4304 let ptok: *Tok = peek_at(P, 1)
4305 let nxt: i64 = ptok.kind
4306 if nxt == TK_ASSIGN { return parse_stmt_ident_assign(P) }
4307 if nxt == TK_LBRACKET { return parse_stmt_ident_subscript(P) }
4308 if nxt == TK_DOT { return parse_stmt_ident_dot(P) }
4309 return parse_stmt_expr_fallback(P)
4310}
4311
4312func parse_stmt_ident_assign(P: *Parser) -> i64 {
4313 let name_idx: i64 = P.pos
4314 let name_tok: *Tok = advance_tok(P)
4315 advance_tok(P)
4316 let rhs: i64 = parse_expr(P)
4317 match_kind(P, TK_SEMI)
4318 let name: *u8 = tok_text_ptr(name_tok)
4319 // Identifier length for diagnostics (stop at first non-ident byte).
4320 var nlen: i64 = 0
4321 var stop: i64 = 0
4322 while stop == 0 {
4323 let c: i64 = name[nlen] as i64
4324 var ident: i64 = 0
4325 if c >= 0x61 { if c <= 0x7A { ident = 1 } }
4326 if c >= 0x41 { if c <= 0x5A { ident = 1 } }
4327 if c >= 0x30 { if c <= 0x39 { ident = 1 } }
4328 if c == 0x5F { ident = 1 }
4329 if ident == 0 { stop = 1 }
4330 if ident == 1 { nlen = nlen + 1 }
4331 if nlen >= 64 { stop = 1 }
4332 }
4333 let L: *Local = find_local(P, name)
4334 // Bits-up no-silent-failure (matches parse.c:2214-2215): an
4335 // assignment that resolves to no local, or to a `let`/param (SSA,
4336 // is_alloca==0), has NOWHERE to store -- the write would be
4337 // silently dropped. Fail LOUD with the offending name.
4338 if L == (0 as *Local) {
4339 sys_write(2, "nx_parse: line " as *u8, 15)
4340 nx_put_dec_err(name_tok.line)
4341 sys_write(2, ":" as *u8, 1)
4342 nx_put_dec_err(name_tok.col)
4343 sys_write(2, ": assign to undefined identifier '" as *u8, 34)
4344 sys_write(2, name, nlen)
4345 sys_write(2, "'" as *u8, 1)
4346 nx_dym_suggest_ident(P, name, nlen)
4347 sys_write(2, "\n" as *u8, 1)
4348 nx_diag_token_window(P, name_idx)
4349 nx_dym_note(P)
4350 nx_diag_note_error()
4351 // RECOVERY: no local to store into -- drop the store, keep parsing.
4352 return 0
4353 }
4354 if L.is_alloca == 0 {
4355 sys_write(2, "nx_parse: line " as *u8, 15)
4356 nx_put_dec_err(name_tok.line)
4357 sys_write(2, ":" as *u8, 1)
4358 nx_put_dec_err(name_tok.col)
4359 sys_write(2, ": cannot reassign 'let' binding '" as *u8, 33)
4360 sys_write(2, name, nlen)
4361 sys_write(2, "' (use 'var')\n" as *u8, 14)
4362 nx_diag_token_window(P, name_idx)
4363 nx_diag_note_error()
4364 // RECOVERY: refuse the store (an SSA let has no slot), keep parsing.
4365 return 0
4366 }
4367 ir_emit_store(P.current_bb, L.value_id, rhs, ir_type_i64())
4368 return 0
4369}
4370
4371func parse_stmt_ident_subscript(P: *Parser) -> i64 {
4372 let name_tok: *Tok = advance_tok(P)
4373 let name: *u8 = tok_text_ptr(name_tok)
4374 let L: *Local = find_local(P, name)
4375 var base: i64 = 0
4376 // Element size + type derived from the local's type pointee.
4377 // *u8 -> stride 1, byte load/store; default i64 -> stride 8,
4378 // word load/store. Without this, every pointer indexing site
4379 // wrote i64-strided through a *u8 buffer (the probe.nx bug).
4380 var elem_sz: i64 = 8
4381 var elem_ty: *Type = ir_type_i64()
4382 var is_arr_w: i64 = 0
4383 var is_slice_w: i64 = 0
4384 // The ARRAY type itself, wherever it lives. For a LOCAL array that is L.ty directly; for a
4385 // MODULE STATIC the Local is typed *(decl_ty) so the array is one level down. The bounds check
4386 // below needs THIS type's size -- taking L.ty.size for a static would read the size of the
4387 // POINTER (8) and compute nelem = 1, rejecting every index above 0.
4388 var arr_ty_w: *Type = 0 as *Type
4389 if L != (0 as *Local) {
4390 base = L.value_id
4391 if L.ty != (0 as *Type) { if L.ty.kind == TY_ARRAY { is_arr_w = 1 arr_ty_w = L.ty } }
4392 if L.ty != (0 as *Type) { if L.ty.kind == TY_SLICE { is_slice_w = 1 } }
4393 // MODULE-STATIC ARRAY: the slot IS the array, so its address is already the base. Treat it
4394 // exactly like a local array -- no load, GEP from the base. Without this the store went
4395 // through element 0 as if it were a pointer (.lcomm zero-filled => base 0 => write near
4396 // null), which is the whole of the "static [N]T is a daemon-killer" behaviour.
4397 let lv_sa: *Value = val_at(P.current_fn, L.value_id)
4398 if lv_sa.kind == VK_GLOBAL {
4399 if L.ty != (0 as *Type) {
4400 if L.ty.pointee != (0 as *Type) {
4401 if L.ty.pointee.kind == TY_ARRAY {
4402 is_arr_w = 1
4403 arr_ty_w = L.ty.pointee
4404 }
4405 }
4406 }
4407 }
4408 if L.is_alloca == 1 {
4409 if is_arr_w == 0 {
4410 // Load at the local's DECLARED type, not hardcoded i64.
4411 // ARRAY skips this: base stays the frame ADDRESS (GEP'd below).
4412 var lty2: *Type = L.ty
4413 if lty2 == (0 as *Type) { lty2 = ir_type_i64() }
4414 base = ir_emit_load(P.current_bb, L.value_id, lty2)
4415 }
4416 }
4417 // Static-ptr STRIDE fix (mirror of parse_primary_local_or_const): a static's Local type is *(e.ty)
4418 // (the slot ADDRESS); the loaded base is the real pointer (e.ty), so the element stride is
4419 // e.ty.pointee = L.ty.pointee.pointee. For a normal pointer local eff_ptr stays L.ty. Without this a
4420 // *u8 static WRITE strided by 8 -> `staticptr[big] = x` ran off the buffer (nx_drbench SIGSEGV).
4421 var eff_ptr: *Type = L.ty
4422 let lvw: *Value = val_at(P.current_fn, L.value_id)
4423 if lvw.kind == VK_GLOBAL {
4424 if L.ty != (0 as *Type) {
4425 if L.ty.kind == TY_PTR {
4426 if L.ty.pointee != (0 as *Type) { eff_ptr = L.ty.pointee }
4427 }
4428 }
4429 }
4430 if eff_ptr != (0 as *Type) {
4431 if eff_ptr.kind == TY_PTR {
4432 if eff_ptr.pointee != (0 as *Type) {
4433 elem_ty = eff_ptr.pointee
4434 if elem_ty.size > 0 { elem_sz = elem_ty.size }
4435 }
4436 }
4437 if eff_ptr.kind == TY_ARRAY {
4438 if eff_ptr.pointee != (0 as *Type) {
4439 elem_ty = eff_ptr.pointee
4440 if elem_ty.size > 0 { elem_sz = elem_ty.size }
4441 }
4442 }
4443 if eff_ptr.kind == TY_SLICE {
4444 if eff_ptr.pointee != (0 as *Type) {
4445 elem_ty = eff_ptr.pointee
4446 if elem_ty.size > 0 { elem_sz = elem_ty.size }
4447 }
4448 }
4449 }
4450 }
4451 advance_tok(P)
4452 let idx: i64 = parse_expr(P)
4453 match_kind(P, TK_RBRACKET)
4454 // SLICE WRITE: `base` currently holds the HANDLE (address of {data,len}). Read the length,
4455 // check the index against it, and only then load the data pointer, so the store address is
4456 // computed only on the path where the index is known good. Afterwards `base` is the data
4457 // pointer and the ordinary POINTER path below does the arithmetic.
4458 if is_slice_w == 1 {
4459 let wo_v: i64 = safe_const_i64(P, NX_SLICE_LEN_OFFSET, "parse.nx:slice-wr-off" as *u8)
4460 let wla: i64 = ir_emit_binop(P.current_bb, OP_ADD, base, wo_v, ir_type_i64())
4461 let wlen: i64 = ir_emit_load(P.current_bb, wla, ir_type_i64())
4462 emit_bounds_check_v(P, idx, wlen)
4463 base = ir_emit_load(P.current_bb, base, ir_type_i64())
4464 }
4465 // SPATIAL SAFETY (CWE-787): a fixed-array WRITE with a compile-time-constant index is bounds-checked at
4466 // compile time (zero-cost, no false positives). L is non-null whenever is_arr_w == 1.
4467 if is_arr_w == 1 {
4468 if arr_ty_w != (0 as *Type) {
4469 if elem_sz > 0 {
4470 let idxv_w: *Value = val_at(P.current_fn, idx)
4471 let nelem_w: i64 = arr_ty_w.size / elem_sz
4472 if idxv_w.kind == VK_CONST_INT {
4473 if idxv_w.const_int < 0 { parse_die("fixed-array index < 0" as *u8, 21) }
4474 if idxv_w.const_int >= nelem_w { parse_die("fixed-array index >= size" as *u8, 25) }
4475 }
4476 // RUNTIME index -> inject the check. The WRITE side matters more than the read:
4477 // an out-of-range store is CWE-787, the corruption primitive behind most memory
4478 // exploits, and it is exactly what C lets through silently.
4479 if idxv_w.kind != VK_CONST_INT { emit_bounds_check(P, idx, nelem_w) }
4480 }
4481 }
4482 }
4483 let esize: i64 = safe_const_i64(P, elem_sz, "parse.nx:subscript-esize" as *u8)
4484 let off: i64 = ir_emit_binop(P.current_bb, OP_MUL, idx, esize, ir_type_i64())
4485 // ARRAY: base is the frame ADDRESS -> GEP. POINTER: base is the loaded pointer VALUE -> OP_ADD.
4486 var cur_addr: i64 = 0
4487 if is_arr_w == 1 {
4488 cur_addr = ir_emit_gep(P.current_bb, base, off, elem_ty)
4489 } else {
4490 cur_addr = ir_emit_binop(P.current_bb, OP_ADD, base, off, ir_type_i64())
4491 }
4492 var cur_ty: *Type = elem_ty
4493 // Trailing `.field` after the subscript: `obj[idx].field = x`
4494 // (e.g. out_funcs[filled].addr = value). This handler used to stop
4495 // after `]`, leaving `.field` to misparse as a bare ident-assign
4496 // (T#parser-subscript-dot-assign-lhs; sibling of the dot-bracket-dot
4497 // gap). Walk the element struct's field chain, mirroring the fix in
4498 // parse_stmt_ident_dot + the read-side parse_field_chain.
4499 while peek_kind(P) == TK_DOT {
4500 advance_tok(P)
4501 let sf_tok: *Tok = advance_tok(P)
4502 let sf_name: *u8 = tok_text_ptr(sf_tok)
4503 var sf_len: i64 = 0
4504 while sf_name[sf_len] != 0 { sf_len = sf_len + 1 }
4505 var sf_stty: *Type = cur_ty
4506 if sf_stty != (0 as *Type) {
4507 if sf_stty.kind == TY_PTR {
4508 if sf_stty.pointee != (0 as *Type) { sf_stty = sf_stty.pointee }
4509 }
4510 }
4511 if sf_stty == (0 as *Type) { break }
4512 let sf_field: *StructField = ir_type_struct_find_field(sf_stty, sf_name, sf_len)
4513 if sf_field == (0 as *StructField) { break }
4514 let sf_off: i64 = safe_const_i64(P, sf_field.offset, "parse.nx:subscript-dot-offset" as *u8)
4515 cur_addr = ir_emit_gep(P.current_bb, cur_addr, sf_off, sf_field.ty)
4516 cur_ty = sf_field.ty
4517 if peek_kind(P) == TK_DOT {
4518 if sf_field.ty != (0 as *Type) {
4519 if sf_field.ty.kind == TY_PTR {
4520 cur_addr = ir_emit_load(P.current_bb, cur_addr, sf_field.ty)
4521 }
4522 }
4523 }
4524 }
4525 if peek_kind(P) == TK_ASSIGN {
4526 advance_tok(P)
4527 let rhs: i64 = parse_expr(P)
4528 match_kind(P, TK_SEMI)
4529 ir_emit_store(P.current_bb, cur_addr, rhs, cur_ty)
4530 return 0
4531 }
4532 ir_emit_load(P.current_bb, cur_addr, cur_ty)
4533 match_kind(P, TK_SEMI)
4534 return 0
4535}
4536
4537func parse_stmt_ident_dot(P: *Parser) -> i64 {
4538 let name_tok: *Tok = advance_tok(P)
4539 let name: *u8 = tok_text_ptr(name_tok)
4540 let L: *Local = find_local(P, name)
4541 if L == (0 as *Local) {
4542 match_kind(P, TK_SEMI)
4543 return 0
4544 }
4545 var v: i64 = L.value_id
4546 // F-meta-3 fix (Session 12): when the local is alloca-backed AND
4547 // it's a pointer (e.g. function parameter `out: *Struct`), the
4548 // alloca holds the pointer value, not the struct. Without the
4549 // load, GEP applies field-offset to the alloca slot itself --
4550 // writes land in random stack memory. Mirrors the alloca-deref
4551 // logic already in parse_stmt_ident_subscript (lines 1593-1604)
4552 // and parse_primary_local_or_const (line 943). Same shape as
4553 // F-meta-2 fix (at_stmt_boundary): missing parallel logic between
4554 // sibling parse handlers.
4555 if L.is_alloca == 1 {
4556 if L.ty != (0 as *Type) {
4557 if L.ty.kind == TY_PTR {
4558 // Pointer value, type L.ty (8 bytes).
4559 v = ir_emit_load(P.current_bb, L.value_id, L.ty)
4560 }
4561 }
4562 }
4563 // Use the local's actual type so the field-access loop emits
4564 // GEP correctly. Was 0 as *Type which made stty NULL on the
4565 // first iteration -> break -> store to raw pointer instead of
4566 // o + field_offset. Same root cause as parse_primary_local_or_const
4567 // (T#bar3-codegen-001). Closes the WRITE side of the bug.
4568 var ty: *Type = L.ty
4569 // Static-ptr FIELD-WRITE fix (2026-07-10; sibling of the READ fix in parse_primary_local_or_const and
4570 // the LBRACKET multi-static fix): a STATIC's injected Local is typed *(decl_ty), so after the pointer
4571 // load above `v` is the REAL pointer typed L.ty.pointee. With the un-unwrapped **Struct the field loop's
4572 // ONE unwrap yields *Struct -> struct lookup FAILS -> break -> the store lands at pointer+0 (ALL fields
4573 // collapsed to one slot -- nx_static_field_probe witness: a=444 b=c=d=0). VK_GLOBAL-gated (equiv-safe).
4574 if L.is_alloca == 1 {
4575 if L.ty != (0 as *Type) {
4576 if L.ty.kind == TY_PTR {
4577 let lv_w: *Value = val_at(P.current_fn, L.value_id)
4578 if lv_w.kind == VK_GLOBAL {
4579 if L.ty.pointee != (0 as *Type) { ty = L.ty.pointee }
4580 }
4581 }
4582 }
4583 }
4584 let saved_pos: i64 = P.pos
4585 while peek_kind(P) == TK_DOT {
4586 advance_tok(P)
4587 let ftok: *Tok = advance_tok(P)
4588 let fname: *u8 = tok_text_ptr(ftok)
4589 var flen: i64 = 0
4590 while fname[flen] != 0 { flen = flen + 1 }
4591 var stty: *Type = ty
4592 if stty != (0 as *Type) {
4593 if stty.kind == TY_PTR {
4594 if stty.pointee != (0 as *Type) { stty = stty.pointee }
4595 }
4596 }
4597 if stty == (0 as *Type) { break }
4598 let field: *StructField = ir_type_struct_find_field(stty, fname, flen)
4599 if field == (0 as *StructField) { break }
4600 let off2: i64 = safe_const_i64(P, field.offset, "parse.nx:dot-offset" as *u8)
4601 v = ir_emit_gep(P.current_bb, v, off2, field.ty)
4602 ty = field.ty
4603 // Intermediate pointer-field deref. For `obj.ptr.subfield = x`,
4604 // .ptr yields the ADDRESS of the pointer field; the next .subfield
4605 // must GEP from the pointer's VALUE, not its address. Mirrors
4606 // parse_field_chain (line 788-792) which already does this for
4607 // the read path -- write path was missing it, causing
4608 // chained-pointer-field stores to write to (obj + ptr_off + sub_off)
4609 // instead of (*ptr + sub_off). Bug class symptom: BB linked-list
4610 // chain only ever held first instr (append_instr emits this exact
4611 // pattern: `bb.tail.next = inst`).
4612 if peek_kind(P) == TK_DOT {
4613 if field.ty != (0 as *Type) {
4614 if field.ty.kind == TY_PTR {
4615 v = ir_emit_load(P.current_bb, v, field.ty)
4616 }
4617 }
4618 }
4619 }
4620 // Handle `o.field[idx] = X` (dot-chain followed by bracket
4621 // index then assign). Closes T#bar3-codegen-002. v is the
4622 // ADDRESS of the field; we load to get the pointer value,
4623 // then offset by idx*elem_size for the LValue address.
4624 if peek_kind(P) == TK_LBRACKET {
4625 let buf_val: i64 = ir_emit_load(P.current_bb, v, ty)
4626 advance_tok(P)
4627 let idx: i64 = parse_expr(P)
4628 match_kind(P, TK_RBRACKET)
4629 var elem_sz: i64 = 8
4630 var elem_ty: *Type = ir_type_i64()
4631 if ty != (0 as *Type) {
4632 if ty.kind == TY_PTR {
4633 if ty.pointee != (0 as *Type) {
4634 elem_ty = ty.pointee
4635 if elem_ty.size > 0 { elem_sz = elem_ty.size }
4636 }
4637 }
4638 }
4639 let esize: i64 = safe_const_i64(P, elem_sz, "parse.nx:dot-bracket-esize" as *u8)
4640 let off3: i64 = ir_emit_binop(P.current_bb, OP_MUL, idx, esize, ir_type_i64())
4641 var cur_addr: i64 = ir_emit_binop(P.current_bb, OP_ADD, buf_val, off3, ir_type_i64())
4642 var cur_ty: *Type = elem_ty
4643 // Trailing `.field` after the [idx]: e.g. `obj.field[idx].subfield = x`.
4644 // The bracket branch used to stop here and leave `.subfield` unconsumed,
4645 // so it misparsed as a bare ident-assign (T#parser-dot-bracket-dot-assign-lhs;
4646 // surfaced by nx_eqsat `g.classes[id].canon = id`). Walk the field chain
4647 // on the element struct -- mirrors the dot-loop above + read-side
4648 // parse_field_chain so the write and read paths agree.
4649 while peek_kind(P) == TK_DOT {
4650 advance_tok(P)
4651 let bf_tok: *Tok = advance_tok(P)
4652 let bf_name: *u8 = tok_text_ptr(bf_tok)
4653 var bf_len: i64 = 0
4654 while bf_name[bf_len] != 0 { bf_len = bf_len + 1 }
4655 var bf_stty: *Type = cur_ty
4656 if bf_stty != (0 as *Type) {
4657 if bf_stty.kind == TY_PTR {
4658 if bf_stty.pointee != (0 as *Type) { bf_stty = bf_stty.pointee }
4659 }
4660 }
4661 if bf_stty == (0 as *Type) { break }
4662 let bf_field: *StructField = ir_type_struct_find_field(bf_stty, bf_name, bf_len)
4663 if bf_field == (0 as *StructField) { break }
4664 let bf_off: i64 = safe_const_i64(P, bf_field.offset, "parse.nx:dot-bracket-dot-offset" as *u8)
4665 cur_addr = ir_emit_gep(P.current_bb, cur_addr, bf_off, bf_field.ty)
4666 cur_ty = bf_field.ty
4667 if peek_kind(P) == TK_DOT {
4668 if bf_field.ty != (0 as *Type) {
4669 if bf_field.ty.kind == TY_PTR {
4670 cur_addr = ir_emit_load(P.current_bb, cur_addr, bf_field.ty)
4671 }
4672 }
4673 }
4674 }
4675 if peek_kind(P) == TK_ASSIGN {
4676 advance_tok(P)
4677 let rhs: i64 = parse_expr(P)
4678 match_kind(P, TK_SEMI)
4679 ir_emit_store(P.current_bb, cur_addr, rhs, cur_ty)
4680 return 0
4681 }
4682 // No assign: load + discard for parser progress.
4683 ir_emit_load(P.current_bb, cur_addr, cur_ty)
4684 match_kind(P, TK_SEMI)
4685 return 0
4686 }
4687 if peek_kind(P) == TK_ASSIGN {
4688 advance_tok(P)
4689 let rhs: i64 = parse_expr(P)
4690 match_kind(P, TK_SEMI)
4691 ir_emit_store(P.current_bb, v, rhs, ir_type_i64())
4692 return 0
4693 }
4694 match_kind(P, TK_SEMI)
4695 return 0
4696}
4697
4698// Expression-statement fallback with Q1 forward-progress guarantee.
4699func parse_stmt_expr_fallback(P: *Parser) -> i64 {
4700 let prev_pos: i64 = P.pos
4701 parse_expr(P)
4702 match_kind(P, TK_SEMI)
4703 if P.pos == prev_pos {
4704 if peek_kind(P) != TK_EOF {
4705 advance_tok(P)
4706 }
4707 }
4708 return 0
4709}
4710
4711func parse_stmt_list(P: *Parser) -> i64 {
4712 // Loop invariant guards: after EVERY statement, P.current_bb
4713 // must still point to a BasicBlock whose .parent is a valid
4714 // Function. If a stmt violates this, we catch the violation
4715 // BEFORE the next stmt runs, narrowing the culprit to a
4716 // single handler.
4717 //
4718 // Forward-progress guard (Q1): if parse_stmt ever returns
4719 // WITHOUT advancing P.pos, the loop would spin forever. We
4720 // assert progress as a belt-and-braces check; parse_stmt
4721 // itself guarantees this via its expression-statement
4722 // recovery path (see parse.nx:LINE-expr-stmt-fwd-progress).
4723 while peek_kind(P) != TK_RBRACE {
4724 if peek_kind(P) == TK_EOF { return 0 }
4725 nx_assert_ptr(P.current_bb as *u8,
4726 "parse_stmt_list: pre-stmt bb" as *u8)
4727 nx_assert_ptr(P.current_bb.parent as *u8,
4728 "parse_stmt_list: pre-stmt bb.parent" as *u8)
4729 let pre_pos: i64 = P.pos
4730 parse_stmt(P)
4731 nx_assert(P.pos > pre_pos,
4732 "parse_stmt_list: no forward progress" as *u8)
4733 }
4734 return 0
4735}
4736
4737// ---- duplicate-definition guard (debt 1785447657) -------------------
4738//
4739// Compare a token's identifier text against a (ptr,len) name INCLUDING
4740// the NUL terminator, so "foo" can never match a prefix of "foobar".
4741func nx_dupdef_name_eq(t: *Tok, name: *u8, name_len: i64) -> i64 {
4742 let txt: *u8 = tok_text_ptr(t)
4743 var k: i64 = 0
4744 var ok: i64 = 1
4745 var go: i64 = 1
4746 while go == 1 {
4747 if k >= name_len { go = 0 }
4748 if go == 1 {
4749 let a: i64 = txt[k] as i64
4750 let b: i64 = name[k] as i64
4751 if a != b { ok = 0 go = 0 }
4752 if go == 1 { k = k + 1 }
4753 }
4754 }
4755 if ok == 1 {
4756 let tail: i64 = txt[name_len] as i64
4757 if tail != 0 { ok = 0 }
4758 }
4759 return ok
4760}
4761
4762// Line of the FIRST *definition* (body, not forward decl) of `name` in
4763// this translation unit, or 0 if none is found.
4764//
4765// Runs ONLY on the duplicate-definition error path, which exits
4766// immediately -- so a full O(tokens) rescan costs nothing, and we avoid
4767// growing struct Function with a def-line field (its layout is load-
4768// bearing for the self-hosting bootstrap; never-brick says do not
4769// perturb what you do not have to).
4770//
4771// Scanning P.toks is correct w.r.t. conditional compilation: @if/@ifdef
4772// branches are eliminated in the LEXER (nx_tokenizer.nx,
4773// _lex_skip_to_matching_endif advances L.pos WITHOUT emitting tokens),
4774// so an eliminated branch is not in this array to be found.
4775func nx_find_first_def_line(P: *Parser, name: *u8, name_len: i64) -> i64 {
4776 var i: i64 = 0
4777 var result: i64 = 0
4778 var go: i64 = 1
4779 var hit: i64 = 0
4780 var j: i64 = 0
4781 var depth: i64 = 0
4782 var verdict: i64 = 0
4783 while go == 1 {
4784 let t: *Tok = tok_at(P.toks, i)
4785 let k: i64 = t.kind
4786 if k == TK_EOF { go = 0 }
4787 if go == 1 {
4788 hit = 0
4789 if k == TK_FUNC {
4790 let nt: *Tok = tok_at(P.toks, i + 1)
4791 let nk: i64 = nt.kind
4792 if nk == TK_IDENT {
4793 if nx_dupdef_name_eq(nt, name, name_len) == 1 { hit = 1 }
4794 }
4795 if hit == 1 {
4796 // Settle declaration-vs-definition exactly the way the
4797 // grammar does: after the name, the first '{' or ';'
4798 // seen at paren depth 0 decides.
4799 j = i + 2
4800 depth = 0
4801 verdict = 0
4802 while verdict == 0 {
4803 let s: *Tok = tok_at(P.toks, j)
4804 let sk: i64 = s.kind
4805 if sk == TK_EOF { verdict = 2 }
4806 if sk == TK_LPAREN { depth = depth + 1 }
4807 if sk == TK_RPAREN { depth = depth - 1 }
4808 if depth == 0 {
4809 if sk == TK_LBRACE { verdict = 1 }
4810 if sk == TK_SEMI { verdict = 2 }
4811 }
4812 j = j + 1
4813 }
4814 if verdict == 1 {
4815 let nline: i64 = nt.line
4816 result = nline
4817 go = 0
4818 }
4819 }
4820 }
4821 if go == 1 { i = i + 1 }
4822 }
4823 }
4824 return result
4825}
4826
4827// ---- function declaration ----
4828//
4829// `func NAME ( PARAMS ) -> TYPE { BODY }`
4830
4831func parse_function(P: *Parser) -> i64 {
4832 match_kind(P, TK_FUNC)
4833 let name_tok: *Tok = advance_tok(P)
4834 nx_require_ident_name(P, name_tok)
4835 // Compute the name's actual byte length. Earlier the literal 4
4836 // was passed to ir_function_new for every function; find_function
4837 // gates on name_len equality, so only 4-char names ever resolved.
4838 // Compute once + thread through both the forward-decl and full-def
4839 // paths so the call's stored name_len matches the search key.
4840 let name_ptr: *u8 = tok_text_ptr(name_tok)
4841 var name_len: i64 = 0
4842 while name_ptr[name_len] != 0 { name_len = name_len + 1 }
4843 sys_write(2, name_ptr, name_len)
4844 sys_write(2, ":" as *u8, 1)
4845
4846 // Parameter list: gather name + type pairs into a scratch buffer
4847 // so we can register them as alloca-backed locals AFTER the
4848 // Function + entry block exist. Each param yields an alloca +
4849 // ir_param + store-from-param, mirroring parse.c.
4850 // STUB(parser, T#parser-003): params capped at 8.
4851 // Plan: heap-allocate sized by an early scan, OR raise to 32
4852 // with a hard-error on overflow (better: error so we never
4853 // silently truncate a wider signature).
4854 // Closes when: any signature with >8 params -- none today.
4855 let param_names_raw: *u8 = sys_mmap(8 * 8 + 16)
4856 let param_names: *i64 = param_names_raw as *i64
4857 let param_tys_raw: *u8 = sys_mmap(8 * 8 + 16)
4858 let param_tys: *i64 = param_tys_raw as *i64
4859 var n_params: i64 = 0
4860 match_kind(P, TK_LPAREN)
4861 while peek_kind(P) != TK_RPAREN {
4862 if peek_kind(P) == TK_EOF { return 0 }
4863 let pn_tok: *Tok = advance_tok(P)
4864 nx_require_ident_name(P, pn_tok)
4865 let pn_addr: i64 = pn_tok as i64
4866 param_names[n_params] = pn_addr
4867 if match_kind(P, TK_COLON) {
4868 let pty: *Type = parse_type(P)
4869 let pty_addr: i64 = pty as i64
4870 param_tys[n_params] = pty_addr
4871 } else {
4872 param_tys[n_params] = 0
4873 }
4874 n_params = n_params + 1
4875 if peek_kind(P) == TK_COMMA { advance_tok(P) }
4876 }
4877 match_kind(P, TK_RPAREN)
4878 // RETURN TYPE: capture it. Until 2026-08-01 this line called parse_type(P) and
4879 // DISCARDED the result, and both the forward-decl and full-def paths below then
4880 // hardcoded `ir_type_i64()` -- so EVERY function in the IR claimed to return i64,
4881 // including the ones returning pointers. That is the SAME defect as the parameter
4882 // types (parsed, used locally, thrown away) and it is why a call result carried no
4883 // pointerness: ir_emit_call faithfully copies callee.ret_ty, which was always i64.
4884 // Consequence measured: argument type checking reported `takes_ptr(mkp(), 16)` as
4885 // passing an INTEGER when mkp() is declared `-> *u8`, an 88% false-positive rate
4886 // across the corpus that traced back here.
4887 var declared_ret: *Type = 0 as *Type
4888 if match_kind(P, TK_ARROW) {
4889 declared_ret = parse_type(P)
4890 }
4891 if declared_ret == (0 as *Type) { declared_ret = ir_type_i64() }
4892 // Forward declaration (`func NAME(...) -> T ;` with no body).
4893 // Register the Function so subsequent call sites resolve by name;
4894 // no body parsed. When the full def arrives later it UPGRADES
4895 // this existing entry rather than creating a duplicate
4896 // (T#bar3-codegen-003 closed 2026-04-26: previously the duplicate
4897 // emitted both a stub-only forward-decl symbol AND the full def,
4898 // and the linker picked the stub -- callers got an immediate
4899 // ret-zero, breaking nxc.elf-self-compiled binaries).
4900 if match_kind(P, TK_SEMI) {
4901 // T#parser-004 (2026-05-20): if prepass_register_funcs OR an
4902 // earlier forward decl already registered this name, REUSE
4903 // that entry instead of duplicating. Idempotent.
4904 let ret_ty_fwd: *Type = declared_ret
4905 let fwd_existing: *Function = find_function(P.module, name_ptr, name_len)
4906 var fwd: *Function = fwd_existing
4907 if fwd_existing == (0 as *Function) {
4908 fwd = ir_function_new(P.module, name_ptr, name_len, ret_ty_fwd)
4909 }
4910 fwd.n_params = n_params
4911 return 0
4912 }
4913
4914 match_kind(P, TK_LBRACE)
4915 let ret_ty: *Type = declared_ret // was hardcoded ir_type_i64() -- see the note above
4916 // If a forward-decl already registered this name, REUSE that
4917 // Function entry instead of creating a duplicate. Closes
4918 // T#bar3-codegen-003 -- the missing piece of the
4919 // forward-decl-then-full-def pattern.
4920 let existing: *Function = find_function(P.module, name_ptr, name_len)
4921 if existing != (0 as *Function) {
4922 if existing.n_blocks == 0 {
4923 // Empty entry => was a forward decl. Upgrade it.
4924 P.current_fn = existing
4925 P.current_fn.ret_ty = ret_ty
4926 P.current_fn.n_params = n_params
4927 } else {
4928 // MEASURED FAIL-OPEN, ROOT-FIXED 2026-07-31 (debt 1785447657,
4929 // handed to this lane by ws=sota).
4930 //
4931 // This branch used to create a SECOND Function entry, on an
4932 // assumption stated in a comment and never once verified:
4933 // "backend will see two same-named functions and error".
4934 // IT DOES NOT. nx_cc compiled _hdl_build/nx_gate_sweep.nx --
4935 // which had three functions EACH defined twice after two
4936 // nx_fs_write calls returned 503 (a lost response; both had
4937 // applied) -- into a clean, working 86223B binary with zero
4938 // diagnostics, and let one body silently win. It was caught
4939 // only because nx_fs_write's uniqueness contract refused a
4940 // later edit as AMBIGUOUS: the editor was stricter than the
4941 // language.
4942 //
4943 // A duplicate definition is precisely what a botched merge, a
4944 // retried write, or two agents on one shared file produce, and
4945 // this tree has all three live. The compiler is the last line
4946 // that can catch a merge artefact, so it fails CLOSED here,
4947 // where both line numbers are still recoverable.
4948 //
4949 // SAFE FOR CONDITIONAL COMPILATION: @if/@ifdef/@ifndef branches
4950 // are eliminated in the LEXER (nx_tokenizer.nx, skip-to-matching
4951 // -endif advances past source WITHOUT emitting tokens), so two
4952 // mutually-exclusive definitions never both reach this point.
4953 // nx_probe.nx's target-conditional _probe_compiled_isa /
4954 // _probe_pointer_width_bits pairs still compile unchanged.
4955 let prev_line: i64 = nx_find_first_def_line(P, name_ptr, name_len)
4956 sys_write(2, "nx_parse: line " as *u8, 15)
4957 nx_put_dec_err(name_tok.line)
4958 sys_write(2, ": duplicate definition of '" as *u8, 27)
4959 nx_put_tok_text_err(name_tok)
4960 sys_write(2, "' -- already defined at line " as *u8, 29)
4961 nx_put_dec_err(prev_line)
4962 sys_write(2, "\n" as *u8, 1)
4963 nx_diag_note_error()
4964 // RECOVERY: parse the duplicate body into a FRESH entry (lookups keep
4965 // resolving to the first definition; nx_cc provably tolerates the
4966 // second entry -- that WAS the old fail-open). End gate discards it.
4967 P.current_fn = ir_function_new(P.module, name_ptr, name_len, ret_ty)
4968 P.current_fn.n_params = n_params
4969 }
4970 } else {
4971 P.current_fn = ir_function_new(P.module, name_ptr, name_len, ret_ty)
4972 P.current_fn.n_params = n_params
4973 }
4974 // ARGUMENT TYPE CHECKING (2026-08-01): record which parameters are POINTERS.
4975 // The types were ALREADY parsed into param_tys above and were previously used only to
4976 // size the allocas and then discarded -- which is precisely why nx_cc could check
4977 // arity but never types, and why an mmap'd *i64 could land in an `i64` parameter and
4978 // become a loop bound (the sev-8 ev_num_after silent-zero, debt 1785518763).
4979 // Bit 63 marks the mask KNOWN so a not-yet-parsed forward stub is skipped, not guessed.
4980 var ptr_mask: i64 = 0
4981 var mi: i64 = 0
4982 while mi < n_params {
4983 let mty: *Type = param_tys[mi] as *Type
4984 if mty != (0 as *Type) {
4985 if mty.kind == TY_PTR { ptr_mask = ptr_mask | (1 << mi) }
4986 }
4987 mi = mi + 1
4988 }
4989 P.current_fn.param_ptr_mask = ptr_mask | (1 << 63)
4990
4991 P.current_bb = ir_block_new(P.current_fn)
4992 nx_assert_ptr(P.current_bb as *u8,
4993 "parse_function: bb NULL after ir_block_new" as *u8)
4994 nx_assert_ptr(P.current_bb.parent as *u8,
4995 "parse_function: bb.parent NULL after ir_block_new" as *u8)
4996 P.n_locals = 0
4997
4998 // Register each param as an alloca-backed local. ir_param builds
4999 // a VAL_PARAM; we alloca + store-from-param so the body can
5000 // reassign and mem2reg (later) can promote.
5001 var pi: i64 = 0
5002 while pi < n_params {
5003 var pty: *Type = param_tys[pi] as *Type
5004 if pty == (0 as *Type) { pty = ir_type_i64() }
5005 let pv: i64 = ir_param(P.current_fn, pi, pty)
5006 let addr: i64 = ir_emit_alloca(P.current_bb, pty)
5007 ir_emit_store(P.current_bb, addr, pv, pty)
5008 let pn_tok2: *Tok = param_names[pi] as *Tok
5009 let pname: *u8 = tok_text_ptr(pn_tok2)
5010 var kind: i64 = TY_I64
5011 if pty.kind == TY_STRUCT { kind = TY_STRUCT }
5012 add_local(P, pname, addr, 1, kind, pty)
5013 pi = pi + 1
5014 }
5015
5016 inject_statics(P) // bind every `static` name into this function's scope
5017 P.block_base = P.n_locals // the body block starts after params+statics
5018 parse_stmt_list(P)
5019 // Terminate the function's final open block. A body whose last
5020 // statement is a loop/if leaves the exit/join block OPEN (no
5021 // OP_RETURN / OP_BR tail). Backends emit blocks in creation order,
5022 // so later-created blocks (loop body, if-arms) land AFTER the open
5023 // block and it FALLS THROUGH into a sibling block instead of
5024 // returning. Caught 2026-06-09: nx_ttt_evaluate's board-full DRAW
5025 // store fell through into the cell-empty ONGOING store, clobbering
5026 // the verdict on every draw (x86; same IR feeds RISC-V + WAT).
5027 // The backend's defensive end-of-function epilogue only covers an
5028 // open block that happens to be emitted last -- every block must
5029 // carry its own terminator.
5030 let fin_bb: *BasicBlock = P.current_bb
5031 if fin_bb != (0 as *BasicBlock) {
5032 var fin_terminated: i64 = 0
5033 let fin_tail: *Instr = fin_bb.tail
5034 if fin_tail != (0 as *Instr) {
5035 if fin_tail.op == OP_RETURN { fin_terminated = 1 }
5036 if fin_tail.op == OP_BR { fin_terminated = 1 }
5037 if fin_tail.op == OP_BR_COND { fin_terminated = 1 }
5038 if fin_tail.op == OP_TAIL_CALL { fin_terminated = 1 }
5039 }
5040 // ⚠A synthesized fall-off-the-end return MUST carry a real constant. Passing 0 as a
5041 // "no value" sentinel is indistinguishable from VALUE ID 0, which is the function's
5042 // FIRST PARAMETER -- so an unterminated block silently returned param0. Measured
5043 // 2026-08-04: emu_run's exit block emitted `movq -7184(%rbp),%rax` (the %rdi slot),
5044 // the caller read that pointer as a length and wrote past a 512B buffer. (1785883072)
5045 if fin_terminated == 0 { ir_emit_return(fin_bb, ir_const_i64(fin_bb.parent, 0)) }
5046 }
5047 match_kind(P, TK_RBRACE)
5048 return 0
5049}
5050
5051// ---- module loop ----
5052
5053// T#parser-004 bits-up fix (2026-05-20): two-pass parser.
5054//
5055// Pass 1 -- prepass_register_funcs: walk every `func NAME(...)`
5056// declaration at module level, pre-register a stub Function entry
5057// in the module's function table. Subsequent call-sites earlier
5058// in the file resolve to these stubs.
5059//
5060// Pass 2 -- the existing main parse loop. When parse_function
5061// sees a name that already has an entry (from pass 1), the
5062// existing "upgrade existing entry" path (line ~2438 in this file,
5063// T#bar3-codegen-003 fix from 2026-04-26) populates the body
5064// without creating a duplicate.
5065//
5066// Eliminates the C-style "must define before use" requirement
5067// caught by bench/nx_parse_order_check.sh. ADDITIVE: no change
5068// to existing parse_function logic; pass 1 just feeds the
5069// upgrade path the right input set up-front.
5070//
5071// Brace-depth walker handles nested braces correctly; depth
5072// counter operates on TOKENS not chars so string-literal braces
5073// (already lexed as TK_STRING) don't confuse it.
5074//
5075// Forward declarations (`func NAME(...) -> T ;`) and full
5076// definitions both feed the same find_function lookup, so a
5077// file that has BOTH a forward decl AND a full def for the same
5078// name still works (pass 1 sees both, both are no-ops since the
5079// stub from the first occurrence already registered the name).
5080// Pre-pass — walk every `struct NAME { ... }` declaration at module
5081// level, pre-register an empty Type so forward references in
5082// later-defined struct fields (`struct Instr { callee: *Function }`
5083// where Function appears later in source order) resolve via
5084// lookup_struct instead of falling through to the TY_VOID placeholder.
5085// Mirrors parse.c's two-pass struct handling that this self-host
5086// parser was missing — caused the bootstrap-divergence on chained
5087// pointer-field READS through Instr.callee in the IR dump primitive.
5088// Pass 2's parse_struct_decl detects the existing entry and mutates
5089// its already-registered Type via ir_type_struct_add_field so the
5090// pointee held in earlier-parsed *FwdName references becomes fully
5091// populated once Function's body is processed.
5092// Resolve a primitive base-type NAME -> a fresh *Type with correct
5093// size/align. Returns null for anything that is not a known primitive
5094// (caller decides what to do with a non-primitive alias target).
5095// Mirrors the primitive table in parse_type so the two never drift.
5096func resolve_primitive_type(name: *u8) -> *Type {
5097 if streq_n(name, "i64", 3) { return alloc_type(TY_I64, 8, 8) }
5098 if streq_n(name, "u64", 3) { return alloc_type(TY_I64, 8, 8) }
5099 if streq_n(name, "i32", 3) { return alloc_type_s(TY_I32, 4, 4) } // signed subword -> sign-extend on load
5100 if streq_n(name, "u32", 3) { return alloc_type(TY_I32, 4, 4) } // unsigned -> zero-extend (default)
5101 if streq_n(name, "i16", 3) { return alloc_type_s(TY_I16, 2, 2) } // signed subword -> sign-extend on load
5102 if streq_n(name, "u16", 3) { return alloc_type(TY_I16, 2, 2) } // unsigned -> zero-extend
5103 if streq_n(name, "i8", 2) { return alloc_type_s(TY_I8, 1, 1) } // signed subword -> sign-extend on load
5104 if streq_n(name, "u8", 2) { return alloc_type(TY_I8, 1, 1) } // unsigned -> zero-extend (x509 0xA0 witness)
5105 if streq_n(name, "bool", 4) { return alloc_type(TY_BOOL, 1, 1) }
5106 if streq_n(name, "f32", 3) { return alloc_type(TY_F32, 4, 4) }
5107 if streq_n(name, "f64", 3) { return alloc_type(TY_F64, 8, 8) }
5108 return 0 as *Type
5109}
5110
5111// PASS 0b -- prepass_register_aliases: walk every top-level
5112// `type NAME = BASE` declaration and register NAME in the SAME structs
5113// pool that parse_type's lookup_struct already consults, mapped to the
5114// resolved primitive *Type for BASE. This is the ADDITIVE fix for the
5115// T#nx-int-alias-size-0 bug: before this, an alias field type (e.g.
5116// `nx_int`) fell through parse_type to the `TY_VOID size 0` placeholder,
5117// so EVERY struct field of an aliased type got size 0 -> all later field
5118// offsets collapsed -> a pointer field landed at offset 0 and was later
5119// overwritten by an int field -> deref of a tiny integer -> segfault.
5120// (Found bits-up: NxMesh.verts read back as the literal 4.) The C
5121// bootstrap resolved aliases; the self-hosted parser did not -> a
5122// self-host parity gap that silently corrupted the whole print stack.
5123// Data-driven: reads the real `type` decls, no hardcoded alias list.
5124// Chained aliases (type A = B; type B = i64) resolve because we register
5125// in source order and look up already-registered names too.
5126func prepass_register_aliases(P: *Parser) -> i64 {
5127 let saved_pos: i64 = P.pos
5128 P.pos = 0
5129 while peek_kind(P) != TK_EOF {
5130 // A top-level alias is the IDENT "type" followed by NAME '=' BASE.
5131 // 'type' is not a reserved keyword (lexed as TK_IDENT), so match
5132 // on the text, and require the '=' form to avoid colliding with
5133 // any other identifier that happens to be named "type".
5134 if peek_kind(P) == TK_IDENT {
5135 let t0: *Tok = advance_tok(P)
5136 let w0: *u8 = tok_text_ptr(t0)
5137 if streq_n(w0, "type", 4) == 1 {
5138 if peek_kind(P) == TK_IDENT {
5139 let nt: *Tok = advance_tok(P)
5140 let nm: *u8 = tok_text_ptr(nt)
5141 var nlen: i64 = 0
5142 while nm[nlen] != 0 { nlen = nlen + 1 }
5143 if peek_kind(P) == TK_ASSIGN {
5144 advance_tok(P) // consume '='
5145 if peek_kind(P) == TK_IDENT {
5146 let bt: *Tok = advance_tok(P)
5147 let bn: *u8 = tok_text_ptr(bt)
5148 // Resolve BASE: primitive first, else an
5149 // already-registered alias/struct (chained).
5150 var rty: *Type = resolve_primitive_type(bn)
5151 if rty == (0 as *Type) { rty = lookup_struct(P, bn) }
5152 // Only register if we resolved to a sized type
5153 // AND the alias name is not already taken.
5154 if rty != (0 as *Type) {
5155 if lookup_struct(P, nm) == (0 as *Type) {
5156 if P.n_structs >= 1024 {
5157 parse_die("too many module type aliases" as *u8, 30)
5158 }
5159 let e: *StructEntry = struct_entry_at(P, P.n_structs)
5160 let dst: *u8 = (e as i64) as *u8
5161 var k: i64 = 0
5162 while k < 64 {
5163 dst[k] = nm[k]
5164 if nm[k] == 0 { k = 64 }
5165 k = k + 1
5166 }
5167 e.name_len = nlen
5168 e.ty = rty
5169 P.n_structs = P.n_structs + 1
5170 }
5171 }
5172 }
5173 }
5174 }
5175 continue
5176 }
5177 continue
5178 }
5179 advance_tok(P)
5180 }
5181 P.pos = saved_pos
5182 return 0
5183}
5184
5185func prepass_register_structs(P: *Parser) -> i64 {
5186 let saved_pos: i64 = P.pos
5187 P.pos = 0
5188
5189 while peek_kind(P) != TK_EOF {
5190 if peek_kind(P) == TK_STRUCT {
5191 advance_tok(P) // consume 'struct'
5192
5193 if peek_kind(P) != TK_IDENT {
5194 continue
5195 }
5196 let name_tok: *Tok = advance_tok(P)
5197 let name_ptr: *u8 = tok_text_ptr(name_tok)
5198 var name_len: i64 = 0
5199 while name_ptr[name_len] != 0 { name_len = name_len + 1 }
5200
5201 // Skip generic type params `<T, U, ...>` — they're parsed
5202 // for real in pass 2; here we just need the name.
5203 if match_kind(P, TK_LT) {
5204 while peek_kind(P) != TK_GT {
5205 if peek_kind(P) == TK_EOF { break }
5206 advance_tok(P)
5207 }
5208 match_kind(P, TK_GT)
5209 }
5210
5211 // Register stub Type if not already present.
5212 let existing: *Type = lookup_struct(P, name_ptr)
5213 if existing == (0 as *Type) {
5214 if P.n_structs >= 1024 {
5215 parse_die("too many module structs" as *u8, 23)
5216 }
5217 let stub_ty: *Type = ir_type_struct_new(name_ptr, name_len)
5218 let e: *StructEntry = struct_entry_at(P, P.n_structs)
5219 let dst_base: i64 = e as i64
5220 let dst: *u8 = dst_base as *u8
5221 var k: i64 = 0
5222 while k < 64 {
5223 dst[k] = name_ptr[k]
5224 if name_ptr[k] == 0 { k = 64 }
5225 k = k + 1
5226 }
5227 e.name_len = name_len
5228 e.ty = stub_ty
5229 P.n_structs = P.n_structs + 1
5230 }
5231
5232 // Skip the body `{ ... }` so we land on the next top-
5233 // level form. Brace-balanced walk so nested blocks /
5234 // generic args don't trip us up.
5235 var saw_lbrace: i64 = 0
5236 var keep: i64 = 1
5237 while keep == 1 {
5238 let k2: i64 = peek_kind(P)
5239 if k2 == TK_EOF {
5240 keep = 0
5241 } else {
5242 if k2 == TK_LBRACE {
5243 saw_lbrace = 1
5244 keep = 0
5245 } else {
5246 advance_tok(P)
5247 }
5248 }
5249 }
5250
5251 if saw_lbrace == 1 {
5252 advance_tok(P) // consume initial '{'
5253 var depth: i64 = 1
5254 while depth > 0 {
5255 let k3: i64 = peek_kind(P)
5256 if k3 == TK_EOF {
5257 depth = 0
5258 } else {
5259 if k3 == TK_LBRACE { depth = depth + 1 }
5260 if k3 == TK_RBRACE { depth = depth - 1 }
5261 advance_tok(P)
5262 }
5263 }
5264 }
5265 } else {
5266 advance_tok(P)
5267 }
5268 }
5269
5270 P.pos = saved_pos
5271 return 0
5272}
5273
5274// Count the DECLARED parameters of the function whose name token was just consumed, by walking its
5275// parameter list to the matching ')'. Depth-tracked, so a parenthesised type inside the list cannot
5276// end the walk early, and commas are only counted at depth 1. Consumes those tokens; the caller's
5277// skip loop then continues from '->' / '{' / ';' and is unaffected. A malformed or absent list
5278// returns 0 and leaves the diagnostic to pass 2, which has the real error machinery.
5279func prepass_count_params(P: *Parser) -> i64 {
5280 if peek_kind(P) != TK_LPAREN { return 0 }
5281 advance_tok(P)
5282 var depth: i64 = 1
5283 var commas: i64 = 0
5284 var sawtok: i64 = 0
5285 var go: i64 = 1
5286 while go == 1 {
5287 let k: i64 = peek_kind(P)
5288 if k == TK_EOF {
5289 go = 0
5290 } else {
5291 if k == TK_LPAREN {
5292 depth = depth + 1
5293 sawtok = 1
5294 advance_tok(P)
5295 } else {
5296 if k == TK_RPAREN {
5297 depth = depth - 1
5298 advance_tok(P)
5299 if depth == 0 { go = 0 } else { sawtok = 1 }
5300 } else {
5301 if k == TK_COMMA {
5302 if depth == 1 { commas = commas + 1 }
5303 sawtok = 1
5304 advance_tok(P)
5305 } else {
5306 sawtok = 1
5307 advance_tok(P)
5308 }
5309 }
5310 }
5311 }
5312 }
5313 if sawtok == 0 { return 0 }
5314 return commas + 1
5315}
5316
5317func prepass_register_funcs(P: *Parser) -> i64 {
5318 let saved_pos: i64 = P.pos
5319 P.pos = 0
5320
5321 while peek_kind(P) != TK_EOF {
5322 if peek_kind(P) == TK_FUNC {
5323 advance_tok(P) // consume 'func'
5324
5325 if peek_kind(P) != TK_IDENT {
5326 // malformed -- let pass 2 emit the real diagnostic
5327 continue
5328 }
5329 let name_tok: *Tok = advance_tok(P)
5330 let name_ptr: *u8 = tok_text_ptr(name_tok)
5331 var name_len: i64 = 0
5332 while name_ptr[name_len] != 0 { name_len = name_len + 1 }
5333
5334 // Register stub Function entry if not already present.
5335 // n_params and ret_ty are placeholder values -- the
5336 // full def in pass 2 upgrades both via the existing
5337 // T#bar3-codegen-003 path.
5338 let existing: *Function = find_function(P.module, name_ptr, name_len)
5339 var fstub: *Function = existing
5340 if existing == (0 as *Function) {
5341 let stub_ret: *Type = ir_type_i64()
5342 fstub = ir_function_new(P.module, name_ptr, name_len, stub_ret)
5343 }
5344 // ARITY (2026-07-20): record the DECLARED parameter count on the stub. Without it the
5345 // stub reports n_params == 0 until pass 2 reaches the definition, so every FORWARD call
5346 // is unverifiable and the call-site arity check has to skip it. Counting here -- the
5347 // walk is already sitting on '(' -- makes every signature known before ANY body is
5348 // parsed, which is exactly what lets the check cover forward calls and mutual recursion.
5349 let np: i64 = prepass_count_params(P)
5350 if fstub != (0 as *Function) { fstub.n_params = np }
5351
5352 // RETURN POINTER-NESS (2026-08-01): record on the stub whether this function
5353 // returns a POINTER, exactly as the arity line above records its parameter
5354 // count and for the same reason. Without it a stub carries the i64 PLACEHOLDER
5355 // until pass 2 reaches its body, so a call to a function defined LATER with no
5356 // forward declaration yields a result typed i64 even when it returns *u8.
5357 // Measured consequence: argument type checking could only be enabled in the
5358 // pointer-into-integer direction, because an i64 argument was ambiguous
5359 // (genuine integer OR placeholder). With this, the placeholder disappears and
5360 // the integer-into-pointer direction becomes decidable too.
5361 // Token-level on purpose: we only need "does the return type begin with '*'",
5362 // and calling parse_type here would register struct types during a prepass
5363 // whose only job is to record signatures.
5364 if peek_kind(P) == TK_ARROW {
5365 advance_tok(P)
5366 if peek_kind(P) == TK_STAR {
5367 if fstub != (0 as *Function) {
5368 fstub.ret_ty = alloc_type(TY_PTR, 8, 8)
5369 }
5370 }
5371 }
5372
5373 // Skip the rest of this declaration (signature + body
5374 // OR semicolon for forward decl) so we land on the next
5375 // top-level form. Brace-balanced walk handles nested
5376 // blocks; semicolon-terminated forward decls exit early.
5377 var saw_lbrace: i64 = 0
5378 var keep: i64 = 1
5379 while keep == 1 {
5380 let k: i64 = peek_kind(P)
5381 if k == TK_EOF {
5382 keep = 0
5383 } else {
5384 if k == TK_SEMI {
5385 advance_tok(P)
5386 keep = 0
5387 } else {
5388 if k == TK_LBRACE {
5389 saw_lbrace = 1
5390 keep = 0
5391 } else {
5392 advance_tok(P)
5393 }
5394 }
5395 }
5396 }
5397
5398 if saw_lbrace == 1 {
5399 advance_tok(P) // consume initial '{'
5400 var depth: i64 = 1
5401 while depth > 0 {
5402 let k2: i64 = peek_kind(P)
5403 if k2 == TK_EOF {
5404 depth = 0 // bail; pass 2 will error properly
5405 } else {
5406 if k2 == TK_LBRACE { depth = depth + 1 }
5407 if k2 == TK_RBRACE { depth = depth - 1 }
5408 advance_tok(P)
5409 }
5410 }
5411 }
5412 } else {
5413 advance_tok(P)
5414 }
5415 }
5416
5417 P.pos = saved_pos
5418 return 0
5419}
5420
5421// Caller hands us a Module (so the driver can wire lex -> parse ->
5422// opt -> ... with shared state). If null, we allocate a fresh one
5423// ourselves -- convenient for stand-alone parser tests.
5424func parse_module(toks: *Tok, m: *Module) -> *Module {
5425 let p_raw: *u8 = sys_mmap(512) // +block_base; headroom so a future field cannot silently overrun
5426 let P: *Parser = p_raw as *Parser
5427 P.toks = toks
5428 P.pos = 0
5429 if m == (0 as *Module) {
5430 let m_raw: *u8 = sys_mmap(256)
5431 let new_m: *Module = m_raw as *Module
5432 new_m.name = "anon" as *u8
5433 // Allocate a Function pool so ir_function_new can append.
5434 // Stride is 176 bytes. 4096 slots = ~720 KB.
5435 //
5436 // ROOT CAUSE for T#selfhost-006 globals corruption (closed
5437 // 2026-04-26): pool was sized at 256 slots. nxc.nx has
5438 // 419 functions, so function entries 257..419 overflowed
5439 // past the pool boundary into the adjacent globals pool
5440 // (kernel returns adjacent virtual pages from consecutive
5441 // anonymous mmap calls), corrupting written globals --
5442 // which explained why the dump read zeros from addresses
5443 // where ir_add_global_string had written real data.
5444 // ir.nx::ir_module_new already had this bumped to 4096
5445 // (T#selfhost-004 fix); the parse_module code path was
5446 // missed. Both paths now agree.
5447 let fn_pool: *u8 = sys_mmap(4096 * 176 + 64)
5448 new_m.functions = fn_pool as *Function
5449 new_m.n_functions = 0
5450 // Globals pool: must be initialised here too. Without this,
5451 // ir_add_global_string would write into a zeroed pointer +
5452 // SIGSEGV on the first string literal (task #21 stage-2 root
5453 // cause -- found 2026-04-24). 4096 slots × 80 bytes/slot.
5454 let glob_pool: *u8 = sys_mmap(4096 * 80 + 64)
5455 new_m.globals = glob_pool as *Global
5456 new_m.n_globals = 0
5457 new_m.globals_cap = 4096
5458 P.module = new_m
5459 } else {
5460 P.module = m
5461 }
5462 P.current_fn = 0 as *Function
5463 P.current_bb = 0 as *BasicBlock
5464 let locals_raw: *u8 = sys_mmap(NX_PARSE_LOCALS_CAP * LOCAL_BYTES + 64)
5465 P.locals = locals_raw as *Local
5466 P.n_locals = 0
5467 P.loop_head = 0 as *BasicBlock
5468 P.loop_exit = 0 as *BasicBlock
5469 // Capacity bumped 128 -> 2048 (2026-05-19) -> 8192 (2026-05-20)
5470 // because the substrate is OFF C bootstrap -- this is the
5471 // daily-driver parser and the cap belongs to NishiLang. Live
5472 // HTTPS get import graph trace 2026-05-20 = 740 consts across
5473 // 100 modules, but the parser counts every const at parse time
5474 // including indirect ones; 2048 was overflowed by the full
5475 // nx_https_get_live_real_ca + cert chain + Google-target test
5476 // graph. 8192 gives 4x headroom for adding AES-GCM, HTTP/2,
5477 // and image-decoder primitives without re-bumping every session.
5478 // Per Cardinal 21 (resource awareness): each MConst is ~72 bytes,
5479 // so 8192 * 72 = ~590 KB of Parser-struct growth -- still
5480 // harmless for a compiler running once at build time.
5481 let mconsts_raw: *u8 = sys_mmap(8192 * MCONST_BYTES + 64)
5482 P.mconsts = mconsts_raw as *MConst
5483 P.n_mconsts = 0
5484 // Module-level table caps bumped 2026-05-19 alongside the mconsts
5485 // bump so the self-host doesn't silently corrupt the heap at the
5486 // same scale where mconsts was first to hit. Bits-up substrate's
5487 // growing import graph now declares hundreds of structs (TLS
5488 // session + X.509 + URL + DNS + crypto + ...). Per Cardinal 12.
5489 let structs_raw: *u8 = sys_mmap(1024 * STRUCT_ENTRY_BYTES + 64)
5490 P.structs = structs_raw as *StructEntry
5491 P.n_structs = 0
5492 let statics_raw: *u8 = sys_mmap(512 * STATIC_ENTRY_BYTES + 64)
5493 P.statics = statics_raw as *StaticEntry
5494 P.n_statics = 0
5495 let enums_raw: *u8 = sys_mmap(256 * ENUM_ENTRY_BYTES + 64)
5496 P.enums = enums_raw as *EnumEntry
5497 P.n_enums = 0
5498 // 8 active-type-param slots; a slot is one i64 holding a *u8
5499 // pointer to the NUL-terminated param name.
5500 let ap_raw: *u8 = sys_mmap(8 * 8 + 16)
5501 P.active_params = ap_raw as *i64
5502 P.n_active_params = 0
5503 // PASS 0: pre-register every top-level struct name so
5504 // forward-referenced field types (`callee: *Function` in Instr
5505 // where Function appears later in source order) resolve via
5506 // lookup_struct. Mirrors parse.c.
5507 prepass_register_structs(P)
5508 // PASS 0b: register `type NAME = BASE` aliases (into the structs
5509 // pool, so parse_type's lookup resolves them). AFTER structs so an
5510 // alias to a struct type resolves; T#nx-int-alias-size-0 fix.
5511 prepass_register_aliases(P)
5512 // PASS 1: pre-register every top-level func name so call sites
5513 // earlier in the file resolve. T#parser-004 bits-up fix.
5514 // ADDITIVE: pass 2 below is the original parse loop; only
5515 // change is that the symbol table is pre-populated.
5516 prepass_register_funcs(P)
5517
5518 // PASS 2: original parse loop. parse_function upgrades the
5519 // pass-1 stubs via the existing find_function lookup at
5520 // line ~2540 (T#bar3-codegen-003 upgrade path).
5521 //
5522 // Progress marker: one dot per top-level item. Suppressed if
5523 // first char written this run is newline (i.e. lex just wrote
5524 // its own status line). Helps isolate parse_function stalls.
5525 var n_items: i64 = 0
5526 while peek_kind(P) != TK_EOF {
5527 if peek_kind(P) == TK_FUNC {
5528 parse_function(P)
5529 n_items = n_items + 1
5530 // Fine-grained: 1 dot per function. Noisy for small
5531 // sources, essential for localising parse stalls on
5532 // large self-host source.
5533 sys_write(2, "." as *u8, 1)
5534 continue
5535 }
5536 if peek_kind(P) == TK_CONST {
5537 parse_module_const(P)
5538 continue
5539 }
5540 if peek_kind(P) == TK_STRUCT {
5541 parse_struct_decl(P)
5542 continue
5543 }
5544 if peek_kind(P) == TK_STATIC {
5545 parse_static_decl(P)
5546 continue
5547 }
5548 // `@priv` VISIBILITY ATTRIBUTE (debt seq1460/seq1471): consumed EXPLICITLY so the
5549 // marker is acknowledged syntax rather than something the generic advance_tok at the
5550 // bottom of this loop happens to discard. The following TK_FUNC parses normally, so
5551 // this is additive and back-compatible -- default visibility stays PUBLIC and no
5552 // existing source changes meaning. Cross-file ENFORCEMENT (calling a @priv symbol
5553 // from another file must be a compile error) is the follow-on in seq1471; that is
5554 // what turns adoption from INFERRED into EXACT.
5555 if peek_kind(P) == TK_PRIV {
5556 advance_tok(P)
5557 continue
5558 }
5559 if peek_kind(P) == TK_ENUM {
5560 parse_module_enum(P)
5561 continue
5562 }
5563 // A token matching no top-level form used to be DISCARDED SILENTLY, one at a time.
5564 // That is how a stray `return` -- left outside its function by ONE BRACE TOO MANY in a
5565 // deep else-chain -- vanished with no diagnostic, truncating the function body and
5566 // costing a day of misattribution to the compiler (2026-08-04, debt 1785883072).
5567 // Statement keywords can NEVER be valid at module level, so they are REFUSED with a
5568 // line number. Everything else still skips, but SAYS SO: silence is never the default.
5569 let stray: *Tok = tok_at(P.toks, P.pos)
5570 var stray_stmt: i64 = 0
5571 if stray.kind == TK_RETURN { stray_stmt = 1 }
5572 if stray.kind == TK_LET { stray_stmt = 1 }
5573 if stray.kind == TK_VAR { stray_stmt = 1 }
5574 if stray.kind == TK_IF { stray_stmt = 1 }
5575 if stray.kind == TK_WHILE { stray_stmt = 1 }
5576 if stray_stmt == 1 {
5577 nx_puts_err("\nnx_cc: STATEMENT AT MODULE LEVEL, line " as *u8)
5578 nx_puti_err(stray.line)
5579 nx_puts_err(" -- outside any function. Most often ONE BRACE TOO MANY closed a function early.\n" as *u8)
5580 nx_assert(0, "stray statement at module level" as *u8)
5581 }
5582 // NOTE: non-statement leftovers (TK_IDENT/TK_ASSIGN) are still skipped SILENTLY here.
5583 // MEASURED 2026-08-04: 132 such tokens are dropped while compiling a 60-line probe, so a
5584 // warning on them would fire hundreds of times per build and be trained away -- a
5585 // diagnostic nobody reads is worse than none. Ledgered as its own finding (1785883072).
5586 advance_tok(P)
5587 }
5588 // MULTI-ERROR END GATE (2026-08-05): every recovered diagnostic above kept
5589 // parsing so the author sees ALL errors in ONE build, but the module they
5590 // produced holds placeholder values and is POISONED. Refuse to hand it to
5591 // codegen: no output is ever emitted from a build that reported an error.
5592 if nx_diag_nerr > 0 {
5593 sys_write(2, "nx_parse: " as *u8, 10)
5594 nx_put_dec_err(nx_diag_nerr)
5595 sys_write(2, " error(s) -- no output emitted\n" as *u8, 31)
5596 sys_exit(2)
5597 }
5598 return P.module
5599}
5600
5601// `enum Name { V1, V2(T), V3 }` -- register each variant as a
5602// qualified module const, and when any variant has a payload
5603// auto-generate a shadow struct `Name { tag: i64, payload: i64 }`
5604// so constructors can alloca + store tag/payload and match can
5605// extract them via GEP. Plain enums (no payload anywhere) skip
5606// the shadow and stay int-discriminant representations.
5607func parse_module_enum(P: *Parser) -> i64 {
5608 match_kind(P, TK_ENUM)
5609 let ename_tok: *Tok = advance_tok(P)
5610 let ename: *u8 = tok_text_ptr(ename_tok)
5611 // Measure the enum name so we can build qualified "Name::V"
5612 // labels per variant. Capped at 32 chars so the concatenation
5613 // fits the 64-byte MConst name slot.
5614 var en_len: i64 = 0
5615 while ename[en_len] != 0 { en_len = en_len + 1 }
5616 if en_len > 32 { en_len = 32 }
5617
5618 // Optional generic type parameters: `enum Name<T, E, ...>`.
5619 // Collect names + publish as active while parsing variants
5620 // (payload types may reference T / E). Stored on the shadow
5621 // struct after it's created so instantiate_generic_n can
5622 // substitute them on use.
5623 let e_tparam_raw: *u8 = sys_mmap(8 * 8 + 16)
5624 let e_tparams: *i64 = e_tparam_raw as *i64
5625 var e_n_tparams: i64 = 0
5626 if match_kind(P, TK_LT) {
5627 let etp0: *Tok = advance_tok(P)
5628 e_tparams[e_n_tparams] = tok_text_ptr(etp0) as i64
5629 e_n_tparams = e_n_tparams + 1
5630 while match_kind(P, TK_COMMA) {
5631 let etpn: *Tok = advance_tok(P)
5632 e_tparams[e_n_tparams] = tok_text_ptr(etpn) as i64
5633 e_n_tparams = e_n_tparams + 1
5634 }
5635 match_kind(P, TK_GT)
5636 }
5637 let e_saved_active: i64 = P.n_active_params
5638 var e_ai: i64 = 0
5639 while e_ai < e_n_tparams {
5640 P.active_params[e_saved_active + e_ai] = e_tparams[e_ai]
5641 e_ai = e_ai + 1
5642 }
5643 P.n_active_params = e_saved_active + e_n_tparams
5644
5645 match_kind(P, TK_LBRACE)
5646
5647 // Pre-scan variants looking for any payload. We save P.pos,
5648 // walk the variant list counting payloads, then restore and
5649 // parse the body for real with knowledge of whether to build
5650 // a shadow struct.
5651 let saved_pos: i64 = P.pos
5652 var any_payload: i64 = 0
5653 while peek_kind(P) != TK_RBRACE {
5654 if peek_kind(P) == TK_EOF { return 0 }
5655 advance_tok(P) // variant name
5656 if match_kind(P, TK_LPAREN) {
5657 any_payload = 1
5658 parse_type(P)
5659 match_kind(P, TK_RPAREN)
5660 }
5661 if match_kind(P, TK_ASSIGN) {
5662 if match_kind(P, TK_MINUS) {}
5663 advance_tok(P) // int literal
5664 }
5665 match_kind(P, TK_COMMA)
5666 }
5667 P.pos = saved_pos // rewind
5668
5669 // Build shadow struct once up front so all constructors and match
5670 // arms share the same Type pointer. Fields are both i64 for the
5671 // first pass -- payload is i64 regardless of the declared type,
5672 // which keeps lowering trivial even when variants differ.
5673 var shadow: *Type = 0 as *Type
5674 if any_payload == 1 {
5675 shadow = ir_type_struct_new(ename, en_len)
5676 let tag_name: *u8 = "tag"
5677 ir_type_struct_add_field(shadow, tag_name, 3, ir_type_i64())
5678 let pay_name: *u8 = "payload"
5679 ir_type_struct_add_field(shadow, pay_name, 7, ir_type_i64())
5680 // Preserve generic param list so instantiate_generic_n can
5681 // substitute against concrete args at use sites.
5682 if e_n_tparams > 0 {
5683 shadow.n_type_params = e_n_tparams
5684 shadow.type_params = e_tparams
5685 }
5686 // Register the shadow in the struct table so `*Name` parses
5687 // as a pointer to it.
5688 if P.n_structs >= 1024 {
5689 parse_die("too many module structs (enum shadow)" as *u8, 38)
5690 }
5691 let se: *StructEntry = struct_entry_at(P, P.n_structs)
5692 let sd_base: i64 = se as i64
5693 let sd: *u8 = sd_base as *u8
5694 var sk: i64 = 0
5695 while sk < 64 {
5696 sd[sk] = ename[sk]
5697 if ename[sk] == 0 { sk = 64 }
5698 sk = sk + 1
5699 }
5700 se.name_len = en_len
5701 se.ty = shadow
5702 P.n_structs = P.n_structs + 1
5703 }
5704
5705 // Register the enum itself (even plain enums) in the enums table
5706 // so parse_primary Name::Variant + matc