code wiki / (root) / nx_parse.nx

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