code wiki / (root) / nx_parse.nx

nx_parse.nx source

↩ module page · 5077 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" 47import "nx_f64.nx" 48import "nx_f64_div.nx" 49 50// Shared Tok record + TK_* TokenKind constants. 51import "nx_lex_kinds.nx" 52 53// Real IR builders. Previously stubbed here; since the module- 54// library split, parse.nx directly emits into ir.nx's Module/Function 55// pools. Still-missing builders (load/store/alloca/br_cond) remain 56// stubbed at the bottom of this file until ir.nx grows them. 57import "nx_ir.nx" 58 59// Expanded-line -> (file, source-line) map. Consumed by nx_diag_at below; produced by 60// nx_import.nx. Imported in BOTH so neither file depends on the other's import list. 61import "nx_linemap.nx" 62 63// ---- runtime helpers ---- 64// sys_mmap comes from syscalls.nx (via ir.nx); no local copy. 65 66func tok_at(toks: *Tok, i: i64) -> *Tok { 67 let base: i64 = toks as i64 68 return (base + i * TOK_BYTES) as *Tok 69} 70 71// One-char marker for tracing parser dispatch. Caller picks a unique 72// character per function entry; sequence on stderr reveals the exact 73// dispatch path taken at runtime. Cheap (1 syscall, 1 byte) so we 74// can drop it on every parse_* function without bloating output. 75// Switch to a no-op (just `return 0`) once the parser is bug-free. 76func tr(c: i64) -> i64 { 77 let buf: *u8 = sys_mmap(8) 78 buf[0] = c 79 sys_write(2, buf, 1) 80 return 0 81} 82 83// Bits-up equivalent of parse.c's die(P, msg). Emits a stderr line 84// then sys_exit(2). Added 2026-05-19 with the mconsts cap bump so 85// the NishiLang self-host fails LOUDLY on parser-table overflow 86// instead of silently corrupting the heap past the mmap'd capacity. 87// Per Cardinal 12 (defensive at boundaries): the parser is the 88// boundary between input source and compiled IR; overflow there is 89// the kind of silent-corruption false-OK class [[feedback-no-false- 90// ok-substrate-honesty-audit]] explicitly names. 91// Cap on type arguments in a generic instantiation `Name<A, B, ...>` (2026-08-07). The array that 92// receives them was allocated 8 * 8 + 16 = 80 bytes and the append loop had NO bound of any kind, 93// so a deep or malformed instantiation wrote straight past it. Page granularity hid that for as long 94// as an 80-byte request got its own 4096-byte page; the small-allocation arena packs neighbours and 95// turned it into silent corruption of the parser's type table, which surfaced as type diagnostics 96// fired against declarations that were CORRECT. The cap and the allocation are now derived from the 97// same constant so they cannot drift apart again, and a breach REFUSES rather than truncating. 98const NX_GENERIC_MAX_ARGS: i64 = 8 99 100// Cap on DECLARED PARAMETERS in a function signature (2026-08-07). Derived from the IR call 101// operand ceiling: ir_emit_call fills op0..op23, so 24 is the widest signature the whole pipeline 102// can actually carry, and the two must not drift apart. 103const NX_PARSE_MAX_PARAMS: i64 = 24 104 105// LN42 position statics: declared BEFORE parse_die because a static, like a const, must be declared 106// before the first function that reads it. 107static nx_pd_line: i64 108static nx_pd_col: i64 109// LN42: the same refusal, now with WHERE and a caret. A site reached before any token was consumed has no 110// honest position, so it keeps the bare form rather than pointing at line 0 -- A LOCATION THAT WAS GUESSED IS 111// WORSE THAN NO LOCATION, BECAUSE THE READER WILL GO THERE. 112func parse_die(msg: *u8, msg_len: i64) -> i64 { 113 if nx_pd_line > 0 { 114 nx_diag_organ_at(nx_pd_line) 115 nx_diag_puts(": " as *u8) 116 nx_diag_puts(msg) 117 nx_diag_puts("\n" as *u8) 118 nx_diag_caret(nx_pd_line, nx_pd_col) 119 nx_diag_puts(" why the build stopped: the parser refused this construct, so there is no program to emit here. The caret is on the last token it consumed -- the construct that broke is at or just before it.\n" as *u8) 120 nx_diag_puts(" fix: read the sentence above as the rule that was broken. If this line looks correct, the real break is usually just before it: an unclosed brace, a missing comma, or a name that is not declared yet.\n" as *u8) 121 sys_exit(2) 122 return 0 123 } 124 sys_write(2, "nx_parse: " as *u8, 10) 125 sys_write(2, msg, msg_len) 126 sys_write(2, "\n" as *u8, 1) 127 sys_exit(2) 128 return 0 129} 130 131// ---- CARET + SOURCE SNIPPET (2026-08-05) ---------------------------------- 132// Toks have carried line AND col all along; what the Parser never had was the SOURCE, so a 133// diagnostic could say WHERE but never SHOW it. rustc and Elm both show the offending line with a 134// caret under the column, and that is most of why their errors feel different. 135// PLUMBED ADDITIVELY: a module static the driver fills right after it reads the input, so NO 136// existing caller of parse_module changes signature, and any caller that does not set it simply 137// gets no snippet (the diagnostic still carries line/col). Degrades to exactly today's behaviour. 138// NOTE ON LINE NUMBERS: nx_cc parses PRE-EXPANDED source (imports inlined), so the line is the 139// expanded-source line -- which is the line the caret must point at to be truthful about what the 140// compiler actually read. Mapping back to per-file lines is a separate rung. 141// ---- MULTI-ERROR RECOVERY (2026-08-05) ------------------------------------ 142// Every semantic diagnostic used to sys_exit(2) on the spot, so one broken 143// build reported ONE error and charged the author a full rebuild per mistake. 144// rustc and Elm report everything they can see. RECOVERY CONTRACT: a site 145// whose parser POSITION is already sane (the construct was fully consumed 146// before the verdict) calls nx_diag_note_error() and continues with a safe 147// placeholder value; parse_module then REFUSES to return the module while 148// nx_diag_nerr > 0, so recovered (poisoned) IR can NEVER reach codegen -- 149// the SITES-LIVE silent-value-id-0 lesson stays intact because recovery 150// values exist only on builds that are already doomed to exit 2. 151// Parser-DESYNC sites (reserved keyword in name position, operator at 152// expression start, parse_die table overflow) stay FATAL BY DESIGN: past a 153// desync every further diagnostic would point at innocent code. 154const NX_DIAG_MAX_ERRS: i64 = 20 155static nx_diag_nerr: i64 156 157func nx_diag_note_error() -> i64 { 158 nx_diag_nerr = nx_diag_nerr + 1 159 if nx_diag_nerr >= NX_DIAG_MAX_ERRS { 160 nx_diag_puts("error: too many problems (20) -- fix the ones above and build again.\n" as *u8) 161 nx_diag_puts(" why the build stopped: after twenty errors the parser is usually reporting consequences of the first few, not new defects, and a longer list hides the cause.\n" as *u8) 162 nx_diag_puts(" fix: start with the FIRST error above (it names the file and line with a caret); one fix there often clears many of the rest, then build again.\n" as *u8) 163 sys_exit(2) 164 } 165 return 0 166} 167 168// VOICE HELPER (2026-08-06, the 5W+H / Elm-class cardinals): every diagnostic sentence goes 169// through here, length computed at runtime -- the whole hand-counted sys_write length bug 170// class (which twice silently truncated messages) cannot exist on this path. 171func nx_diag_puts(s: *u8) -> i64 { 172 var n: i64 = 0 173 while s[n] != (0 as u8) { n = n + 1 } 174 sys_write(2, s, n) 175 return 0 176} 177 178// The WHY for the whole undefined-name class, said once, in user language (5W+H: a message 179// that names WHAT but not WHY once cost a 3-hour debug -- the rule exists for this line). 180func nx_diag_why_undefined() -> i64 { 181 nx_diag_puts(" why the build stopped: this name has no definition, so the program would have nothing to run here -- building on would ship a crash or a wrong value at this exact spot.\n" as *u8) 182 return 0 183} 184 185static nx_diag_src: *u8 186static nx_diag_src_len: i64 187 188func nx_diag_set_source(p: *u8, n: i64) -> i64 { 189 nx_diag_src = p 190 nx_diag_src_len = n 191 return 0 192} 193 194// Print " <source line>" then " <spaces>^" under column col (1-based). Silent when the source 195// was never registered or the line cannot be found -- a caret pointing at the wrong place would be 196// worse than none. 197func nx_diag_caret(line: i64, col: i64) -> i64 { 198 if (nx_diag_src as i64) == 0 { return 0 } 199 if nx_diag_src_len <= 0 { return 0 } 200 if line <= 0 { return 0 } 201 let s: *u8 = nx_diag_src 202 var start: i64 = 0 - 1 203 if line == 1 { start = 0 } 204 var i: i64 = 0 205 var cur: i64 = 1 206 while i < nx_diag_src_len { 207 if s[i] == (10 as u8) { 208 cur = cur + 1 209 if cur == line { start = i + 1; i = nx_diag_src_len } 210 } 211 if i < nx_diag_src_len { i = i + 1 } 212 } 213 if start < 0 { return 0 } 214 var e: i64 = start 215 var go: i64 = 1 216 while go == 1 { 217 if e >= nx_diag_src_len { go = 0 } else { 218 if s[e] == (10 as u8) { go = 0 } else { e = e + 1 } 219 } 220 } 221 var n: i64 = e - start 222 if n > 200 { n = 200 } 223 if n <= 0 { return 0 } 224 sys_write(2, " " as *u8, 2) 225 sys_write(2, ((s as i64) + start) as *u8, n) 226 sys_write(2, "\n " as *u8, 3) 227 var c: i64 = 1 228 while c < col { 229 var ch: *u8 = " " as *u8 230 if c - 1 < n { if s[start + c - 1] == (9 as u8) { ch = "\t" as *u8 } } 231 sys_write(2, ch, 1) 232 c = c + 1 233 } 234 sys_write(2, "^\n" as *u8, 2) 235 return 1 236} 237 238// Print source line N indented, NO caret -- the quoting half of the caret machinery, used by 239// nx_dym_note to SHOW a suggestion's declaration instead of describing it in shorthand. 240func nx_diag_show_line(line: i64) -> i64 { 241 if (nx_diag_src as i64) == 0 { return 0 } 242 if nx_diag_src_len <= 0 { return 0 } 243 let s: *u8 = nx_diag_src 244 var cur: i64 = 1 245 var start: i64 = 0 - 1 246 if line == 1 { start = 0 } 247 var i: i64 = 0 248 while i < nx_diag_src_len { 249 if s[i] == (10 as u8) { 250 cur = cur + 1 251 if cur == line { start = i + 1; i = nx_diag_src_len } 252 } 253 if i < nx_diag_src_len { i = i + 1 } 254 } 255 if start < 0 { return 0 } 256 var e: i64 = start 257 var go: i64 = 1 258 while go == 1 { 259 if e >= nx_diag_src_len { go = 0 } else { 260 if s[e] == (10 as u8) { go = 0 } else { e = e + 1 } 261 } 262 } 263 var n: i64 = e - start 264 if n > 200 { n = 200 } 265 if n <= 0 { return 0 } 266 sys_write(2, " " as *u8, 6) 267 sys_write(2, ((s as i64) + start) as *u8, n) 268 sys_write(2, "\n" as *u8, 1) 269 return 1 270} 271 272// ---- DID-YOU-MEAN (2026-08-05) -------------------------------------------- 273// rustc and Elm both answer a misspelled name with the name you meant; nx_cc used to answer 274// "call to undefined function: sys_wrte" and stop, which tells the author WHAT is wrong and 275// nothing about what to type. The names are all already in the module: this is a scan, not a 276// new index. Bounded Levenshtein (classic two-row DP) with an early length filter. 277// COST CONTROL: only runs on the ERROR path, immediately before exiting. 278func nx_dym_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 279 280// Edit distance between a (len la) and b (len lb), capped: any pair whose lengths differ by more 281// than udget is rejected without the DP. Returns a large number when over budget. 282func nx_dym_dist(a: *u8, la: i64, b: *u8, lb: i64, budget: i64) -> i64 { 283 var d: i64 = la - lb 284 if d < 0 { d = 0 - d } 285 if d > budget { return 999 } 286 if la > 64 { return 999 } 287 if lb > 64 { return 999 } 288 let prev: *u8 = sys_mmap(80) 289 let cur: *u8 = sys_mmap(80) 290 var j: i64 = 0 291 while j <= lb { prev[j] = j as u8; j = j + 1 } 292 var i: i64 = 1 293 while i <= la { 294 cur[0] = i as u8 295 var k: i64 = 1 296 while k <= lb { 297 var cost: i64 = 1 298 if a[i - 1] == b[k - 1] { cost = 0 } 299 var best: i64 = (prev[k] as i64) + 1 300 let ins: i64 = (cur[k - 1] as i64) + 1 301 if ins < best { best = ins } 302 let sub: i64 = (prev[k - 1] as i64) + cost 303 if sub < best { best = sub } 304 cur[k] = best as u8 305 k = k + 1 306 } 307 var c: i64 = 0 308 while c <= lb { prev[c] = cur[c]; c = c + 1 } 309 i = i + 1 310 } 311 return prev[lb] as i64 312} 313 314// DYM WINNER CONTEXT (2026-08-05, operator bar: "did-you-mean is the floor -- the suggestion 315// must take the reader to what the recommended spelling DOES, so an AI or a human judges by 316// CONTEXT, not naming"). The suggesters stash the winning candidate here; the diagnostic site 317// calls nx_dym_note(P) after the caret to print ONE `note:` line with the winner's declaration 318// facts. kind: 0 none, 1 function, 2 module const, 3 local, 4 struct, 5 generic type param. 319static nx_dym_last_kind: i64 320static nx_dym_last_p: *u8 321static nx_dym_last_l: i64 322static nx_dym_last_aux: i64 323 324// Print " -- did you mean 'X'?" for the closest DEFINED function name, if one is close enough. 325// Silent when nothing is near: a wrong suggestion is worse than none. 326func nx_dym_suggest_fn(m: *Module, name: *u8, nlen: i64) -> i64 { 327 if m == (0 as *Module) { return 0 } 328 var budget: i64 = 2 329 if nlen <= 4 { budget = 1 } 330 var best_d: i64 = 999 331 var best_p: *u8 = 0 as *u8 332 var best_l: i64 = 0 333 var best_np: i64 = 0 334 var i: i64 = 0 335 while i < m.n_functions { 336 let base: i64 = m.functions as i64 337 let f: *Function = (base + i * 176) as *Function 338 let fn_p: *u8 = f.name_start as *u8 339 let fn_l: i64 = f.name_len 340 if fn_l > 0 { 341 let dd: i64 = nx_dym_dist(name, nlen, fn_p, fn_l, budget) 342 if dd < best_d { best_d = dd; best_p = fn_p; best_l = fn_l; best_np = f.n_params } 343 } 344 i = i + 1 345 } 346 if best_d > budget { return 0 } 347 if best_d == 0 { return 0 } 348 if (best_p as i64) == 0 { return 0 } 349 nx_dym_last_kind = 1 350 nx_dym_last_p = best_p 351 nx_dym_last_l = best_l 352 nx_dym_last_aux = best_np 353 return 1 354} 355 356// Decimal-print an i64 to stderr (for line numbers in diagnostics). 357func nx_put_dec_err(n: i64) -> i64 { 358 if n == 0 { sys_write(2, "0" as *u8, 1) return 0 } 359 var m: i64 = n 360 if m < 0 { sys_write(2, "-" as *u8, 1) m = 0 - m } 361 let digits: *u8 = sys_mmap(24) 362 var k: i64 = 0 363 while m > 0 { 364 digits[k] = (0x30 + (m % 10)) as u8 365 m = m / 10 366 k = k + 1 367 } 368 var i: i64 = k - 1 369 while i >= 0 { 370 let one: *u8 = sys_mmap(1) 371 one[0] = digits[i] 372 sys_write(2, one, 1) 373 i = i - 1 374 } 375 return 0 376} 377 378// ---- PER-FILE LOCATION (2026-08-06) --------------------------------------- 379// nx_cc parses PRE-EXPANDED source, so every `line` in this file has always been a 380// line in the FLATTENED buffer: truthful about what the compiler read, and useless 381// for finding the file to edit. The note above nx_diag_caret said it plainly -- 382// "Mapping back to per-file lines is a separate rung". nx_linemap.nx is that rung; 383// the driver hands the map over the same additive way it hands over the source text. 384static nx_diag_lm: *LineMap 385static nx_diag_lm_scratch: *i64 386 387func nx_diag_set_linemap(lm: *LineMap) -> i64 { 388 nx_diag_lm = lm 389 // Publish the SAME map to the module that owns the type, so the x86 emitter can reach it 390 // without depending on where nx_parse happens to land in the expanded unit. One setter, 391 // two consumers, no ordering assumption. 392 lm_set_active(lm) 393 return 0 394} 395 396// Emit a diagnostic's location: `nx_parse.nx:412` when the map can PROVE it, and 397// `line 1118` -- today's exact bytes -- when it cannot. Returns 1 if a file was named. 398// 399// TWO CONTRACTS ARE LOAD-BEARING HERE. 400// (1) The literal "error at " prefix is shared by BOTH forms on purpose. nx_mgmt_api 401// anchors its diag_errors window on that string to lift errors out of a build log; 402// a writer and a reader that must agree should agree on the SHORTEST stable form, 403// so widening the anchor beats a flag day on both sides. 404// (2) Falling back is not a failure mode, it is the design. A WRONG file name sends 405// the author to edit a file that is not the problem -- the same class as a caret 406// under the wrong line, which this compiler shipped, caught, and fixed before 407// promotion (2026-08-05). Unknown prints the old form; it never guesses. 408// SHARED LOCATION RENDERER. Both voices below call this, so they can never drift apart on 409// what a location looks like -- the failure mode where a log-scraper learns one shape and a 410// second emitter quietly ships another. 411// Prints "<file>:<line>" when the map can PROVE it, else "line <line>" (today's exact bytes). 412// Returns 1 if a file was named. 413func nx_diag_loc(line: i64) -> i64 { 414 if (nx_diag_lm as i64) != 0 { 415 if (nx_diag_lm_scratch as i64) == 0 { nx_diag_lm_scratch = sys_mmap(32) as *i64 } 416 let sc: *i64 = nx_diag_lm_scratch 417 let fp: *i64 = sc 418 let sp: *i64 = ((sc as i64) + 8) as *i64 419 *fp = 0 420 *sp = 0 421 if lm_lookup(nx_diag_lm, line, fp, sp) == 1 { 422 let path: *u8 = lm_file_path(nx_diag_lm, *fp) 423 if (path as i64) != 0 { 424 nx_diag_puts(lm_basename(path)) 425 nx_diag_puts(":" as *u8) 426 nx_put_dec_err(*sp) 427 return 1 428 } 429 } 430 } 431 nx_diag_puts("line " as *u8) 432 nx_put_dec_err(line) 433 return 0 434} 435 436// THE 5W+H VOICE. "error at " is the anchor nx_mgmt_api uses to lift the error window out of 437// a build log, and it is a strict prefix of BOTH the mapped and unmapped forms -- so widening 438// the reader's anchor beat a flag day on both sides. 439func nx_diag_at(line: i64) -> i64 { 440 nx_diag_puts("error at " as *u8) 441 return nx_diag_loc(line) 442} 443 444// THE OLDER ORGAN-PREFIXED VOICE, for the sites whose message text has not been rewritten yet 445// (arity, argument type, reserved keyword, unreachable arm, non-exhaustive match, 446// const-before-declaration, duplicate definition). These gain FILE ATTRIBUTION today without 447// touching the "nx_parse:" prefix that same scraper also anchors on, which keeps the voice 448// rewrite a separate, independently reviewable change instead of a prerequisite. 449func nx_diag_organ_at(line: i64) -> i64 { 450 nx_diag_puts("nx_parse: " as *u8) 451 return nx_diag_loc(line) 452} 453 454// Print one token's inline text to stderr (printable bytes only, capped). 455func nx_put_tok_text_err(t: *Tok) -> i64 { 456 let txt: *u8 = tok_text_ptr(t) 457 let one: *u8 = sys_mmap(1) 458 var k: i64 = 0 459 var stop: i64 = 0 460 while stop == 0 { 461 let c: i64 = txt[k] as i64 462 if c < 0x20 { stop = 1 } 463 if c > 0x7E { stop = 1 } 464 if stop == 0 { 465 one[0] = txt[k] as u8 466 sys_write(2, one, 1) 467 k = k + 1 468 } 469 if k >= 40 { stop = 1 } 470 } 471 if k == 0 { sys_write(2, "." as *u8, 1) } // punctuation w/ empty inline text 472 return 0 473} 474 475// Diagnostic context: print a window of source tokens around `name_tok` 476// so the offending statement is visible even before full snippet+caret 477// (NDX-1 spec interim). Token text is inline in each Tok, so this works 478// without threading the source buffer through the parser. 479func nx_diag_token_window(P: *Parser, tidx: i64) -> i64 { 480 var ci: i64 = tidx - 8 481 if ci < 0 { ci = 0 } 482 sys_write(2, " near: " as *u8, 8) 483 while ci <= tidx + 2 { 484 let ct: *Tok = tok_at(P.toks, ci) 485 nx_put_tok_text_err(ct) 486 sys_write(2, " " as *u8, 1) 487 ci = ci + 1 488 } 489 sys_write(2, "\n" as *u8, 1) 490 return 0 491} 492 493// ---- Parser state ---- 494 495struct Local { 496 // Name stored as first 8 chars in text0..text7 (matches Tok layout). 497 name0: i64, name1: i64, name2: i64, name3: i64, 498 name4: i64, name5: i64, name6: i64, name7: i64, 499 value_id: i64, 500 is_alloca: i64, 501 ty_kind: i64, // 5 = i64, etc. (legacy fast tag) 502 // Full type carried so parse_field_chain can pick the right 503 // pointer stride. *u8 indexing must use stride 1, not the 504 // default i64 stride 8. Closes T#types-001's parser side. 505 ty: *Type, 506 // LN3 raw-pointer provenance (bck_ptr_provenance): the allocation extent in BYTES when this 507 // local is a `let`-bound pointer whose initializer the compiler could SEE allocate -- today 508 // that is `sys_mmap(<compile-time-const>)`, optionally through an `as *T` cast chain. 0 = 509 // no visible provenance (the honest default; a param, a loaded field, a runtime-sized mmap). 510 // Only `let` locals ever carry one: a `let` is SSA (parse_stmt_ident_assign refuses stores), 511 // so the extent can never go stale. Checks are injected ONLY under --ptrprov (g_ptrprov_live). 512 ext_bytes: i64, 513 // LN2 option enforcement (opt_enforce_unwrap): 1 = this pointer-typed local is PROVEN non-null on 514 // the current path (bound from &x / a string literal, or narrowed by `if p != 0 {`, an early-exit 515 // `if p == 0 { return }`, `while p != 0 {`, or nx_assert_ptr(p ...)). 0 = possibly null (the 516 // honest default: a call result, a param, a loaded field, a `0 as *T`). Read ONLY under 517 // --optenforce (g_optenforce_live); a reassignment re-derives it from the new value. 518 nn_checked: i64, 519 // LN4/LN5 OWNERSHIP (own_check_move / own_check_uaf): the ownership state of this local when it 520 // is a pointer bound to a VISIBLE ALLOCATION -- a sys_mmap call, optionally through an `as *T` 521 // cast chain (the same origin recogniser LN2/LN3 use, no second copy). 522 // 0 OWN_NONE not an owner; this rung says NOTHING about it (the honest default) 523 // 1 OWN_LIVE owns a live allocation 524 // 2 OWN_MOVED ownership was transferred away by __move(p) -- the name is dead (LN4) 525 // 3 OWN_RELEASED the allocation was handed to sys_munmap(p, n) -- the pages are gone (LN5) 526 // Read ONLY under --ownership (g_ownership_live). own_line carries the line of the move or the 527 // release, so a refusal can name BOTH ends -- the use AND the statement that ended the local's 528 // life. A diagnostic that names only the use makes the reader hunt for the other half. 529 own_state: i64, 530 own_line: i64, 531} 532 533// HAND-MAINTAINED MIRROR of `struct Local`'s size: 8 name slots + value_id + is_alloca + ty_kind + 534// ty + ext_bytes + nn_checked + own_state + own_line = 16 * 8. It is the ONE stride (parser_loc_at) 535// and the ONE pool size (parse_module's sys_mmap), so those two can never disagree -- but it is a 536// MIRROR of the struct, so adding a field without bumping it reads every local at the wrong offset. 537// COST OF THE LN4/LN5 BUMP 112 -> 128, named rather than left for someone to discover: the locals 538// pool is NX_PARSE_LOCALS_CAP * LOCAL_BYTES, so this is +131,072 B of address space per compiler 539// invocation (917,504 -> 1,048,576), mmapped once at parse_module entry and never grown. 540const LOCAL_BYTES: i64 = 128 541 542// Per-function locals pool capacity. Reset to 0 per function, so this is 543// the MAX simultaneous locals any single function may declare. Was 64 -- 544// missed when the mconsts pool was bumped off the C-bootstrap limit 545// (2026-05-20). 64 silently overflowed the locals pool into the ADJACENT 546// globals pool (consecutive anonymous mmaps share a boundary), corrupting 547// m.globals/m.n_globals -> garbage .rodata emission, for any function with 548// >64 locals (e.g. the ACME issuance drive's main). Bumped to 8192 (the 549// daily-driver cap belongs to NishiLang, not the bootstrap) + add_local now 550// die()s loudly on exhaustion instead of corrupting silently. 551const NX_PARSE_LOCALS_CAP: i64 = 8192 552 553// ONE NAMED CAP PER POOL, READ BY BOTH THE ALLOCATION AND EVERY GUARD. 554// These were bare literals written at the mmap and again at each `>=` check -- two rulers for one 555// invariant -- and they had ALREADY DRIFTED: the mconsts allocation was bumped 2048 -> 8192 on 556// 2026-05-19 and its two guards were left at 2048, so 6144 of the new slots were unreachable and the 557// very overflow that bump was written to fix still died at the old number. A cap written twice is two 558// caps, and the copy nobody updates is the one that decides. 559const NX_PARSE_STRUCTS_CAP: i64 = 1024 560const NX_PARSE_STATICS_CAP: i64 = 512 561const NX_PARSE_ENUMS_CAP: i64 = 256 562const NX_PARSE_MCONSTS_CAP: i64 = 8192 563// GLOBALS POOL: DERIVED FROM THE TOKEN STREAM (2026-09-05). The old text here said there was "no single token kind 564// to count" and left a DECLARED bound of 4096 -- and on 2026-09-05 a unit (nx_wgsl: the shader front-end + both 565// backends + the world and cast shaders) reached 4097 and the build stopped, the same class as the 256-slot 566// function pool this file already derived out of existence. There is no single kind, but there is a SUM: every 567// global the IR ever creates is anchored to a token -- a string literal (TK_STRING), a `static` (TK_STATIC), a 568// float literal (TK_FLOAT, the x86 constant pool) -- or is synthesised per function (TK_FUNC). count_global_tokens 569// sums those, and NX_PARSE_GLOBALS_SLACK is the one figure not derived from the input: headroom for entries the 570// backend synthesises without any of those tokens, named for that purpose and NOT load-bearing -- ir_add_global_* 571// still asserts against globals_cap, so exceeding it REFUSES LOUDLY instead of writing past the pool. 572// Surplus slots cost address space that is never faulted in (mmap); a shortfall used to cost a build. 573const NX_PARSE_GLOBALS_SLACK: i64 = 256 574// Headroom over the COUNTED `func` keywords for Functions the IR synthesises rather than parses. This 575// is the only figure here that is not derived from the input, so it is named for that one purpose and 576// it is deliberately NOT load-bearing: ir_new_function asserts against m.fn_cap, so exceeding it 577// REFUSES LOUDLY instead of running past the pool into the adjacent globals region. 578const NX_PARSE_FN_SLACK: i64 = 64 579 580struct Parser { 581 toks: *Tok, 582 pos: i64, 583 module: *Module, // parent module; owns Functions and Globals 584 current_fn: *Function, 585 current_bb: *BasicBlock, 586 locals: *Local, 587 n_locals: i64, 588 // First local index of the INNERMOST block. A redeclaration is only a duplicate within 589 // THIS block; shadowing an enclosing scope is legal and the estate relies on it. 590 block_base: i64, 591 592 // Innermost loop context 593 loop_head: *BasicBlock, 594 loop_exit: *BasicBlock, 595 596 // Module-level integer constants. `const NAME: i64 = N` registers 597 // here; parse_primary consults this list when an identifier misses 598 // the local table so const uses inside functions fold to literals. 599 mconsts: *MConst, 600 n_mconsts: i64, 601 602 // Module-level struct types. Each TY_STRUCT we parse is registered 603 // here so parse_type can resolve user struct names to their layout. 604 // STUB(parser, T#parser-002): pool capped at 64 entries. 605 // Plan: grow dynamically (mirror globals-pool growth in 606 // ir_module_new) + bounds-check in register_struct. 607 // Closes when: any nxc.nx self-compile with >64 struct decls. 608 structs: *StructEntry, 609 n_structs: i64, 610 611 // Module-level `static` declarations. Each maps an inline-name 612 // copy + its global id. On parse_function entry we inject one 613 // synthesised Local per static with a VAL_GLOBAL_ADDR Value, so 614 // use sites inside functions lower via the standard local-lookup 615 // path (is_alloca=1 => auto-load / indexed write / field chain). 616 statics: *StaticEntry, 617 n_statics: i64, 618 619 // Module-level enums. Tracks per-enum whether any variant has a 620 // payload: tagged enums get an auto-generated shadow struct 621 // (tag + payload fields) so constructors + match can build and 622 // deconstruct the ADT. Plain enums stay as int discriminants. 623 enums: *EnumEntry, 624 n_enums: i64, 625 626 // Active generic type parameters. Populated while parsing 627 // inside `struct Name<T, U> { ... }` so that parse_type can 628 // detect an identifier like `T` and produce a TY_PARAM node. 629 // Cleared after the struct's field list finishes. Static cap 630 // matches parse.c's args[8] limit. 631 active_params: *i64, // array of *u8 name ptrs 632 n_active_params: i64, 633 634 // V-LANGEXT M2 hygiene guard: depth of currently-parsing nested 635 // if-then-else expressions. Incremented at parse-time entry; checked 636 // against NX_PARSE_IFEXP_MAX_DEPTH; rejected if exceeded. Prevents 637 // pathological codegen from any source that nests if-expressions 638 // beyond what real-world code ever needs (e.g., 16 is already deeper 639 // than any human-readable nested ternary -- C `?:` chains are 640 // similarly capped by readability + most linters). 641 // Per [[feedback-nishilang-extension-guardrails-2026-05-27]] + 642 // operator 2026-05-27: "we want to avoid hoisting issues and 643 // namespace issues and all the other hygiene issues that make 644 // languages suck, really research and make sure we are adding good 645 // functionality not future nightmares". 646 ifexp_depth: i64, 647} 648 649// One entry per `enum NAME { ... }`. The shadow_ty is non-null only 650// when any variant declared a payload type -- plain enums keep the 651// int-discriminant representation and leave shadow_ty = null. 652struct EnumEntry { 653 name0: i64, name1: i64, name2: i64, name3: i64, 654 name4: i64, name5: i64, name6: i64, name7: i64, 655 name_len: i64, 656 has_payload: i64, 657 shadow_ty: *Type, 658 n_variants: i64, 659} 660 661const ENUM_ENTRY_BYTES: i64 = 96 662 663struct StaticEntry { 664 name0: i64, name1: i64, name2: i64, name3: i64, 665 name4: i64, name5: i64, name6: i64, name7: i64, 666 name_len: i64, 667 global_id: i64, 668 ty: *Type, 669} 670 671const STATIC_ENTRY_BYTES: i64 = 88 672 673// One entry in the Parser's struct lookup table. Keeps the Type 674// pointer (owned by ir.nx) plus an inline name copy so the name 675// survives past the lexer's token buffer lifetime. 676struct StructEntry { 677 name0: i64, name1: i64, name2: i64, name3: i64, 678 name4: i64, name5: i64, name6: i64, name7: i64, 679 name_len: i64, 680 ty: *Type, 681} 682 683const STRUCT_ENTRY_BYTES: i64 = 80 684 685struct MConst { 686 // First 64 bytes = name (null-terminated in place, matches Local 687 // layout so we can reuse the name-copy helpers). 688 name0: i64, name1: i64, name2: i64, name3: i64, 689 name4: i64, name5: i64, name6: i64, name7: i64, 690 val: i64, 691 // V-LANGEXT M1 (NishiLang self-host): string-literal in module- 692 // const declarations per [[feedback-nishilang-extension-guardrails- 693 // 2026-05-27]]. kind discriminates int (0) vs string (1); for 694 // strings, gid is the ir_add_global_string id + ty_ptr is the 695 // pointer-type cast. 696 kind: i64, 697 gid: i64, 698 ty_ptr: i64, 699} 700 701const MCONST_BYTES: i64 = 96 702 703// ---- token ops ---- 704 705// LN15: TK_STAR is the ONE line-leading operator that is genuinely ambiguous -- 706// `*p = v` is a pointer store (a real statement, parse_stmt_star), while `* b` can 707// only be a multiply continuing the line above. tok_line_continuation returns 708// TLC_STORE_OR_JOIN for it and hands the decision here, because deciding it needs 709// the token STREAM and not just the kind. 710// 711// Decide by looking for the store's `=` at bracket depth 0. The scan is bounded by 712// THIS PHYSICAL LINE, not by a chosen constant: a store statement lives on its own 713// line, so leaving the line is itself the answer. TK_EOF terminates unconditionally, 714// so the loop cannot run away on a malformed stream. 715// 716// Wrong-in-the-direction-of-the-incumbent by construction: anything this cannot 717// prove to be a store falls through to 0 = JOIN only for TK_STAR, the kind whose 718// old behaviour was already a boundary, so a miss costs a diagnostic, never silence. 719func sb_star_starts_store(P: *Parser) -> i64 { 720 let start: *Tok = tok_at(P.toks, P.pos) 721 let ln: i64 = start.line 722 var i: i64 = P.pos 723 var depth: i64 = 0 724 var scanning: i64 = 1 725 while scanning { 726 let t: *Tok = tok_at(P.toks, i) 727 if t.kind == TK_EOF { return 0 } 728 if t.line > ln { return 0 } 729 if t.kind == TK_SEMI { return 0 } 730 if t.kind == TK_LBRACE { return 0 } 731 if t.kind == TK_RBRACE { return 0 } 732 if t.kind == TK_LPAREN { depth = depth + 1 } 733 if t.kind == TK_LBRACKET { depth = depth + 1 } 734 if t.kind == TK_RPAREN { depth = depth - 1 } 735 if t.kind == TK_RBRACKET { depth = depth - 1 } 736 if depth == 0 { 737 if t.kind == TK_ASSIGN { return 1 } 738 } 739 i = i + 1 740 } 741 return 0 742} 743 744func peek_kind(P: *Parser) -> i64 { 745 let t: *Tok = tok_at(P.toks, P.pos) 746 return t.kind 747} 748 749// Statement-boundary detection for precedence-climbers. 750// 751// NishiLang allows optional semicolons. Without them, tokens like 752// `*`, `-`, and `&` are ambiguous: binary operators OR unary 753// operators starting a fresh statement (`*p`, `-x`, `&v`). Ports 754// parse.c's at_stmt_boundary (parse.c line 387): if the current 755// token is on a NEWER source line than the token just consumed, 756// treat it as a statement boundary and break out of the precedence 757// climber. 758// 759// Class-level fix for F-meta-2 (optional-token statement boundary 760// ambiguity). Without this, the regression_no_semis_optional case 761// parses `sys_mmap(16) as *i64\n*p = 100` as 762// `(sys_mmap(16) as *i64) * p = 100` -- corrupts the store + the 763// downstream load. See docs/NISHI_BUG_PREVENTION_PILLARS.md. 764// 765// Pillar 4 alignment: this is ADDITIVE -- it adds a guard at each 766// precedence-climber loop top without forbidding any valid source. 767// Code that explicitly continues across a newline can still do so 768// by parenthesising: `( a\n + b )` keeps the binop because the 769// open-paren resets the line-boundary semantics inside the paren 770// group (no newline insertion inside `()` per the lexer). 771func at_stmt_boundary(P: *Parser) -> i64 { 772 if P.pos == 0 { return 0 } 773 let cur: *Tok = tok_at(P.toks, P.pos) 774 let prev: *Tok = tok_at(P.toks, P.pos - 1) 775 if cur.line > prev.line { 776 // F-meta-2 refinement 2026-05-16: a newline is a statement 777 // boundary ONLY when the next token COULD start a statement. 778 // Always-binary operators (|, ^, /, %, ==, !=, <=, >=, <<, 779 // >>, &&, ||, =) can never appear at statement start in 780 // NishiLang, so when they follow a newline they're an 781 // expression continuation, not a new statement. This 782 // unblocks multi-line bitwise expressions like the pack4_i16 783 // body `return (a & 0xFFFF) | ((b & 0xFFFF) << 16) | ...` 784 // which previously dropped every term past the first 785 // because each `|` started on a new line and tripped the 786 // boundary check. 787 // 788 // Ambiguous tokens (*, -, &, +) STILL trigger the boundary 789 // -- the F-meta-2 fix targeted those, where `*p` and `-x` 790 // and `&v` and `+x` can legitimately start a fresh 791 // statement. The original case `sys_mmap(16) as *i64\n*p = 792 // 100` continues to parse correctly. 793 // 794 // Additive PREVENT per the four-pillar cardinal: this RELAXES 795 // statement-boundary detection for tokens that COULDN'T have 796 // been false positives, but keeps it strict for the tokens 797 // where ambiguity exists. No source patterns are forbidden 798 // by this change; previously-rejected multi-line expressions 799 // now parse, previously-accepted programs are unaffected. 800 // LN15: ONE SHARED RULER decides this now -- tok_line_continuation in 801 // nx_lex_kinds.nx, which lex.nx and parse.nx both already import. The 802 // bare token NUMBERS this function used to carry (56, 57, 43 ...) went 803 // with it: a kind spelled as a literal here and as TK_PIPE in the lexer 804 // is one constant written two ways, and two spellings drift silently. 805 // 806 // The three kinds LN15 adds -- TK_PLUS, TK_MINUS, TK_AMP -- are the ones 807 // this function used to call ambiguous. They are not: no statement FORM 808 // begins with them, so at statement level they can only ever have been a 809 // continuation of the line above. The reasoning is banked beside the 810 // predicate, where the next reader of it will actually find it. 811 let tlc: i64 = tok_line_continuation(cur.kind) 812 if tlc == TLC_CONTINUATION { return 0 } 813 if tlc == TLC_STORE_OR_JOIN { return sb_star_starts_store(P) } 814 return 1 815 } 816 return 0 817} 818func peek_at(P: *Parser, k: i64) -> *Tok { 819 return tok_at(P.toks, P.pos + k) 820} 821// LN42 (2026-09-03): THE POSITION parse_die NEVER HAD. nx_diag_organ_at has existed since the file-attribution 822// pass, waiting for a line number; parse_die had none, so 35 refusal sites printed a bare one-line sentence with 823// no file, no line, no caret and no fix -- and a whole-population census grouped 90 of them into a single blank 824// "no recognised diagnostic marker" class because there was nothing in them to key on. Every token in the unit 825// passes through advance_tok, so recording its line/col here costs two stores on the one chokepoint and gives 826// every parse_die site a truthful location without touching a single call site. 827// A DIAGNOSTIC WITHOUT A LOCATION IS A SENTENCE, NOT A DIAGNOSTIC -- AND IT IS ALSO UNGROUPABLE, SO IT HIDES 828// ITS OWN FREQUENCY FROM EVERY CENSUS THAT WOULD HAVE RANKED IT. 829func advance_tok(P: *Parser) -> *Tok { 830 let t: *Tok = tok_at(P.toks, P.pos) 831 nx_pd_line = t.line 832 nx_pd_col = t.col 833 P.pos = P.pos + 1 834 return t 835} 836func check_kind(P: *Parser, k: i64) -> i64 { 837 if peek_kind(P) == k { return 1 } 838 return 0 839} 840func match_kind(P: *Parser, k: i64) -> i64 { 841 if peek_kind(P) == k { 842 P.pos = P.pos + 1 843 return 1 844 } 845 return 0 846} 847 848// ---- generic monomorphization helpers ---------------------------- 849// 850// Port of parse.c's clone_type_substituting + instantiate_generic_n. 851// These functions realise `Option<T>` / `Result<T, E>` -- when a use 852// site references `Name<Arg1, Arg2>`, the template struct's field 853// list is walked and every TY_PARAM field that matches one of the 854// declared type-parameter names is replaced with the concrete arg. 855// The resulting monomorphic Type gets a mangled name (`Name$arg1 856// $arg2`) and is cached so repeated uses of the same instantiation 857// share one struct. 858// 859// These helpers are pure over Type graphs -- they do not read or 860// mutate Parser state beyond the struct registration cache. 861 862// Compare two byte ranges for exact equality. Same semantics as 863// strncmp == 0 when both are known-length. Returns 1 if equal. 864func ty_name_eq(a: *u8, a_len: i64, b: *u8, b_len: i64) -> i64 { 865 if a_len != b_len { return 0 } 866 var i: i64 = 0 867 while i < a_len { 868 if a[i] != b[i] { return 0 } 869 i = i + 1 870 } 871 return 1 872} 873 874// Walk a Type tree, substituting any TY_PARAM whose name matches 875// `param_name[0..param_name_len]` with `concrete`. Recurses into 876// TY_PTR. Returns the same Type pointer if no substitution 877// occurred (so caller can cheaply detect the no-op case). 878// 879// Nested TY_STRUCT templates (e.g. a field of type Option<U> inside 880// a Result<T, U>) are NOT re-walked here -- that requires ambient 881// substitution tracking which adds complexity we don't need yet. 882// The parse.c port has the same shortcut (see its comment). 883func clone_type_substituting(t: *Type, param_name: *u8, 884 param_name_len: i64, 885 concrete: *Type) -> *Type { 886 if t == (0 as *Type) { return t } 887 if t.kind == TY_PARAM { 888 if t.param_name != (0 as *u8) { 889 if ty_name_eq(t.param_name, t.param_name_len, 890 param_name, param_name_len) == 1 { 891 return concrete 892 } 893 } 894 } 895 if t.kind == TY_PTR { 896 if t.pointee != (0 as *Type) { 897 let sub: *Type = clone_type_substituting(t.pointee, 898 param_name, 899 param_name_len, 900 concrete) 901 if sub == t.pointee { return t } 902 let np: *Type = alloc_type(TY_PTR, 8, 8) 903 np.pointee = sub 904 return np 905 } 906 } 907 return t 908} 909 910// Helpers for name mangling. Build a mangled name "Base$a1$a2..." 911// into out[] and return the length written. Each arg contributes 912// "$<kind-suffix>". 913func ty_mangle_suffix(arg: *Type, out: *u8, off: i64, cap: i64) -> i64 { 914 if off >= cap { return off } 915 out[off] = 0x24 // '$' 916 var cur: i64 = off + 1 917 if arg == (0 as *Type) { 918 if cur < cap { out[cur] = 0x3F; cur = cur + 1 } // '?' 919 return cur 920 } 921 if arg.kind == TY_I64 { 922 if cur + 3 <= cap { 923 out[cur] = 0x69; out[cur+1] = 0x36; out[cur+2] = 0x34 // "i64" 924 cur = cur + 3 925 } 926 return cur 927 } 928 if arg.kind == TY_I32 { 929 if cur + 3 <= cap { 930 out[cur] = 0x69; out[cur+1] = 0x33; out[cur+2] = 0x32 931 cur = cur + 3 932 } 933 return cur 934 } 935 if arg.kind == TY_I8 { 936 if cur + 2 <= cap { 937 out[cur] = 0x69; out[cur+1] = 0x38 938 cur = cur + 2 939 } 940 return cur 941 } 942 if arg.kind == TY_BOOL { 943 if cur + 4 <= cap { 944 out[cur] = 0x62; out[cur+1] = 0x6F; out[cur+2] = 0x6F; out[cur+3] = 0x6C 945 cur = cur + 4 946 } 947 return cur 948 } 949 if arg.kind == TY_PTR { 950 if cur < cap { out[cur] = 0x70; cur = cur + 1 } // 'p' 951 return ty_mangle_suffix(arg.pointee, out, cur, cap) 952 } 953 if arg.kind == TY_STRUCT { 954 // Emit struct name bytes. 955 var i: i64 = 0 956 while i < arg.name_len { 957 if cur >= cap { break } 958 out[cur] = arg.name_bytes[i] 959 cur = cur + 1 960 i = i + 1 961 } 962 return cur 963 } 964 if arg.kind == TY_PARAM { 965 // Mangled name for an un-instantiated param: "T" etc. 966 var i: i64 = 0 967 while i < arg.param_name_len { 968 if cur >= cap { break } 969 out[cur] = arg.param_name[i] 970 cur = cur + 1 971 i = i + 1 972 } 973 return cur 974 } 975 // Fallback: '?' 976 if cur < cap { out[cur] = 0x3F; cur = cur + 1 } 977 return cur 978} 979 980// Forward decls used by instantiate_generic_n below. Real impls 981// live further down with the existing struct-registration code. 982func lookup_struct(P: *Parser, name: *u8) -> *Type; 983func register_struct(P: *Parser, name: *u8, name_len: i64, 984 ty: *Type) -> i64; 985// Resolve an enum NAME used in a TYPE position. Declared here, beside 986// lookup_struct, because parse_type sits above the enum table code. 987func lookup_enum_type(P: *Parser, name: *u8) -> *Type; 988 989// Instantiate a template struct with `n_args` concrete type args. 990// Returns the cached monomorphic Type, or a freshly-built one on 991// first use. If `tmpl` has no declared type params, returns tmpl 992// unchanged (caller paid attention to the generic syntax but there 993// was nothing to substitute). 994// 995// The cache is Parser.structs itself -- we stash every monomorph as 996// a regular struct entry keyed by the mangled name. Repeated uses 997// of `Result<i64, i64>` share one Type. 998func instantiate_generic_n(P: *Parser, tmpl: *Type, 999 args: *i64, n_args: i64) -> *Type { 1000 if tmpl == (0 as *Type) { return tmpl } 1001 if tmpl.n_type_params == 0 { return tmpl } 1002 1003 // Mangle: tmpl.name + "$arg1$arg2..." 1004 let mangled: *u8 = sys_mmap(256) 1005 var pos: i64 = 0 1006 var i: i64 = 0 1007 while i < tmpl.name_len { 1008 if pos >= 256 { break } 1009 mangled[pos] = tmpl.name_bytes[i] 1010 pos = pos + 1 1011 i = i + 1 1012 } 1013 var j: i64 = 0 1014 while j < n_args { 1015 let arg_ptr: i64 = args[j] 1016 let arg: *Type = arg_ptr as *Type 1017 pos = ty_mangle_suffix(arg, mangled, pos, 256) 1018 j = j + 1 1019 } 1020 1021 // Cache lookup by mangled name. 1022 let cached: *Type = lookup_struct(P, mangled) 1023 if cached != (0 as *Type) { return cached } 1024 1025 // Miss: clone the template, substituting each field's type. 1026 let inst: *Type = ir_type_struct_new(mangled, pos) 1027 // Register in parser cache so future use-sites hit. 1028 register_struct(P, mangled, pos, inst) 1029 1030 var fi: i64 = 0 1031 while fi < tmpl.n_fields { 1032 let fbase: i64 = tmpl.fields as i64 1033 let src: *StructField = (fbase + fi * 32) as *StructField 1034 var fty: *Type = src.ty 1035 var k: i64 = 0 1036 while k < n_args { 1037 if k >= tmpl.n_type_params { break } 1038 let param_name_ptr: i64 = tmpl.type_params[k] 1039 let param_name: *u8 = param_name_ptr as *u8 1040 // Param name length: we stored each name as a NUL- 1041 // terminated span, so scan to NUL for length. 1042 var pn_len: i64 = 0 1043 while param_name[pn_len] != 0 { pn_len = pn_len + 1 } 1044 let concrete_ptr: i64 = args[k] 1045 let concrete: *Type = concrete_ptr as *Type 1046 fty = clone_type_substituting(fty, param_name, pn_len, concrete) 1047 k = k + 1 1048 } 1049 ir_type_struct_add_field(inst, src.name_bytes, src.name_len, fty) 1050 fi = fi + 1 1051 } 1052 return inst 1053} 1054 1055// ---- type parsing (minimal: just i64/bool/void + *T) ---- 1056 1057func parse_type(P: *Parser) -> *Type { 1058 // [N]T: fixed-size stack array. N = integer literal, T = element type. 1059 // size = N * elem.size; pointee = element type (reuses the PTR/ARRAY pointee slot). 1060 if match_kind(P, TK_LBRACKET) { 1061 // []T: SLICE -- a pointer that carries its length. Distinguished from [N]T by the 1062 // absence of a size token, so the two forms cannot be confused by the parser. 1063 if peek_kind(P) == TK_RBRACKET { 1064 advance_tok(P) 1065 let selem: *Type = parse_type(P) 1066 let st: *Type = alloc_type(TY_SLICE, 8, 8) 1067 st.pointee = selem 1068 return st 1069 } 1070 let n_tok: *Tok = advance_tok(P) 1071 let n: i64 = n_tok.int_val 1072 match_kind(P, TK_RBRACKET) 1073 let elem: *Type = parse_type(P) 1074 let at: *Type = alloc_type(TY_ARRAY, n * elem.size, elem.align) 1075 at.pointee = elem 1076 return at 1077 } 1078 // func(T,...)->R: function-pointer type = 8 bytes (a code address). Params are consumed but NOT 1079 // stored (MVP: no arity/param-type check); the return type is kept in `pointee`. 1080 if match_kind(P, TK_FUNC) { 1081 match_kind(P, TK_LPAREN) 1082 if peek_kind(P) != TK_RPAREN { 1083 parse_type(P) 1084 while match_kind(P, TK_COMMA) { parse_type(P) } 1085 } 1086 match_kind(P, TK_RPAREN) 1087 var fret: *Type = ir_type_void() 1088 if match_kind(P, TK_ARROW) { fret = parse_type(P) } 1089 let ft: *Type = alloc_type(TY_FUNC, 8, 8) 1090 ft.pointee = fret 1091 return ft 1092 } 1093 // *T: recurse into pointee, wrap in TY_PTR. 1094 if match_kind(P, TK_STAR) { 1095 let pointee: *Type = parse_type(P) 1096 let t: *Type = alloc_type(TY_PTR, 8, 8) 1097 t.pointee = pointee 1098 return t 1099 } 1100 // Ident: primitive? active type param? user struct? 1101 let tk: *Tok = advance_tok(P) 1102 let name: *u8 = tok_text_ptr(tk) 1103 if streq_n(name, "i64", 3) { return alloc_type(TY_I64, 8, 8) } 1104 if streq_n(name, "i32", 3) { return alloc_type_s(TY_I32, 4, 4) } // signed subword -> sign-extend on load 1105 if streq_n(name, "u32", 3) { return alloc_type(TY_I32, 4, 4) } // unsigned 32 -> zero-extend (default) 1106 if streq_n(name, "i16", 3) { return alloc_type_s(TY_I16, 2, 2) } // signed 16 -> sign-extend on load 1107 if streq_n(name, "u16", 3) { return alloc_type(TY_I16, 2, 2) } // unsigned 16 -> zero-extend 1108 if streq_n(name, "i8", 2) { return alloc_type_s(TY_I8, 1, 1) } // signed 8 -> sign-extend on load 1109 if streq_n(name, "u8", 2) { return alloc_type(TY_I8, 1, 1) } 1110 if streq_n(name, "bool", 4) { return alloc_type(TY_BOOL, 1, 1) } 1111 if streq_n(name, "void", 4) { return alloc_type(TY_VOID, 0, 1) } 1112 // F-extension primitive types. Matches TY_F32/TY_F64 in types.nx. 1113 // ABI: f32 is 4 bytes 4-aligned, f64 is 8 bytes 8-aligned. 1114 if streq_n(name, "f32", 3) { return alloc_type(TY_F32, 4, 4) } 1115 if streq_n(name, "f64", 3) { return alloc_type(TY_F64, 8, 8) } 1116 1117 // Active generic type parameter? Inside a `struct X<T> { ... }` 1118 // body, `T` should become a TY_PARAM that later instantiation 1119 // walks substitute against concrete args. Scan the parser's 1120 // active-param stack -- if any entry's NUL-terminated name 1121 // matches the identifier we're looking at, emit a fresh TY_PARAM. 1122 var pi: i64 = 0 1123 var name_len: i64 = 0 1124 while name[name_len] != 0 { name_len = name_len + 1 } 1125 while pi < P.n_active_params { 1126 let ap_ptr: i64 = P.active_params[pi] 1127 let ap: *u8 = ap_ptr as *u8 1128 var ap_len: i64 = 0 1129 while ap[ap_len] != 0 { ap_len = ap_len + 1 } 1130 if ty_name_eq(name, name_len, ap, ap_len) == 1 { 1131 let pt: *Type = alloc_type(TY_PARAM, 8, 8) 1132 pt.param_name = ap 1133 pt.param_name_len = ap_len 1134 return pt 1135 } 1136 pi = pi + 1 1137 } 1138 1139 // User struct -- linear scan over declared struct types. 1140 let found: *Type = lookup_struct(P, name) 1141 if found != (0 as *Type) { 1142 // Generic instantiation syntax: `Name<T, ...>`. If the 1143 // target struct has declared type params and we see a '<', 1144 // parse the args and instantiate via instantiate_generic_n. 1145 // If it has no type params, fall back to the old 1146 // consume-and-return-base behaviour (still needed so legacy 1147 // non-generic code parses `Option<T>` gracefully). 1148 if match_kind(P, TK_LT) { 1149 if found.n_type_params > 0 { 1150 let args_raw: *u8 = sys_mmap(NX_GENERIC_MAX_ARGS * 8 + 16) 1151 let args: *i64 = args_raw as *i64 1152 var n_args: i64 = 0 1153 let a0: *Type = parse_type(P) 1154 args[n_args] = a0 as i64 1155 n_args = n_args + 1 1156 while match_kind(P, TK_COMMA) { 1157 if n_args >= NX_GENERIC_MAX_ARGS { parse_die("generic instantiation exceeds the type-argument cap" as *u8, 51) } 1158 let an: *Type = parse_type(P) 1159 args[n_args] = an as i64 1160 n_args = n_args + 1 1161 } 1162 match_kind(P, TK_GT) 1163 return instantiate_generic_n(P, found, args, n_args) 1164 } else { 1165 parse_type(P) 1166 while match_kind(P, TK_COMMA) { 1167 parse_type(P) 1168 } 1169 match_kind(P, TK_GT) 1170 } 1171 } 1172 return found 1173 } 1174 // User enum -- a name declared by `enum NAME { ... }` used in a TYPE 1175 // position. A TAGGED enum (some variant carries a payload) publishes a 1176 // shadow struct under its own name, so lookup_struct above already 1177 // resolved it; only a PLAIN enum falls through to here, and before this 1178 // existed it died as "I do not know a type named X". That made every 1179 // payload-free enum a value namespace that could not be NAMED in a 1180 // signature: `enum NetError { ... }` parsed, `NetError::ConnRefused` 1181 // evaluated, and `-> *Result<i64, NetError>` did not compile. Measured 1182 // 2026-09-04 on runtime/nx_net.nx (8 errors) and runtime/fs.nx (10). 1183 let ety: *Type = lookup_enum_type(P, name) 1184 if ety != (0 as *Type) { 1185 // An enum name takes no type arguments. Consume a stray `<...>` 1186 // the same way the no-type-params struct path does, so a malformed 1187 // use yields one clear error instead of cascading through the 1188 // caller's parse. 1189 if match_kind(P, TK_LT) { 1190 parse_type(P) 1191 while match_kind(P, TK_COMMA) { 1192 parse_type(P) 1193 } 1194 match_kind(P, TK_GT) 1195 } 1196 return ety 1197 } 1198 // Unknown type name. PREVENT pillar (T#nx-int-alias-size-0): this 1199 // used to silently return `TY_VOID size 0`, which is CATASTROPHIC for 1200 // a struct field type -- a size-0 field collapses every later field's 1201 // offset, so a pointer field can land at offset 0 and be overwritten 1202 // by an int field, producing a deref-of-a-small-integer SEGFAULT far 1203 // from the cause. An unknown type at this point is ALWAYS a real 1204 // error (missing primitive, unregistered struct, or -- the bug we 1205 // fixed -- an unresolved `type` alias). Fail LOUD + name it, so the 1206 // entire silent-size-0 class is impossible going forward. 1207 var en: i64 = 0 1208 while name[en] != 0 { en = en + 1 } 1209 nx_dym_suggest_type(P, name, en) 1210 // Line anchor: parse_type consumed the type-name token just before resolving it, so it 1211 // sits one slot back (same anchoring as calls and identifiers; verified by witness bite). 1212 var uty_ln: i64 = 0 1213 if P.pos > 0 { 1214 let uty_tok: *Tok = tok_at(P.toks, P.pos - 1) 1215 uty_ln = uty_tok.line 1216 } 1217 nx_diag_at(uty_ln) 1218 nx_diag_puts(": I do not know a type named '" as *u8) 1219 sys_write(2, name, en) 1220 nx_diag_puts("' -- it is not a built-in type, and no struct or alias declares it.\n" as *u8) 1221 nx_diag_puts(" why the build stopped: a type that does not exist has no size or shape, so nothing can be laid out in memory for it.\n" as *u8) 1222 nx_dym_note2(P, name, en) 1223 nx_diag_note_error() 1224 // RECOVERY PLACEHOLDER: i64-shaped (size 8, align 8) so struct-offset math 1225 // in the parser stays sane -- NEVER the silent size-0 collapse this check 1226 // exists to prevent. The end-of-parse gate keeps it out of codegen. 1227 return alloc_type(TY_I64, 8, 8) 1228} 1229 1230// ---- local lookup (ident -> Value id) ---- 1231 1232func parser_loc_at(P: *Parser, i: i64) -> *Local { 1233 let base: i64 = P.locals as i64 1234 return (base + i * LOCAL_BYTES) as *Local 1235} 1236 1237// Copy name bytes from a *u8 buffer into a Local. 1238func copy_name_into_local(L: *Local, src: *u8) -> i64 { 1239 // Write first 64 bytes of name into Local's slot. 1240 let base: i64 = L as i64 1241 let dst: *u8 = base as *u8 1242 var i: i64 = 0 1243 while i < 64 { 1244 dst[i] = src[i] 1245 if src[i] == 0 { return 0 } 1246 i = i + 1 1247 } 1248 return 0 1249} 1250 1251// Compare Local's name against a *u8. 1252func local_name_eq(L: *Local, name: *u8) -> i64 { 1253 let base: i64 = L as i64 1254 let ln: *u8 = base as *u8 1255 var i: i64 = 0 1256 while i < 64 { 1257 if ln[i] != name[i] { return 0 } 1258 if ln[i] == 0 { return 1 } 1259 i = i + 1 1260 } 1261 return 1 1262} 1263 1264// DID-YOU-MEAN for bare IDENTIFIERS (2026-08-05, climb v9 rung 2). Same contract as 1265// nx_dym_suggest_fn -- error-path only, silent when nothing is near (a wrong suggestion is 1266// worse than none), bounded Levenshtein, ZERO new indexes: every pool scanned here (the live 1267// locals window, module consts incl Enum::Variant rows, the function table) already exists. 1268// LIVES BELOW the Local/MConst struct decls: a `let x: *T` annotation resolves its type name 1269// EAGERLY (unlike a param type), so placing this next to its nx_dym_ siblings above the 1270// structs was refused with two unknown-type errors -- reported in ONE build by the 1271// multi-error recovery this same climb shipped an hour earlier. 1272func nx_dym_suggest_ident(P: *Parser, name: *u8, nlen: i64) -> i64 { 1273 var budget: i64 = 2 1274 if nlen <= 4 { budget = 1 } 1275 var best_d: i64 = 999 1276 var best_p: *u8 = 0 as *u8 1277 var best_l: i64 = 0 1278 var best_k: i64 = 0 1279 var best_aux: i64 = 0 1280 var i: i64 = 0 1281 while i < P.n_locals { 1282 let L: *Local = parser_loc_at(P, i) 1283 let lp: *u8 = (L as i64) as *u8 1284 let ll: i64 = nx_dym_len(lp) 1285 if ll > 0 { 1286 let d1: i64 = nx_dym_dist(name, nlen, lp, ll, budget) 1287 if d1 < best_d { best_d = d1; best_p = lp; best_l = ll; best_k = 3; best_aux = L.ty_kind } 1288 } 1289 i = i + 1 1290 } 1291 var mi: i64 = 0 1292 while mi < P.n_mconsts { 1293 let mc: *MConst = mconst_at(P, mi) 1294 let mp: *u8 = (mc as i64) as *u8 1295 let ml: i64 = nx_dym_len(mp) 1296 if ml > 0 { 1297 let d2: i64 = nx_dym_dist(name, nlen, mp, ml, budget) 1298 if d2 < best_d { best_d = d2; best_p = mp; best_l = ml; best_k = 2; best_aux = mc.val } 1299 } 1300 mi = mi + 1 1301 } 1302 if P.module != (0 as *Module) { 1303 var fi: i64 = 0 1304 while fi < P.module.n_functions { 1305 let fbase: i64 = P.module.functions as i64 1306 let f: *Function = (fbase + fi * 176) as *Function 1307 let fp: *u8 = f.name_start as *u8 1308 let fl: i64 = f.name_len 1309 if fl > 0 { 1310 let d3: i64 = nx_dym_dist(name, nlen, fp, fl, budget) 1311 if d3 < best_d { best_d = d3; best_p = fp; best_l = fl; best_k = 1; best_aux = f.n_params } 1312 } 1313 fi = fi + 1 1314 } 1315 } 1316 if best_d > budget { return 0 } 1317 if best_d == 0 { return 0 } 1318 if (best_p as i64) == 0 { return 0 } 1319 nx_dym_last_kind = best_k 1320 nx_dym_last_p = best_p 1321 nx_dym_last_l = best_l 1322 nx_dym_last_aux = best_aux 1323 return 1 1324} 1325 1326func find_local(P: *Parser, name: *u8) -> *Local { 1327 var i: i64 = P.n_locals - 1 1328 while i >= 0 { 1329 let L: *Local = parser_loc_at(P, i) 1330 if local_name_eq(L, name) { return L } 1331 i = i - 1 1332 } 1333 return 0 as *Local 1334} 1335 1336// Is 1337ame already bound in the CURRENT block? find_local scans the whole live window (every 1338// enclosing scope), which is the right answer for RESOLUTION and the wrong one for REDECLARATION. 1339func local_in_block(P: *Parser, name: *u8) -> i64 { 1340 var i: i64 = P.n_locals - 1 1341 while i >= P.block_base { 1342 let L: *Local = parser_loc_at(P, i) 1343 if local_name_eq(L, name) { return 1 } 1344 i = i - 1 1345 } 1346 return 0 1347} 1348 1349func add_local(P: *Parser, name: *u8, value_id: i64, is_alloca: i64, 1350 ty_kind: i64, ty: *Type) -> i64 { 1351 nx_assert_lt(P.n_locals, NX_PARSE_LOCALS_CAP, 1352 "add_local: locals pool exhausted (raise NX_PARSE_LOCALS_CAP)" as *u8) 1353 let L: *Local = parser_loc_at(P, P.n_locals) 1354 copy_name_into_local(L, name) 1355 L.value_id = value_id 1356 L.is_alloca = is_alloca 1357 L.ty_kind = ty_kind 1358 L.ty = ty 1359 // LN3: the locals pool is RESET per function (n_locals=0) and slots are REUSED, so a stale 1360 // ext_bytes from the previous function's local at this index would be read as provenance. 1361 // Zero it here; parse_stmt_let sets the real extent on the just-added local when it binds one. 1362 L.ext_bytes = 0 1363 // LN2: same reuse hazard for the non-null bit -- a stale 1 here would ACQUIT an unchecked deref. 1364 L.nn_checked = 0 1365 // LN4/LN5: same reuse hazard again, and it fails the OTHER way -- a stale OWN_MOVED/OWN_RELEASED 1366 // here would REFUSE a brand-new local in the next function that never owned anything. That is 1367 // the loud direction rather than the silent one, but it is still a false positive, and a mode 1368 // that refuses innocent code is the one everybody turns off. These two lines ARE the whole 1369 // per-function reset for this rung: the state lives on Locals, and the locals pool is reset by 1370 // n_locals = 0, so unlike LN2/LN3 there is no global handshake needing an oe_reset_fn sibling. 1371 L.own_state = 0 1372 L.own_line = 0 1373 P.n_locals = P.n_locals + 1 1374 return 0 1375} 1376 1377// ---- IR emitter stubs (real impls live in ir.nx) ---- 1378// 1379// We declare them so the parser can call out; when multi-file 1380// linkage concatenates ir.nx + parse.nx they resolve to the real 1381// definitions. Listed here as forward references only; NishiLang 1382// has no separate `extern` block so we leave them unimplemented 1383// locally and rely on the concatenated build to supply them. 1384 1385// Forward uses rely on multi-file build + link-time name resolution. 1386// Since NishiLang doesn't have separate compilation yet, the actual 1387// calls happen when this file is built alongside ir.nx. 1388// 1389// For the parser to be useful it MUST be compiled with ir.nx, opt.nx, 1390// regalloc.nx, riscv.nx, lex.nx -- the full self-host bundle. The 1391// self-test in this file only exercises local logic (precedence, 1392// token consumption); runtime IR emission happens only under the 1393// bundled compile. 1394// 1395// Stub implementations below let parse.nx compile standalone for 1396// syntax verification. 1397 1398// All IR builders now live in ir.nx; no local stubs remain. 1399 1400// ---- expression parser (precedence climbing) ---- 1401// 1402// Mirrors parse.c's chain. Returns the Value id for the parsed 1403// expression. Each helper consumes its level and returns. 1404 1405// Forward declarations for the mutually-recursive parse chain. 1406func parse_logical_or(P: *Parser) -> i64; 1407func parse_unary_core(P: *Parser) -> i64; 1408func parse_primary(P: *Parser) -> i64; 1409func parse_primary_ident(P: *Parser, t: *Tok) -> i64; 1410func parse_primary_qualified_variant(P: *Parser, name: *u8) -> i64; 1411func parse_primary_intrinsic(P: *Parser, name: *u8) -> i64; 1412func parse_primary_call(P: *Parser, name: *u8) -> i64; 1413func parse_primary_local_or_const(P: *Parser, name: *u8) -> i64; 1414func parse_stmt_list(P: *Parser) -> i64; 1415func parse_stmt(P: *Parser) -> i64; 1416func parse_stmt_return(P: *Parser) -> i64; 1417func parse_stmt_let(P: *Parser) -> i64; 1418func parse_stmt_var(P: *Parser) -> i64; 1419func parse_stmt_if(P: *Parser) -> i64; 1420func parse_stmt_while(P: *Parser) -> i64; 1421func parse_stmt_for(P: *Parser) -> i64; 1422func parse_stmt_break(P: *Parser) -> i64; 1423func parse_stmt_continue(P: *Parser) -> i64; 1424func parse_stmt_match(P: *Parser) -> i64; 1425func parse_stmt_star(P: *Parser) -> i64; 1426func parse_stmt_ident(P: *Parser) -> i64; 1427func parse_stmt_ident_assign(P: *Parser) -> i64; 1428func parse_stmt_ident_subscript(P: *Parser) -> i64; 1429func parse_stmt_ident_dot(P: *Parser) -> i64; 1430func parse_stmt_expr_fallback(P: *Parser) -> i64; 1431func parse_module_const(P: *Parser) -> i64; 1432func lookup_mconst(P: *Parser, name: *u8, out: *i64) -> i64; 1433func lookup_mconst_f64(P: *Parser, name: *u8, out: *i64) -> i64; 1434func const_eval_f64_addsub(P: *Parser, depth: i64) -> i64; 1435func const_eval_f64_unary(P: *Parser, depth: i64) -> i64; 1436func lookup_mconst_string(P: *Parser, name: *u8, 1437 out_gid: *i64, out_ty: **Type) -> i64; 1438func mconst_name_eq(mc: *MConst, name: *u8) -> i64; 1439func mconst_at(P: *Parser, i: i64) -> *MConst; 1440// V-LANGEXT M4: const-expression evaluator helpers (mutually recursive 1441// precedence-climbing chain). 1442func const_eval_expr(P: *Parser, depth: i64) -> i64; 1443func const_eval_bor(P: *Parser, depth: i64) -> i64; 1444func const_eval_bxor(P: *Parser, depth: i64) -> i64; 1445func const_eval_band(P: *Parser, depth: i64) -> i64; 1446func const_eval_shift(P: *Parser, depth: i64) -> i64; 1447func const_eval_addsub(P: *Parser, depth: i64) -> i64; 1448func const_eval_muldiv(P: *Parser, depth: i64) -> i64; 1449func const_eval_unary(P: *Parser, depth: i64) -> i64; 1450func const_eval_primary(P: *Parser, depth: i64) -> i64; 1451func mconst_at(P: *Parser, i: i64) -> *MConst; 1452func copy_name_into_mconst(mc: *MConst, src: *u8) -> i64; 1453func parse_struct_decl(P: *Parser) -> i64; 1454func parse_static_decl(P: *Parser) -> i64; 1455func parse_module_enum(P: *Parser) -> i64; 1456func inject_statics(P: *Parser) -> i64; 1457func enum_entry_at(P: *Parser, i: i64) -> *EnumEntry; 1458func lookup_enum(P: *Parser, name: *u8) -> *EnumEntry; 1459// (lookup_struct forward-decl lives above parse_type since parse_type uses it.) 1460func struct_entry_at(P: *Parser, i: i64) -> *StructEntry; 1461 1462func parse_expr(P: *Parser) -> i64 { 1463 nx_assert_ptr(P.current_bb as *u8, "parse_expr: bb" as *u8) 1464 let cond: i64 = parse_logical_or(P) 1465 if peek_kind(P) != TK_QUESTION { return cond } 1466 // C-style ternary `cond ? a : b`. Desugars to the SAME control-flow 1467 // the `if..then..else` expression uses (alloca slot + br_cond + per- 1468 // branch store + merge load), so only the chosen branch evaluates. 1469 advance_tok(P) // eat '?' 1470 let result_ty: *Type = ir_type_i64() 1471 let result_addr: i64 = ir_emit_alloca(P.current_bb, result_ty) 1472 // ARITY FIX 2026-07-20: these passed a block-label string to ir_block_new, which takes ONE 1473 // argument (nx_ir.nx:269) and whose BasicBlock has no name field -- the label was SILENTLY 1474 // DISCARDED for as long as this code has existed. Found the moment the new call-arity check 1475 // went in; the labels are kept here as comments so the intent survives the fix. 1476 let then_bb: *BasicBlock = ir_block_new(P.current_fn) // "ternary_then" 1477 let else_bb: *BasicBlock = ir_block_new(P.current_fn) // "ternary_else" 1478 let merge_bb: *BasicBlock = ir_block_new(P.current_fn) // "ternary_merge" 1479 ir_emit_br_cond(P.current_bb, cond, then_bb, else_bb) 1480 P.current_bb = then_bb 1481 let then_v: i64 = parse_expr(P) 1482 ir_emit_store(P.current_bb, result_addr, then_v, result_ty) 1483 ir_emit_br(P.current_bb, merge_bb) 1484 if peek_kind(P) != TK_COLON { 1485 parse_die("ternary needs colon" as *u8, 19) 1486 } 1487 advance_tok(P) // eat ':' 1488 P.current_bb = else_bb 1489 let else_v: i64 = parse_expr(P) 1490 ir_emit_store(P.current_bb, result_addr, else_v, result_ty) 1491 ir_emit_br(P.current_bb, merge_bb) 1492 P.current_bb = merge_bb 1493 return ir_emit_load(P.current_bb, result_addr, result_ty) 1494} 1495 1496// Line-tagged wrapper for every ir_const_i64 call in parse.nx. 1497// When the assertion fires, the tag identifies the exact callsite. 1498// Required because vanilla ir_const_i64's assert message can't tell 1499// us which of ~25 callers passed a bad P.current_fn. Purely 1500// diagnostic; when the root-cause of the sys_read_file stage-2 bug 1501// is understood and fixed, the wrapper can retire. 1502func safe_const_i64(P: *Parser, val: i64, tag: *u8) -> i64 { 1503 nx_assert_ptr(P.current_fn as *u8, tag) 1504 nx_assert(P.current_fn.values_cap > 0, tag) 1505 return ir_const_i64(P.current_fn, val) 1506} 1507 1508// ===================== SPATIAL SAFETY: runtime bounds checking ===================== 1509// These consts are read ONLY by the two functions immediately below them. They sit HERE, not 1510// at the use sites, because a NishiLang function defined textually ABOVE a const it reads 1511// silently resolves that const to garbage (reference-nishilang-nx-cc-gotchas, the fwd-const class). 1512// 1513// NX_BOUNDS_CHECK_LIVE is the kill switch. Set to 0 and every injection below vanishes, which 1514// is how the A/B overhead measurement is taken -- same source, same compiler, one const flipped. 1515const NX_BOUNDS_CHECK_LIVE: i64 = 1 1516// Exit code the injected trap raises. Distinct from the parser's own die code (2) and from the 1517// 128+signal range, so a bounds abort is unambiguous in a gate's exit status. 1518const NX_TRAP_BOUNDS: i64 = 71 1519// RV64 asm-generic syscall numbers. The x86_64 backend translates const-numbered syscalls 1520// (x86ctx_rv64_to_x86_64_syscall), so emitting RV64 form here keeps the injected IR 1521// target-agnostic -- the same instructions are correct on both backends. 1522const NX_RV64_SYS_WRITE: i64 = 64 1523const NX_RV64_SYS_EXIT_GROUP: i64 = 94 1524 1525// One-per-module cache for the trap message global. ir_add_global_string does NOT intern: 1526// every call burns a slot in a FIXED-capacity pool that asserts "globals pool exhausted" when 1527// full. Emitting the message per check site cost 8 copies of a 220-byte string in a 60-line 1528// probe, so a module with heavy fixed-array use could have exhausted the pool -- a regression 1529// the check itself would have caused. Storing gid+1 lets 0 mean "unset" without colliding 1530// with the legitimate global id 0. The module pointer is cached alongside so the id is 1531// invalidated rather than reused if a single process ever compiles more than one Module. 1532static g_bchk_msg_gid1: i64 1533static g_bchk_msg_mod: i64 1534 1535// Emit the abort sequence into P.current_bb: a diagnostic on stderr, then exit_group. 1536// Kept separate from emit_bounds_check so each function stays small and single-purpose. 1537// Does NOT emit a terminator -- the caller owns block termination. 1538func emit_bounds_trap(P: *Parser) -> i64 { 1539 // One trap serves both [N]T and []T, so the wording must not name only one of them -- it said 1540 // "fixed array [N]T" and was therefore actively wrong on the slice path, which is the path 1541 // that covers heap memory and so the one most violations will come through. 1542 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 1543 var mlen: i64 = 0 1544 while msg[mlen] != 0 { mlen = mlen + 1 } 1545 let modv: i64 = P.module as i64 1546 if g_bchk_msg_mod != modv { g_bchk_msg_gid1 = 0 } 1547 var gid: i64 = g_bchk_msg_gid1 - 1 1548 if g_bchk_msg_gid1 == 0 { 1549 gid = ir_add_global_string(P.module, msg, mlen) 1550 g_bchk_msg_gid1 = gid + 1 1551 g_bchk_msg_mod = modv 1552 } 1553 let u8ty: *Type = alloc_type(TY_I8, 1, 1) 1554 let pt: *Type = alloc_type(TY_PTR, 8, 8) 1555 pt.pointee = u8ty 1556 let msg_v: i64 = ir_global_value(P.current_fn, gid, pt) 1557 return emit_trap_syscalls(P, msg_v, mlen, NX_TRAP_BOUNDS) 1558} 1559 1560// THE ONE TRAP EMITTER: write(2, msg, mlen) then exit_group(code). Shared by the bounds trap 1561// (NX_TRAP_BOUNDS) and the LN1 overflow trap (NX_TRAP_OVERFLOW) so the abort sequence exists 1562// once. Value-creation ORDER is load-bearing for byte-identical output: the caller creates the 1563// message global value first, then this emits 64, 2, mlen, then 94, code -- exactly the order 1564// emit_bounds_trap emitted inline before the extraction (2026-08-23), so every default build 1565// keeps its value-id numbering and its bytes. 1566// exit_group, not exit -- whole thread group, so a violation in a worker cannot be survived by 1567// the rest of the program (sys_exit alone would only retire the calling task). 1568func emit_trap_syscalls(P: *Parser, msg_v: i64, mlen: i64, code: i64) -> i64 { 1569 let raw: *u8 = sys_mmap(8 * 8 + 16) 1570 let sargs: *i64 = raw as *i64 1571 // write(2, msg, mlen) 1572 sargs[0] = safe_const_i64(P, NX_RV64_SYS_WRITE, "parse.nx:btrap-wnum" as *u8) 1573 sargs[1] = safe_const_i64(P, 2, "parse.nx:btrap-fd" as *u8) 1574 sargs[2] = msg_v 1575 sargs[3] = safe_const_i64(P, mlen, "parse.nx:btrap-len" as *u8) 1576 ir_emit_syscall(P.current_bb, sargs, 4) 1577 sargs[0] = safe_const_i64(P, NX_RV64_SYS_EXIT_GROUP, "parse.nx:btrap-enum" as *u8) 1578 sargs[1] = safe_const_i64(P, code, "parse.nx:btrap-code" as *u8) 1579 ir_emit_syscall(P.current_bb, sargs, 2) 1580 return 0 1581} 1582 1583// SPATIAL SAFETY (CWE-787/125), the RUNTIME half. A fixed array [N]T indexed by a NON-CONSTANT 1584// expression cannot be decided at compile time, so the check is injected into the program: 1585// 1586// cur: c1 = idx >=s 0 ; br_cond c1 -> lo_ok, fail 1587// lo_ok: c2 = idx <s nelem ; br_cond c2 -> ok, fail 1588// fail: write(2,msg) ; exit_group(71) ; br ok 1589// ok: <caller keeps emitting here> 1590// 1591// The `br ok` in fail is unreachable -- exit_group never returns -- and exists only so every 1592// block ends in a terminator, which cfg_rebuild_edges requires to resolve successors. 1593// 1594// nelem comes from the array's own TYPE, so this costs no annotations and has ZERO false 1595// positives: a rejected index provably could not have been in range. Raw *T indexing is left 1596// untouched, because a bare pointer carries no length -- that is the honest remaining gap and 1597// it needs a slice type, not a check. 1598// 1599// On return P.current_bb is the `ok` block, and the value id `idx` still dominates it. 1600// THE one bounds check. `len_v` is a VALUE id, not a number, so a fixed array (whose length is a 1601// constant folded in by the caller) and a slice (whose length is loaded from the header at run 1602// time) go through exactly this code -- there is no second implementation to drift. 1603func emit_bounds_check_v(P: *Parser, idx: i64, len_v: i64) -> i64 { 1604 if NX_BOUNDS_CHECK_LIVE == 0 { return 0 } 1605 let lo_ok: *BasicBlock = ir_block_new(P.current_fn) 1606 let fail_bb: *BasicBlock = ir_block_new(P.current_fn) 1607 let ok_bb: *BasicBlock = ir_block_new(P.current_fn) 1608 let zero_v: i64 = safe_const_i64(P, 0, "parse.nx:bchk-zero" as *u8) 1609 let c1: i64 = ir_emit_binop(P.current_bb, OP_GE_S, idx, zero_v, ir_type_i64()) 1610 ir_emit_br_cond(P.current_bb, c1, lo_ok, fail_bb) 1611 P.current_bb = lo_ok 1612 let c2: i64 = ir_emit_binop(P.current_bb, OP_LT_S, idx, len_v, ir_type_i64()) 1613 ir_emit_br_cond(P.current_bb, c2, ok_bb, fail_bb) 1614 P.current_bb = fail_bb 1615 emit_bounds_trap(P) 1616 ir_emit_br(P.current_bb, ok_bb) 1617 P.current_bb = ok_bb 1618 return 0 1619} 1620 1621// Fixed-array wrapper: the length is known from the TYPE, so materialise it as a constant and 1622// hand it to the one checker above. 1623func emit_bounds_check(P: *Parser, idx: i64, nelem: i64) -> i64 { 1624 if NX_BOUNDS_CHECK_LIVE == 0 { return 0 } 1625 if nelem <= 0 { return 0 } 1626 let n_v: i64 = safe_const_i64(P, nelem, "parse.nx:bchk-n" as *u8) 1627 return emit_bounds_check_v(P, idx, n_v) 1628} 1629 1630// ===================== LN3: RAW-POINTER PROVENANCE (bck_ptr_provenance) ===================== 1631// The comment on emit_bounds_check_v names the gap this closes: "Raw *T indexing is left 1632// untouched, because a bare pointer carries no length". Where the compiler can SEE the 1633// allocation -- `let p: *T = sys_mmap(<const>)`, optionally through an `as *T` cast chain -- 1634// the extent is recorded on the Local (Local.ext_bytes) and every `p[i]` read/write is checked 1635// through the SAME emit_bounds_check ruler as typed arrays (one checker, no second drift copy). 1636// 1637// DECLARED MODE, OFF BY DEFAULT: injections happen only under the `--ptrprov` compiler flag 1638// (g_ptrprov_live, set by nx_compile_x86 main). Under the default every build is byte-identical 1639// to the pre-LN3 compiler BY CONSTRUCTION -- the lang.plan risk row demands the corpus build 1640// unchanged until the per-class ratchet flips the default after a clean corpus census. 1641// 1642// SOUNDNESS OF THE VALUE-ID KEY: a `let` local is SSA (is_alloca=0; parse_stmt_ident_assign 1643// REFUSES stores to it), so every use of the name IS the initializer's value id and the extent 1644// can never go stale. Aliases inherit for free: `let b = a` binds b to the same value id, so 1645// the same table row covers it. Value ids are per-function and monotonic, so an id can only 1646// match its own binding; g_prov/g_pp state is reset at parse_function entry because a FRESH 1647// function's ids restart and could otherwise collide with a stale id from the previous one. 1648// 1649// DECLARED IMPRECISION (floors, each sound in the do-nothing direction -- a miss skips a check, 1650// never emits a wrong one): runtime-sized sys_mmap(n) is not tracked (needs a dominating value, 1651// not a constant); `var` pointers are not tracked (reassignable); arena/allocator helpers other 1652// than sys_mmap are not yet name-keyed; chained `s.f[i]` and nested `p[q[i]]` bases are not 1653// matched (the handshake carries exactly one base). The trap message shared with arrays/slices 1654// still names only those two forms -- reword it in the same change that flips the ratchet, since 1655// any message edit shows as a diff in every rebuilt organ and would break this rung's 1656// byte-neutrality proof today. 1657static g_ptrprov_live: i64 1658static g_prov_val: i64 // value id of the newest sys_mmap(<const>) result (or its cast chain) 1659static g_prov_bytes: i64 // that allocation's extent in bytes 1660static g_pp_base_val: i64 // handshake: value id of the let-local base parse_primary just resolved 1661static g_pp_base_ext: i64 // its extent in bytes; 0 = none 1662 1663// Setter for the driver (nx_compile_x86 main), mirroring lm_set_debug / nx_cg_arm. 1664func nx_ptrprov_set(v: i64) -> i64 { g_ptrprov_live = v; return 0 } 1665 1666// Per-function reset -- value ids restart per function, so stale ids must not be matchable. 1667func nx_ptrprov_reset_fn() -> i64 { 1668 g_prov_val = 0 - 1 1669 g_pp_base_val = 0 - 1 1670 g_pp_base_ext = 0 1671 return 0 1672} 1673 1674// THE one provenance check, shared by the read path (parse_field_chain) and the write path 1675// (parse_stmt_ident_subscript). Early-return shape on purpose: the 5-deep-nested-if with a 1676// multi-statement innermost block is a documented silent-mismatch class (nx-cc gotchas). 1677// A COMPILE-TIME-CONSTANT index is decided here with no runtime cost (diag voice, parsing 1678// continues, the end-of-parse gate keeps the poisoned build from emitting); a runtime index 1679// injects the standard trap (exit NX_TRAP_BOUNDS). 1680func pp_idx_check(P: *Parser, idx: i64, nelem: i64, ln: i64) -> i64 { 1681 if g_ptrprov_live != 1 { return 0 } 1682 if nelem <= 0 { return 0 } 1683 let iv: *Value = val_at(P.current_fn, idx) 1684 if iv != (0 as *Value) { 1685 if iv.kind == VK_CONST_INT { 1686 if iv.const_int >= 0 { if iv.const_int < nelem { return 0 } } 1687 nx_diag_at(ln) 1688 nx_diag_puts(": this index is outside the allocation this pointer provably refers to (index " as *u8) 1689 nx_put_dec_err(iv.const_int) 1690 nx_diag_puts(", allocation holds " as *u8) 1691 nx_put_dec_err(nelem) 1692 nx_diag_puts(" element(s) -- --ptrprov mode).\n" as *u8) 1693 nx_diag_puts(" why the build stopped: the pointer was bound to a sys_mmap of a known size, so this access provably reads or writes past the end of that allocation -- the silent over-run class (CWE-787/125) that --ptrprov exists to refuse.\n" as *u8) 1694 nx_diag_puts(" fix: clamp the index below the allocation's element count, or size the allocation to cover the access.\n" as *u8) 1695 nx_diag_note_error() 1696 return 0 1697 } 1698 } 1699 emit_bounds_check(P, idx, nelem) 1700 return 0 1701} 1702 1703// The rung's public predicate: is raw-pointer provenance enforcement live in this build of the 1704// compiler? Kept trivially readable so the fixture organ (runtime/nx_boundscheck.nx) and any 1705// census can name the mode without reaching into parser internals. 1706func bck_ptr_provenance_live() -> i64 { return g_ptrprov_live } 1707 1708// ===================== LN1: CHECKED INTEGER ARITHMETIC (chk_add_overflow) ===================== 1709// CWE-190, the class the /compare/lang row "Integer overflow checked" has carried as a gap since 1710// 2026-08-14 (the constant-shift-count rung was its first half). MEASURED before writing this: 1711// `9223372036854775807 + 1` compiles clean and returns -9223372036854775808; the program keeps 1712// running on a silently WRAPPED value -- the worst class this estate tracks (silent wrong value). 1713// 1714// DECLARED MODE, OFF BY DEFAULT, the LN3 shape exactly: checks are injected only under the 1715// `--chkarith` compiler flag (g_chkarith_live, set by nx_compile_x86 main). Under the default 1716// every build is byte-identical by construction (nothing below emits), so the corpus keeps 1717// building unchanged; the per-class ratchet flips the default only after a clean corpus census. 1718// 1719// WHAT IS CHECKED: every `+ - *` whose RESULT TYPE is i64 and whose operands are not pointers 1720// (pointer arithmetic is the LN3/provenance axis, i32 wraps mod 2^32 by declared semantics and 1721// stays out of this rung). Three legs, one injection point (emit_typed_binop -- the ONE typed 1722// binop ruler, so there is no second site to drift): 1723// ADD r = a + b overflows iff ((a ^ r) & (b ^ r)) < 0 (signs agree, result sign differs) 1724// SUB r = a - b overflows iff ((a ^ b) & (a ^ r)) < 0 (signs differ, result sign != a) 1725// MUL r = a * b overflows iff a != 0 and (a == -1 ? b == MIN : r / a != b) 1726// -- the a == -1 case is branched around BEFORE the division because MIN / -1 raises #DE 1727// on x86 (SIGFPE), which would turn the check itself into the crash it exists to prevent. 1728// Division is used rather than a mul-high intrinsic because OP_UMULHI exists on the x86 1729// backend only (measured: absent from nx_riscv.nx and nx_wasm.nx); the IR below is 1730// backend-agnostic, like the bounds check. 1731// Each leg is control flow + the shared trap emitter (emit_trap_syscalls), so no opt pass can 1732// fold the trap away while keeping the overflowing value. 1733// 1734// COMPILE-TIME LEG: a literal `+ - *` of two CONSTANTS that provably overflows is REFUSED at 1735// parse time naming the rule (chk_const_overflow_refuse), the same voice as the constant 1736// shift-count refusal -- a constant overflow in source is never intent. 1737// 1738// WRAP-AROUND BY INTENT: `__wrap_add/__wrap_sub/__wrap_mul(a, b)` emit the plain binop with no 1739// check under either mode (hashes, ring counters, LCGs). Under the default they are `+ - *`. 1740// This is the Zig `+%` / Rust `wrapping_add` contract, so the mode is usable on real code. 1741// 1742// Exit code 72: adjacent to the bounds trap (71) in the trap namespace, distinct from the 1743// parser die code (2), from the 128+signal range and from every gate exit the estate uses 1744// (searched 2026-08-23: no sys_exit(72)/exit_group(72) in the runtime tree). 1745const NX_TRAP_OVERFLOW: i64 = 72 1746 1747static g_chkarith_live: i64 1748func nx_chkarith_set(v: i64) -> i64 { g_chkarith_live = v; return 0 } 1749 1750// ---- LN6 (2026-09-02): DATA-RACE TYPING AT THE ONE PLACE A VALUE CROSSES A THREAD ------------------- 1751// The thread pool takes its per-task context as an opaque i64 (nx_pool_submit(pool, fn, ctx)), and every 1752// caller in the estate hands it `p as i64` where p points at a per-task struct -- measured 2026-09-02: 1753// 40 call sites, ~30 of that exact shape. Nothing says whether that struct may be read on another 1754// thread; the cast erases the type and the pool forgets it. THE MARKER: a struct declares itself safe to 1755// hand across a thread by writing `send` between its name and its body (`struct Job send { ... }`) -- 1756// the author's assertion, the same act as Rust's `unsafe impl Send`. THE CHECK (sync_send_check, the 1757// symbol the lang.matrix watch names): under --sendcheck, an argument to nx_pool_submit that is a cast 1758// FROM a pointer to a struct NOT marked `send` is refused in the teaching voice, naming the struct. 1759// DECLARED IMPRECISION, wrong in the direction of doing nothing: a ctx that is not a cast (an integer, 1760// an address already in i64), or a cast from *u8 / *i64 (an untyped buffer), is ABSTAINED -- the check 1761// can only judge what the type system can see, and it says so here rather than pretending. Default 1762// builds are byte-identical by construction (nothing is emitted under either mode); the per-class 1763// ratchet flips the default only after the 40-site corpus census is marked, the same contract as 1764// --ptrprov, --chkarith, --optenforce, --ownership and --bckelide. 1765static g_sendcheck_live: i64 1766static g_send_names: *u8 // SEND_NAME_W bytes per marked struct name, NUL-terminated 1767static g_send_n: i64 1768static g_last_cast_val: i64 // the value id the most recent `as` chain produced 1769static g_last_cast_src_ty: *Type // the static type of that chain's OPERAND (what the cast erased) 1770const SEND_NAME_W: i64 = 64 1771const SEND_MAX: i64 = 256 1772const SEND_CALLEE_LEN: i64 = 14 // strlen("nx_pool_submit"), the one callee this check keys on 1773func nx_sendcheck_set(v: i64) -> i64 { g_sendcheck_live = v; return 0 } 1774func sync_send_mark(name: *u8, len: i64) -> i64 { 1775 if (g_send_names as i64) == 0 { g_send_names = sys_mmap(SEND_MAX * SEND_NAME_W) } 1776 if g_send_n >= SEND_MAX { return 0 } 1777 let dst: *u8 = ((g_send_names as i64) + g_send_n * SEND_NAME_W) as *u8 1778 var k: i64 = 0 1779 while k < len { if k < SEND_NAME_W - 1 { dst[k] = name[k] } k = k + 1 } 1780 if len < SEND_NAME_W { dst[len] = 0 as u8 } else { dst[SEND_NAME_W - 1] = 0 as u8 } 1781 g_send_n = g_send_n + 1 1782 return 1 1783} 1784func sync_send_is_marked(name: *u8, len: i64) -> i64 { 1785 var i: i64 = 0 1786 while i < g_send_n { 1787 let e: *u8 = ((g_send_names as i64) + i * SEND_NAME_W) as *u8 1788 var k: i64 = 0 1789 var eq: i64 = 1 1790 while k < len { if e[k] != name[k] { eq = 0; k = len } else { k = k + 1 } } 1791 if eq == 1 { if e[len] == (0 as u8) { return 1 } } 1792 i = i + 1 1793 } 1794 return 0 1795} 1796// returns 1 when it refused (the error is already voiced), 0 when it accepted or abstained. 1797func sync_send_check(P: *Parser, name: *u8, nlen: i64, args: *i64, n_args: i64, ln: i64, cl: i64) -> i64 { 1798 if nlen != SEND_CALLEE_LEN { return 0 } 1799 if streq_n(name, "nx_pool_submit" as *u8, SEND_CALLEE_LEN) == 0 { return 0 } 1800 if n_args < 3 { return 0 } 1801 if args[2] != g_last_cast_val { return 0 } // not a cast: abstain (declared above) 1802 let st: *Type = g_last_cast_src_ty 1803 if st == (0 as *Type) { return 0 } 1804 if st.kind != TY_PTR { return 0 } 1805 let pt: *Type = st.pointee 1806 if pt == (0 as *Type) { return 0 } 1807 if pt.kind != TY_STRUCT { return 0 } // *u8 / *i64 buffer: abstain (declared above) 1808 if sync_send_is_marked(pt.name_bytes, pt.name_len) == 1 { return 0 } 1809 nx_diag_at(ln) 1810 nx_diag_puts(": struct '" as *u8) 1811 sys_write(2, pt.name_bytes, pt.name_len) 1812 nx_diag_puts("' is handed to another thread here, and it is not marked send.\n" as *u8) 1813 nx_diag_caret(ln, cl) 1814 nx_diag_puts(" why the build stopped: nx_pool_submit runs this task on a worker thread, so every field of this struct is read (and possibly written) concurrently with the thread that built it -- a data race the compiler cannot see through an i64. Nothing declares that this struct is safe to share.\n" as *u8) 1815 nx_diag_puts(" fix: if every field is either immutable after submit or owned by exactly one thread at a time, mark the declaration `struct " as *u8) 1816 sys_write(2, pt.name_bytes, pt.name_len) 1817 nx_diag_puts(" send { ... }` -- that is your assertion, the compiler records it; otherwise hand the worker a copy, or a struct that is.\n" as *u8) 1818 nx_diag_note_error() 1819 return 1 1820} 1821// The rung's public predicate (the watch-symbol family): is checked arithmetic live in this 1822// build of the compiler? The fixture organ runtime/nx_checked_arith.nx carries the runnable 1823// witness under the watch symbol chk_add_overflow itself. 1824func chk_add_overflow_live() -> i64 { return g_chkarith_live } 1825 1826static g_ovf_msg_gid1: i64 1827static g_ovf_msg_mod: i64 1828 1829// Emit the overflow abort into P.current_bb (diagnostic then exit_group(NX_TRAP_OVERFLOW)). 1830// Message global interned once per module, the emit_bounds_trap discipline -- a fixed-capacity 1831// globals pool must not be spent once per arithmetic site. 1832func emit_overflow_trap(P: *Parser) -> i64 { 1833 let msg: *u8 = "nx: FATAL integer overflow: a 64-bit add, subtract or multiply produced a result outside the i64 range (CWE-190) under --chkarith. The operation was REFUSED and the process aborted rather than continuing on a silently wrapped value. Fix: check the operands before the operation or widen the representation; where wrap-around IS the intent (hashes, ring counters) use __wrap_add / __wrap_sub / __wrap_mul, which are exempt.\n" as *u8 1834 var mlen: i64 = 0 1835 while msg[mlen] != 0 { mlen = mlen + 1 } 1836 let modv: i64 = P.module as i64 1837 if g_ovf_msg_mod != modv { g_ovf_msg_gid1 = 0 } 1838 var gid: i64 = g_ovf_msg_gid1 - 1 1839 if g_ovf_msg_gid1 == 0 { 1840 gid = ir_add_global_string(P.module, msg, mlen) 1841 g_ovf_msg_gid1 = gid + 1 1842 g_ovf_msg_mod = modv 1843 } 1844 let u8ty: *Type = alloc_type(TY_I8, 1, 1) 1845 let pt: *Type = alloc_type(TY_PTR, 8, 8) 1846 pt.pointee = u8ty 1847 let msg_v: i64 = ir_global_value(P.current_fn, gid, pt) 1848 return emit_trap_syscalls(P, msg_v, mlen, NX_TRAP_OVERFLOW) 1849} 1850 1851// Shared tail of every leg: `cond` true means OVERFLOW. Emits br_cond cond -> fail, ok ; 1852// fail: trap ; br ok ; ok: <caller continues>. The `br ok` after exit_group is unreachable and 1853// exists only so every block ends in a terminator (cfg_rebuild_edges requires it), exactly as 1854// emit_bounds_check_v does. On return P.current_bb is `ok`. 1855func chk_branch_on_overflow(P: *Parser, cond: i64) -> i64 { 1856 let fail_bb: *BasicBlock = ir_block_new(P.current_fn) 1857 let ok_bb: *BasicBlock = ir_block_new(P.current_fn) 1858 ir_emit_br_cond(P.current_bb, cond, fail_bb, ok_bb) 1859 P.current_bb = fail_bb 1860 emit_overflow_trap(P) 1861 ir_emit_br(P.current_bb, ok_bb) 1862 P.current_bb = ok_bb 1863 return 0 1864} 1865 1866// ADD leg. `r` is the already-emitted a + b. 1867func chk_add_overflow(P: *Parser, a: i64, b: i64, r: i64) -> i64 { 1868 let t1: i64 = ir_emit_binop(P.current_bb, OP_XOR, a, r, ir_type_i64()) 1869 let t2: i64 = ir_emit_binop(P.current_bb, OP_XOR, b, r, ir_type_i64()) 1870 let t3: i64 = ir_emit_binop(P.current_bb, OP_AND, t1, t2, ir_type_i64()) 1871 let zero_v: i64 = safe_const_i64(P, 0, "parse.nx:chk-add-zero" as *u8) 1872 let c: i64 = ir_emit_binop(P.current_bb, OP_LT_S, t3, zero_v, ir_type_i64()) 1873 return chk_branch_on_overflow(P, c) 1874} 1875 1876// SUB leg. `r` is the already-emitted a - b. 1877func chk_sub_overflow(P: *Parser, a: i64, b: i64, r: i64) -> i64 { 1878 let t1: i64 = ir_emit_binop(P.current_bb, OP_XOR, a, b, ir_type_i64()) 1879 let t2: i64 = ir_emit_binop(P.current_bb, OP_XOR, a, r, ir_type_i64()) 1880 let t3: i64 = ir_emit_binop(P.current_bb, OP_AND, t1, t2, ir_type_i64()) 1881 let zero_v: i64 = safe_const_i64(P, 0, "parse.nx:chk-sub-zero" as *u8) 1882 let c: i64 = ir_emit_binop(P.current_bb, OP_LT_S, t3, zero_v, ir_type_i64()) 1883 return chk_branch_on_overflow(P, c) 1884} 1885 1886// i64 minimum, built by shift so no literal has to spell 2^63 (the tokenizer's i64 literal range 1887// ends one below it). 63 is inside the constant shift-count range this same parser enforces. 1888func chk_i64_min() -> i64 { 1889 var m: i64 = 1 1890 m = m << 63 1891 return m 1892} 1893 1894// MUL leg. `r` is the already-emitted a * b. 1895// cur: c0 = a == 0 ; br_cond c0 -> ok, chk1 (0 * anything cannot overflow) 1896// chk1: c1 = a == -1 ; br_cond c1 -> neg1, div 1897// neg1: c2 = b == MIN ; br_cond c2 -> fail, ok (-1 * MIN is the one a=-1 overflow) 1898// div: q = r /s a ; c3 = q != b ; br_cond c3 -> fail, ok 1899// fail: trap ; br ok 1900// ok: 1901func chk_mul_overflow(P: *Parser, a: i64, b: i64, r: i64) -> i64 { 1902 let chk1_bb: *BasicBlock = ir_block_new(P.current_fn) 1903 let neg1_bb: *BasicBlock = ir_block_new(P.current_fn) 1904 let div_bb: *BasicBlock = ir_block_new(P.current_fn) 1905 let fail_bb: *BasicBlock = ir_block_new(P.current_fn) 1906 let ok_bb: *BasicBlock = ir_block_new(P.current_fn) 1907 let zero_v: i64 = safe_const_i64(P, 0, "parse.nx:chk-mul-zero" as *u8) 1908 let c0: i64 = ir_emit_binop(P.current_bb, OP_EQ, a, zero_v, ir_type_i64()) 1909 ir_emit_br_cond(P.current_bb, c0, ok_bb, chk1_bb) 1910 P.current_bb = chk1_bb 1911 let m1_v: i64 = safe_const_i64(P, 0 - 1, "parse.nx:chk-mul-m1" as *u8) 1912 let c1: i64 = ir_emit_binop(P.current_bb, OP_EQ, a, m1_v, ir_type_i64()) 1913 ir_emit_br_cond(P.current_bb, c1, neg1_bb, div_bb) 1914 P.current_bb = neg1_bb 1915 let min_v: i64 = safe_const_i64(P, chk_i64_min(), "parse.nx:chk-mul-min" as *u8) 1916 let c2: i64 = ir_emit_binop(P.current_bb, OP_EQ, b, min_v, ir_type_i64()) 1917 ir_emit_br_cond(P.current_bb, c2, fail_bb, ok_bb) 1918 P.current_bb = div_bb 1919 let q: i64 = ir_emit_binop(P.current_bb, OP_DIV_S, r, a, ir_type_i64()) 1920 let c3: i64 = ir_emit_binop(P.current_bb, OP_NE, q, b, ir_type_i64()) 1921 ir_emit_br_cond(P.current_bb, c3, fail_bb, ok_bb) 1922 P.current_bb = fail_bb 1923 emit_overflow_trap(P) 1924 ir_emit_br(P.current_bb, ok_bb) 1925 P.current_bb = ok_bb 1926 return 0 1927} 1928 1929// Does this binop qualify for the runtime check? Mode on, op is + - *, result type i64, and 1930// neither operand is a pointer (p - q is an integer difference over a pointer axis). 1931func chk_arith_applies(P: *Parser, op: i64, left: i64, right: i64, rty: *Type) -> i64 { 1932 if g_chkarith_live != 1 { return 0 } 1933 if rty == (0 as *Type) { return 0 } 1934 if rty.kind != TY_I64 { return 0 } 1935 var is_arith: i64 = 0 1936 if op == OP_ADD { is_arith = 1 } 1937 if op == OP_SUB { is_arith = 1 } 1938 if op == OP_MUL { is_arith = 1 } 1939 if is_arith == 0 { return 0 } 1940 let lt: *Type = nx_value_type(P, left) 1941 let rt: *Type = nx_value_type(P, right) 1942 if lt != (0 as *Type) { if lt.kind == TY_PTR { return 0 } } 1943 if rt != (0 as *Type) { if rt.kind == TY_PTR { return 0 } } 1944 return 1 1945} 1946 1947// The runtime injection, called by emit_typed_binop right after it emits the binop. 1948func chk_arith_inject(P: *Parser, op: i64, left: i64, right: i64, r: i64) -> i64 { 1949 if op == OP_ADD { return chk_add_overflow(P, left, right, r) } 1950 if op == OP_SUB { return chk_sub_overflow(P, left, right, r) } 1951 if op == OP_MUL { return chk_mul_overflow(P, left, right, r) } 1952 return 0 1953} 1954 1955// Pure overflow predicates over the compiler's own i64 (which wraps -- the very behaviour this 1956// rung refuses in user programs, used here as the oracle because it is the hardware's answer). 1957func chk_const_add_overflows(a: i64, b: i64) -> i64 { 1958 let r: i64 = a + b 1959 if ((a ^ r) & (b ^ r)) < 0 { return 1 } 1960 return 0 1961} 1962func chk_const_sub_overflows(a: i64, b: i64) -> i64 { 1963 let r: i64 = a - b 1964 if ((a ^ b) & (a ^ r)) < 0 { return 1 } 1965 return 0 1966} 1967func chk_const_mul_overflows(a: i64, b: i64) -> i64 { 1968 if a == 0 { return 0 } 1969 if a == 0 - 1 { 1970 if b == chk_i64_min() { return 1 } 1971 return 0 1972 } 1973 let r: i64 = a * b 1974 if r / a != b { return 1 } 1975 return 0 1976} 1977 1978// COMPILE-TIME LEG: both operands constant and the result provably overflows -> refuse, naming 1979// the rule (the shift-count voice). Called by the additive/multiplicative parsers with the 1980// operator token so the caret lands on the operator. Mode-gated like the runtime leg, so the 1981// default build of the corpus is untouched. 1982func chk_const_overflow_refuse(P: *Parser, op: i64, left: i64, right: i64, op_tok: *Tok) -> i64 { 1983 if g_chkarith_live != 1 { return 0 } 1984 let lv: *Value = val_at(P.current_fn, left) 1985 let rv: *Value = val_at(P.current_fn, right) 1986 if lv.kind != VK_CONST_INT { return 0 } 1987 if rv.kind != VK_CONST_INT { return 0 } 1988 let lt: *Type = nx_value_type(P, left) 1989 let rt: *Type = nx_value_type(P, right) 1990 if lt != (0 as *Type) { if lt.kind != TY_I64 { return 0 } } 1991 if rt != (0 as *Type) { if rt.kind != TY_I64 { return 0 } } 1992 var bad: i64 = 0 1993 if op == OP_ADD { bad = chk_const_add_overflows(lv.const_int, rv.const_int) } 1994 if op == OP_SUB { bad = chk_const_sub_overflows(lv.const_int, rv.const_int) } 1995 if op == OP_MUL { bad = chk_const_mul_overflows(lv.const_int, rv.const_int) } 1996 if bad == 0 { return 0 } 1997 nx_diag_at(op_tok.line) 1998 nx_diag_puts(": this constant arithmetic overflows the 64-bit integer range (operands " as *u8) 1999 nx_put_dec_err(lv.const_int) 2000 nx_diag_puts(" and " as *u8) 2001 nx_put_dec_err(rv.const_int) 2002 nx_diag_puts(").\n" as *u8) 2003 nx_diag_caret(op_tok.line, op_tok.col) 2004 nx_diag_puts(" why the build stopped: the processor keeps only the low 64 bits of the result, so this line would silently compute a WRAPPED value that is not the one written (CWE-190) -- and a constant overflow in source is never the intent.\n" as *u8) 2005 nx_diag_puts(" fix: use operands whose result fits in 64 bits; if wrap-around IS intended (hash constants, ring counters) spell it __wrap_add / __wrap_sub / __wrap_mul, which are exempt from this check.\n" as *u8) 2006 nx_diag_puts(" capability=checked-arith-const-overflow -- roadmap: nishifamily.com/compare/lang\n" as *u8) 2007 nx_diag_note_error() 2008 return 1 2009} 2010 2011// ===================== LN2: OPTION / NULL ENFORCEMENT (opt_enforce_unwrap) ===================== 2012// CWE-476, the estate's recurring P2 meta-class ("sentinel-zero-as-null"): a function returns 0 for 2013// not-found / failed, the caller dereferences the result without looking, and the program is right 2014// only by accident of its input. Option and Result exist in the stdlib but nothing ENFORCES their 2015// use, so they are a convention wearing the shape of a type system (the /compare/lang row). 2016// 2017// DECLARED MODE, OFF BY DEFAULT, the LN3/LN1 shape: enforcement runs only under `--optenforce` 2018// (g_optenforce_live, set by nx_compile_x86 main). Under the default every build is byte-identical by 2019// construction (nothing below emits IR or refuses). 2020// 2021// WHAT IS ENFORCED: a dereference of a pointer-typed LOCAL that is not PROVEN non-null on the 2022// current path is REFUSED at parse time, naming the local and the line. Proof is tracked on the 2023// Local (nn_checked) and comes from exactly these sources -- the idioms this corpus already uses: 2024// bound non-null `let p = &x`, `let s = "lit"` (address-of / string global / function address) 2025// narrowed `if p != 0 {` / `if p != (0 as *T) {` / `if (p as i64) != 0 {` -> inside then 2026// `if p == 0 { ...ends in return/break/continue/sys_exit }` -> rest of the block 2027// `if p == 0 { } else {` -> inside else 2028// `while p != 0 {` -> inside the body 2029// `nx_assert_ptr(p as *u8, ...)` / `nx_assert_ptr(p, ...)` -> rest of the block 2030// un-narrowed `p = <expr>` re-derives from the new value (a call result is nullable again) 2031// Deref sites covered: `p.f` / `p[i]` reads (parse_field_chain's FIRST link through the handshake 2032// parse_primary sets), `p.f = v`, `p[i] = v`, `*p = v`, and `*p` reads. DECLARED FLOOR: chained 2033// derefs of pointers loaded from fields or calls (`a.b.c`, `f().x`, `g.rows[i].y`) are not named 2034// locals and are not checked by this rung -- bind them to a local first, the estate's own idiom. 2035// Flow sensitivity is syntactic (token shape of the condition), which is what a reader sees; it does 2036// not chase aliases (`let q = p` after a check carries p's bit at bind time, nothing more). 2037// 2038// The refusal is a diagnostic (nx_diag_note_error), not a die, so multi-error recovery reports every 2039// site in a unit -- the census a ratchet needs. Exit code / trap: none; this rung is compile-time. 2040static g_optenforce_live: i64 2041func nx_optenforce_set(v: i64) -> i64 { g_optenforce_live = v; return 0 } 2042// The rung's public predicate (watch-symbol family); the fixture organ runtime/nx_option_enforce.nx 2043// carries the runnable witness under the watch symbol opt_enforce_unwrap itself. 2044func opt_enforce_unwrap_live() -> i64 { return g_optenforce_live } 2045 2046// Handshake from parse_primary to parse_field_chain (the LN3 g_pp_base_* shape): the value id the 2047// primary just produced for a pointer-typed local, and whether that local is currently UNCHECKED. 2048// Only the chain's FIRST link can match (value ids are monotonic per function). 2049static g_oe_base_val: i64 2050static g_oe_base_unchk: i64 2051static g_oe_base_line: i64 2052static g_oe_base_col: i64 2053static g_oe_base_name: i64 // *u8 of the identifier text (token text lives for the whole parse) 2054// "Did the statement just parsed END the path?" Set by return/break/continue/sys_exit statements, 2055// cleared at every statement start; compound statements clear it at their end (an `if` as a whole 2056// does not terminate just because its last inner statement did). 2057static g_oe_term: i64 2058func oe_reset_fn() -> i64 { g_oe_base_val = 0 - 1; g_oe_base_unchk = 0; g_oe_term = 0; return 0 } 2059 2060// Is the just-produced value provably non-null? &x (OP_ADDR_OF), an alloca address, a string 2061// global, a function address. Everything else -- calls, loads, params, casts of those, and the 2062// literal 0 -- is not. 2063// `as T` is emitted as a typed identity (OP_ADD v, const 0 -- see parse_unary), so the origin is 2064// found by looking through identity-adds whose right operand is the constant 0. Terminates without 2065// a bound: an operand's value id is always smaller than its result's (SSA order), so each hop 2066// strictly descends. 2067func oe_value_nonnull(P: *Parser, v: i64) -> i64 { 2068 if v < 0 { return 0 } 2069 let val: *Value = val_at(P.current_fn, v) 2070 if val.kind == VK_INSTR { 2071 if val.instr != (0 as *Instr) { 2072 if val.instr.op == OP_ADD { 2073 if val.instr.n_operands == 2 { 2074 let rv: *Value = val_at(P.current_fn, val.instr.op1) 2075 if rv.kind == VK_CONST_INT { if rv.const_int == 0 { 2076 // INTEGER -> POINTER cast (`z as *u8` with z an i64): the programmer's own 2077 // assertion that the integer IS an address -- trusted, the computed-address 2078 // floor (measured: nx_syscalls.nx:518 `let q: *u8 = z as *u8`). A POINTER -> 2079 // pointer cast carries the operand's origin, so recurse. 2080 let ov: *Value = val_at(P.current_fn, val.instr.op0) 2081 if ov.ty != (0 as *Type) { if ov.ty.kind != TY_PTR { return 1 } } 2082 if val.instr.op0 < v { return oe_value_nonnull(P, val.instr.op0) } 2083 } } 2084 } 2085 } 2086 // A COMPUTED ADDRESS is trusted: `(base + off) as *T`, GEP, masked/shifted addresses. The 2087 // class this rung refuses is the SENTINEL-RETURNING CALL, the PARAMETER, the LOADED pointer 2088 // and the REASSIGNMENT (the P2 meta-class); arithmetic on an address is the caller's 2089 // statement that it owns the base (declared floor, measured: the stdlib's own 2090 // nxa_dump_printable does `(src + i) as *u8` and must build under the mode). 2091 if val.instr.op == OP_ADD { return 1 } // non-identity add (the identity case returned above) 2092 if val.instr.op == OP_SUB { return 1 } 2093 if val.instr.op == OP_MUL { return 1 } 2094 if val.instr.op == OP_AND { return 1 } 2095 if val.instr.op == OP_OR { return 1 } 2096 if val.instr.op == OP_SHL { return 1 } 2097 if val.instr.op == OP_GEP { return 1 } 2098 } 2099 } 2100 if val.kind == VK_GLOBAL { return 1 } 2101 if val.kind == VK_FUNC_ADDR { return 1 } 2102 // A PARAMETER is the callee's contract: the caller owes non-null (no nullable annotation exists 2103 // in the language yet; a future `?*T` would flip this). DECLARED FLOOR -- measured: treating 2104 // params as nullable refuses every string helper in the stdlib (`msg[n]` in nxa_die). 2105 if val.kind == VK_PARAM { return 1 } 2106 if val.kind == VK_CONST_INT { if val.const_int != 0 { return 1 } return 0 } 2107 if val.kind == VK_INSTR { 2108 if val.instr != (0 as *Instr) { 2109 if val.instr.op == OP_ADDR_OF { return 1 } 2110 if val.instr.op == OP_ALLOCA { return 1 } 2111 // THE ALLOCATOR IS NON-NULL BY CONTRACT: sys_mmap never returns a poisoned pointer -- every 2112 // failure path ends in nxa_die (sys_exit 12) or degrades to a mapping that was itself 2113 // checked (nx_syscalls.nx:423-). Treating its result as nullable would refuse the 2114 // stdlib itself (measured on the first build of this mode: nx_syscalls.nx:124 `nbox[0]`). 2115 // sys_mmap_shared returns the raw kernel value (can be negative) and stays nullable. 2116 if val.instr.op == OP_CALL { 2117 if val.instr.callee != (0 as *Function) { 2118 if oe_callee_is(val.instr.callee, "sys_mmap" as *u8, 8) == 1 { return 1 } 2119 } 2120 } 2121 } 2122 } 2123 return 0 2124} 2125 2126// Byte-compare a callee's name against (name, len) -- the find_function idiom (name_start IS the *u8). 2127func oe_callee_is(fn: *Function, name: *u8, len: i64) -> i64 { 2128 if fn.name_len != len { return 0 } 2129 let fname: *u8 = fn.name_start as *u8 2130 var j: i64 = 0 2131 var eq: i64 = 1 2132 var go: i64 = 1 2133 while go == 1 { 2134 if j >= len { go = 0 } else { 2135 if fname[j] != name[j] { eq = 0; go = 0 } 2136 j = j + 1 2137 } 2138 } 2139 return eq 2140} 2141 2142// Record the bit on a just-bound or just-assigned pointer local. 2143func oe_bind_local(P: *Parser, L: *Local, v: i64) -> i64 { 2144 if L == (0 as *Local) { return 0 } 2145 if L.ty == (0 as *Type) { return 0 } 2146 if L.ty.kind != TY_PTR { return 0 } 2147 L.nn_checked = oe_value_nonnull(P, v) 2148 return 0 2149} 2150 2151// The refusal, in the shift-count voice. `name` is the identifier text; line/col anchor the caret. 2152func oe_refuse(name: *u8, line: i64, col: i64) -> i64 { 2153 nx_diag_at(line) 2154 nx_diag_puts(": dereference of '" as *u8) 2155 nx_diag_puts(name) 2156 nx_diag_puts("', a pointer that may be null on this path (--optenforce mode).\n" as *u8) 2157 nx_diag_caret(line, col) 2158 nx_diag_puts(" why the build stopped: this pointer came from a call, a parameter, a loaded field or a reassignment, and nothing on the path to this use has proven it non-zero -- the sentinel-zero deref class (CWE-476), which crashes or reads address 0 only on the input nobody tested.\n" as *u8) 2159 nx_diag_puts(" fix: guard the use -- `if " as *u8) 2160 nx_diag_puts(name) 2161 nx_diag_puts(" != 0 { ... }` around it, or `if " as *u8) 2162 nx_diag_puts(name) 2163 nx_diag_puts(" == 0 { return ... }` before it, or nx_assert_ptr(" as *u8) 2164 nx_diag_puts(name) 2165 nx_diag_puts(" as *u8, \"why it cannot be null\") when the invariant is yours; bind a pointer loaded from a field or returned by a call to a local first so the guard has a name to narrow.\n" as *u8) 2166 nx_diag_puts(" capability=option-enforce-unwrap -- roadmap: nishifamily.com/compare/lang\n" as *u8) 2167 nx_diag_note_error() 2168 return 1 2169} 2170 2171// A MODULE STATIC pointer (`static p: *T`) is injected as a Local whose value is the slot's global 2172// address (VK_GLOBAL), loaded on use. DECLARED FLOOR: statics are module state initialised by module 2173// invariants (the arena's nxa_st is null until the first sys_mmap and used only after); this rung 2174// does not model initialisation order, so statics are trusted rather than refused wholesale -- 2175// measured: refusing them refuses the stdlib (nx_syscalls.nx:364 nxa_dump_sizes). 2176func oe_local_is_static(P: *Parser, L: *Local) -> i64 { 2177 if L.value_id < 0 { return 0 } 2178 let sv: *Value = val_at(P.current_fn, L.value_id) 2179 if sv.kind == VK_GLOBAL { return 1 } 2180 return 0 2181} 2182 2183// Statement-level deref of a named local (p.f = / p[i] = / *p = / *p reads): refuse if unchecked. 2184func oe_check_local_deref(P: *Parser, L: *Local, tok: *Tok) -> i64 { 2185 if g_optenforce_live != 1 { return 0 } 2186 if L == (0 as *Local) { return 0 } 2187 if L.ty == (0 as *Type) { return 0 } 2188 if L.ty.kind != TY_PTR { return 0 } 2189 if L.nn_checked == 1 { return 0 } 2190 if oe_local_is_static(P, L) == 1 { return 0 } 2191 return oe_refuse(tok_text_ptr(tok), tok.line, tok.col) 2192} 2193 2194// parse_primary hands the chain walker the base it just resolved (see parse_field_chain). 2195func oe_handshake_set(P: *Parser, L: *Local, v: i64, tok: *Tok) -> i64 { 2196 if g_optenforce_live != 1 { return 0 } 2197 if L.ty == (0 as *Type) { return 0 } 2198 if L.ty.kind != TY_PTR { return 0 } 2199 if oe_local_is_static(P, L) == 1 { return 0 } 2200 g_oe_base_val = v 2201 g_oe_base_unchk = 1 - L.nn_checked 2202 g_oe_base_line = tok.line 2203 g_oe_base_col = tok.col 2204 g_oe_base_name = tok_text_ptr(tok) as i64 2205 return 0 2206} 2207 2208// The chain walker's first-link check: `v` is the base being dereferenced right now. 2209func oe_chain_check(P: *Parser, v: i64) -> i64 { 2210 if g_optenforce_live != 1 { return 0 } 2211 if v != g_oe_base_val { return 0 } 2212 if g_oe_base_unchk != 1 { return 0 } 2213 g_oe_base_unchk = 0 // one refusal per base use, not one per link 2214 return oe_refuse(g_oe_base_name as *u8, g_oe_base_line, g_oe_base_col) 2215} 2216 2217// TOKEN-SHAPE PROBE for a null test at P.pos (the condition of an `if` / `while`), consuming 2218// nothing. Recognised: IDENT (!=|==) 0 IDENT (!=|==) ( 0 as ...) ( IDENT as i64 ) (!=|==) 0 2219// Returns 0 = not a null test, 1 = "!= 0" shape, 2 = "== 0" shape; the tested local in *outL 2220// (0 when the identifier is not a pointer-typed local -- then the caller does nothing). 2221static g_oe_probe_L: i64 // *Local the last probe matched (0 = none); a static, not an out-param, 2222 // so the hot `if` path allocates nothing per statement 2223func oe_probe_null_test(P: *Parser) -> i64 { 2224 g_oe_probe_L = 0 2225 let t0: *Tok = tok_at(P.toks, P.pos) 2226 let t1: *Tok = tok_at(P.toks, P.pos + 1) 2227 let t2: *Tok = tok_at(P.toks, P.pos + 2) 2228 var ident_tok: *Tok = 0 as *Tok 2229 var op_tok: *Tok = 0 as *Tok 2230 var zero_tok: *Tok = 0 as *Tok 2231 // shape A/B: IDENT op 0 | IDENT op ( 0 2232 if t0.kind == TK_IDENT { 2233 if t1.kind == TK_NE { op_tok = t1 } 2234 if t1.kind == TK_EQ { op_tok = t1 } 2235 if op_tok != (0 as *Tok) { 2236 ident_tok = t0 2237 if t2.kind == TK_INT { zero_tok = t2 } 2238 if t2.kind == TK_LPAREN { 2239 let t3: *Tok = tok_at(P.toks, P.pos + 3) 2240 if t3.kind == TK_INT { zero_tok = t3 } 2241 } 2242 } 2243 } 2244 // shape C: ( IDENT as i64 ) op 0 2245 if t0.kind == TK_LPAREN { 2246 if t1.kind == TK_IDENT { 2247 if t2.kind == TK_AS { 2248 let t4: *Tok = tok_at(P.toks, P.pos + 4) // ')' 2249 let t5: *Tok = tok_at(P.toks, P.pos + 5) // op 2250 let t6: *Tok = tok_at(P.toks, P.pos + 6) // 0 2251 if t4.kind == TK_RPAREN { 2252 if t5.kind == TK_NE { op_tok = t5 } 2253 if t5.kind == TK_EQ { op_tok = t5 } 2254 if op_tok != (0 as *Tok) { 2255 ident_tok = t1 2256 if t6.kind == TK_INT { zero_tok = t6 } 2257 } 2258 } 2259 } 2260 } 2261 } 2262 if ident_tok == (0 as *Tok) { return 0 } 2263 if zero_tok == (0 as *Tok) { return 0 } 2264 if zero_tok.int_val != 0 { return 0 } 2265 let L: *Local = find_local(P, tok_text_ptr(ident_tok)) 2266 if L == (0 as *Local) { return 0 } 2267 if L.ty == (0 as *Type) { return 0 } 2268 if L.ty.kind != TY_PTR { return 0 } 2269 g_oe_probe_L = L as i64 2270 if op_tok.kind == TK_NE { return 1 } 2271 return 2 2272} 2273 2274// ============== LN4 + LN5: OWNERSHIP MOVES AND USE-AFTER-FREE (own_check_move / own_check_uaf) ============== 2275// CWE-416 and the move half of it. ONE checker for both rungs on purpose: a move and a release are 2276// the same event seen twice -- the name stops naming a live buffer -- and splitting them would put 2277// two state machines on one Local, which is the duplicate-ruler defect with extra steps. 2278// 2279// DECLARED MODE, OFF BY DEFAULT, the LN1/LN2/LN3 shape: nothing below refuses or emits unless 2280// `--ownership` is passed (g_ownership_live, set by nx_compile_x86 main). Under the default every 2281// build is byte-identical BY CONSTRUCTION -- this rung emits no IR under EITHER mode, so its whole 2282// footprint is parse-time refusals. The per-class ratchet flips the default after a clean census. 2283// 2284// WHAT "RELEASED" MEANS HERE, MEASURED RATHER THAN ASSUMED (2026-08-25). The /compare/lang row 2285// claims an "arena no-free doctrine (nothing frees)". That is FALSE as written: sys_munmap has 2286// exactly ONE definition (nx_syscalls.nx:294) and 1,431 reference sites across buildroot/runtime 2287// (nx_absent, corpus_complete=1), and there is NO arena reset and NO nxa_free -- both ABSENT-PROVEN 2288// over the same corpus. So sys_munmap is not one release verb among several, it is THE release 2289// verb this estate has, and it is the only sound oracle available for a use-after-free check. 2290// 2291// AND THE DOCTRINE IS TRUE ONLY BY AN ALLOCATOR ACCIDENT, WHICH IS THE WHOLE ARGUMENT FOR A CHECKER. 2292// sys_munmap's body is `if len <= NXA_SMALL_MAX { return 0 }` -- a release of 256 bytes or fewer is 2293// a NO-OP today because small allocations come from the bump arena, so reading after it happens to 2294// be harmless. Above 256 bytes the pages really are returned and the read is a genuine SIGSEGV. 2295// THIS RUNG REFUSES BOTH SIZES DELIBERATELY. Keying the check on NXA_SMALL_MAX would make a safety 2296// checker depend on an allocator tuning constant -- and that constant has ALREADY MOVED once 2297// (256 -> 128, recorded in nx_fsops_write.nx's own CAS diagnostic), which would have silently 2298// un-refused a whole class of real defects the day somebody re-tuned the arena. A release is a 2299// release. 2300// 2301// WHAT AN OWNER IS: a POINTER-TYPED LOCAL bound to a VISIBLE ALLOCATION -- a call to sys_mmap, 2302// optionally through an `as *T` cast chain. The origin walk reuses oe_callee_is and the identity-add 2303// look-through that LN2 already established, so there is exactly one "is this an allocation" ruler. 2304// Unlike LN3 the SIZE is irrelevant here, so a runtime-sized sys_mmap(n) IS tracked by this rung 2305// where LN3 declares it a floor. 2306// 2307// WHAT ENDS OWNERSHIP: 2308// __move(p) the DECLARED transfer point (LN4). A pure marker: it returns p's own value 2309// and emits no IR, so `f(__move(p))` is byte-for-byte `f(p)`. This is the 2310// __wrap_add contract -- an intrinsic that states intent the type system cannot. 2311// sys_munmap(p, n) the release point (LN5), measured above. 2312// After either, ANY read of the name is refused. A DOUBLE move and a DOUBLE free fall out for free: 2313// the second statement reads the name to pass it, and that read is the use being refused. 2314// 2315// WHAT RE-STARTS IT: `p = <a visible allocation>` on a `var` re-derives OWN_LIVE from the new value, 2316// exactly as LN2 re-derives its non-null bit. A checker that refused every second use of a name 2317// would be useless, so the accept side is a load-bearing tooth, not a courtesy. 2318// 2319// DECLARED IMPRECISION -- every floor below is sound in the DO-NOTHING direction (a miss skips a 2320// refusal, it never invents one), except the branch case which is called out as deliberate: 2321// * NAMED LOCALS ONLY. A buffer living in a struct field or a module static is not tracked; bind 2322// it to a local first, the estate's own idiom and LN2's identical floor. 2323// * CROSS-FUNCTION IS NOT MODELLED. A buffer passed to a callee that frees it is invisible here; 2324// that needs an ownership annotation on parameters, which the language does not have yet. 2325// * `var` ALIASES ARE NOT SWEPT. Two `let` names for one buffer ARE (see own_mark's alias sweep, 2326// sound because a `let` is SSA so equal value ids mean one allocation), but two `var`s hold 2327// separate alloca slots and do not share a value id. 2328// * LOOP BACK-EDGES ARE NOT MODELLED. The scan is linear over statements, so a release at the 2329// bottom of a loop body is not seen by a use at the top on the next iteration. 2330// * BRANCHES POISON, DELIBERATELY. `if c { __move(p) }` then a use refuses even on the path where 2331// the move did not happen. This is not an oversight -- it is the Rust conditional-move rule, and 2332// Rust is the Best-in-class column this row is measured against. It is the ONE place this rung 2333// is wrong in the refusing direction, and it is named here so nobody has to rediscover it. 2334const OWN_NONE: i64 = 0 2335const OWN_LIVE: i64 = 1 2336const OWN_MOVED: i64 = 2 2337const OWN_RELEASED: i64 = 3 2338 2339static g_ownership_live: i64 2340func nx_ownership_set(v: i64) -> i64 { g_ownership_live = v; return 0 } 2341// The rung's public predicate (the watch-symbol family, mirroring bck_ptr_provenance_live and 2342// opt_enforce_unwrap_live): is ownership checking live in this build of the compiler? The fixture 2343// organ runtime/nx_ownership.nx carries the runnable witnesses under own_check_move / own_check_uaf. 2344// It is ALSO the ONE reader of g_ownership_live -- every gate below asks through it rather than 2345// testing the static directly. Its two siblings ship defined-and-never-called (measured: 2346// opt_enforce_unwrap_live has exactly one reference in the whole tree, its own definition), which 2347// makes them a standing nx_unwired name; routing the mode test through the accessor gives the 2348// predicate a real job instead of adding a third unwired name to the ratchet. 2349func own_check_live() -> i64 { return g_ownership_live } 2350 2351// Is value `v` a VISIBLE ALLOCATION? An OP_CALL to sys_mmap, or an `as *T` cast of one. `as T` is 2352// emitted as a typed identity (OP_ADD v, const 0 -- see parse_unary), so the origin is found by 2353// looking through identity-adds whose right operand is the constant 0. Terminates without a bound: 2354// an operand's value id is strictly smaller than its result's (SSA order), so each hop descends. 2355func own_value_is_alloc(P: *Parser, v: i64) -> i64 { 2356 if v < 0 { return 0 } 2357 let val: *Value = val_at(P.current_fn, v) 2358 if val.kind != VK_INSTR { return 0 } 2359 if val.instr == (0 as *Instr) { return 0 } 2360 if val.instr.op == OP_CALL { 2361 if val.instr.callee != (0 as *Function) { 2362 if oe_callee_is(val.instr.callee, "sys_mmap" as *u8, 8) == 1 { return 1 } 2363 } 2364 return 0 2365 } 2366 if val.instr.op == OP_ADD { 2367 if val.instr.n_operands == 2 { 2368 let rv: *Value = val_at(P.current_fn, val.instr.op1) 2369 if rv.kind == VK_CONST_INT { 2370 if rv.const_int == 0 { 2371 if val.instr.op0 < v { return own_value_is_alloc(P, val.instr.op0) } 2372 } 2373 } 2374 } 2375 } 2376 return 0 2377} 2378 2379// Record ownership on a just-bound or just-assigned pointer local. Runs under BOTH modes (it only 2380// writes parser state and emits nothing), the oe_bind_local discipline -- so the default build is 2381// unaffected and the hot path carries no extra branch. 2382func own_bind_local(P: *Parser, L: *Local, v: i64) -> i64 { 2383 if L == (0 as *Local) { return 0 } 2384 if L.ty == (0 as *Type) { return 0 } 2385 if L.ty.kind != TY_PTR { return 0 } 2386 L.own_line = 0 2387 if own_value_is_alloc(P, v) == 1 { L.own_state = OWN_LIVE; return 0 } 2388 L.own_state = OWN_NONE 2389 return 0 2390} 2391 2392// End a local's ownership, and sweep its `let` ALIASES. `let q = p` binds q to the SAME value id, so 2393// q is a second name for one buffer; marking only the name written in the __move / sys_munmap would 2394// leave every alias reading OWN_LIVE and the use-after-free through `q` would be MISSED. A `let` is 2395// SSA (parse_stmt_ident_assign refuses stores to it), so equal value ids really do mean one 2396// allocation -- the sweep is sound, not a guess. COST: one pass over the locals IN SCOPE per move or 2397// release, both of which are rare statements; there is no per-use cost at all. 2398func own_mark(P: *Parser, L: *Local, st: i64, ln: i64) -> i64 { 2399 L.own_state = st 2400 L.own_line = ln 2401 if L.is_alloca == 1 { return 0 } 2402 if L.value_id < 0 { return 0 } 2403 var i: i64 = 0 2404 while i < P.n_locals { 2405 let A: *Local = parser_loc_at(P, i) 2406 if (A as i64) != (L as i64) { 2407 if A.is_alloca == 0 { 2408 if A.value_id == L.value_id { 2409 if A.own_state == OWN_LIVE { A.own_state = st; A.own_line = ln } 2410 } 2411 } 2412 } 2413 i = i + 1 2414 } 2415 return 0 2416} 2417 2418// THE refusal, in the estate's 5W-and-H diagnostic voice. Both halves share it so the two rungs 2419// cannot drift into two voices, and each stamps its OWN capability= line so a gate can assert WHICH 2420// rule fired rather than merely that something was refused. 2421func own_refuse(name: *u8, line: i64, col: i64, st: i64, from_line: i64) -> i64 { 2422 nx_diag_at(line) 2423 if st == OWN_MOVED { 2424 nx_diag_puts(": use of '" as *u8) 2425 nx_diag_puts(name) 2426 nx_diag_puts("' after its ownership was moved away on line " as *u8) 2427 nx_put_dec_err(from_line) 2428 nx_diag_puts(" (--ownership mode).\n" as *u8) 2429 nx_diag_caret(line, col) 2430 nx_diag_puts(" why the build stopped: __move gave this buffer to somebody else, so this name no longer refers to memory you own -- whoever took it may already have freed it or handed it on, and reading through a name whose owner has changed is the use-after-move half of CWE-416.\n" as *u8) 2431 nx_diag_puts(" fix: use the value the move produced (bind it: `let taken = __move(" as *u8) 2432 nx_diag_puts(name) 2433 nx_diag_puts(")`) and read THAT, or give this name a new allocation before using it again -- `" as *u8) 2434 nx_diag_puts(name) 2435 nx_diag_puts(" = sys_mmap(n)` on a `var` restores it; if the move was not meant to transfer anything, delete the __move.\n" as *u8) 2436 nx_diag_puts(" capability=own-check-move -- roadmap: nishifamily.com/compare/lang\n" as *u8) 2437 nx_diag_note_error() 2438 return 1 2439 } 2440 nx_diag_puts(": use of '" as *u8) 2441 nx_diag_puts(name) 2442 nx_diag_puts("' after the memory it points at was released on line " as *u8) 2443 nx_put_dec_err(from_line) 2444 nx_diag_puts(" (--ownership mode).\n" as *u8) 2445 nx_diag_caret(line, col) 2446 nx_diag_puts(" why the build stopped: sys_munmap handed these pages back to the kernel, so this pointer now refers to memory this process does not own -- the use-after-free class (CWE-416). Above NXA_SMALL_MAX bytes the pages are really gone and this read is a SIGSEGV; at or below it the arena makes the release a no-op TODAY, which is exactly why the arena no-free doctrine reads as safe and exactly why it is not a guarantee.\n" as *u8) 2447 nx_diag_puts(" fix: move the release AFTER the last read, or re-allocate before this line -- `" as *u8) 2448 nx_diag_puts(name) 2449 nx_diag_puts(" = sys_mmap(n)` on a `var` restores it; if this line is the second release of one buffer, delete it (that is a double free, and the first release already returned the pages).\n" as *u8) 2450 nx_diag_puts(" capability=own-check-uaf -- roadmap: nishifamily.com/compare/lang\n" as *u8) 2451 nx_diag_note_error() 2452 return 1 2453} 2454 2455// THE one use site, shared by the read path (parse_primary_local_or_const, which every bare name goes 2456// through -- deref, call argument, initializer, all of it) and the three statement write paths 2457// (`*p = v`, `p[i] = v`, `p.f = v`, which consume their identifier without reaching parse_primary). 2458// Reports EVERY use rather than one per local: the refusal is a diagnostic, not a die, and a ratchet 2459// needs the census. NX_DIAG_MAX_ERRS still caps the volume. 2460func own_check_use(P: *Parser, L: *Local, tok: *Tok) -> i64 { 2461 if own_check_live() != 1 { return 0 } 2462 if L == (0 as *Local) { return 0 } 2463 if L.own_state == OWN_MOVED { return own_refuse(tok_text_ptr(tok), tok.line, tok.col, OWN_MOVED, L.own_line) } 2464 if L.own_state == OWN_RELEASED { return own_refuse(tok_text_ptr(tok), tok.line, tok.col, OWN_RELEASED, L.own_line) } 2465 return 0 2466} 2467 2468// Resolve an identifier token to a local and end its ownership. Silent when the name is not a 2469// tracked owner: that is the DECLARED FLOOR speaking (a param, a field-loaded pointer, an allocator 2470// this rung cannot see), not a user error, and refusing there would reject correct code for the sake 2471// of a limitation of this checker. Already MOVED or RELEASED is silent here too -- the read of the 2472// name in the very statement doing the second move or free was already refused by own_check_use, and 2473// saying it twice would make one defect look like two. 2474func own_mark_named(P: *Parser, tok: *Tok, st: i64) -> i64 { 2475 if own_check_live() != 1 { return 0 } 2476 if tok == (0 as *Tok) { return 0 } 2477 let L: *Local = find_local(P, tok_text_ptr(tok)) 2478 if L == (0 as *Local) { return 0 } 2479 if L.own_state != OWN_LIVE { return 0 } 2480 own_mark(P, L, st, tok.line) 2481 return 0 2482} 2483 2484// __move's argument adjudication. A NON-NAME argument (`__move(f())`, `__move(s.buf)`) is REFUSED 2485// under the mode: a transfer point that cannot name what it transferred is unreadable to this rung 2486// AND to the next human, and unlike the floors above this one the programmer can fix it in one line. 2487func own_move_arg(P: *Parser, tok: *Tok) -> i64 { 2488 if own_check_live() != 1 { return 0 } 2489 if tok.kind != TK_IDENT { 2490 nx_diag_at(tok.line) 2491 nx_diag_puts(": __move needs the NAME of an owned buffer, not an expression (--ownership mode).\n" as *u8) 2492 nx_diag_caret(tok.line, tok.col) 2493 nx_diag_puts(" why the build stopped: a move has to say which name stops being valid; an expression has no name to invalidate, so nothing after this point could be checked and the mode would silently protect nothing.\n" as *u8) 2494 nx_diag_puts(" fix: bind the buffer to a local first -- `let buf = <expr>` -- then `__move(buf)`.\n" as *u8) 2495 nx_diag_puts(" capability=own-check-move -- roadmap: nishifamily.com/compare/lang\n" as *u8) 2496 nx_diag_note_error() 2497 return 1 2498 } 2499 let L: *Local = find_local(P, tok_text_ptr(tok)) 2500 if L == (0 as *Local) { return 0 } 2501 if L.own_state != OWN_LIVE { return 0 } 2502 own_mark(P, L, OWN_MOVED, tok.line) 2503 return 0 2504} 2505 2506// Parse a postfix chain of `.field` and `[index]` on an existing 2507// Value id. Each field access emits GEP + LOAD using the struct's 2508// field offsets; each index emits (base + index*elem_size) + LOAD 2509// with element size driven by the current type's pointee. Returns 2510// the final Value id; loops until the next token isn't a dot or 2511// open bracket. Caller supplies an initial type hint; subsequent 2512// chains infer from the resolved field/element type. 2513// 2514// Arity ceiling for INDIRECT (fn-pointer) calls, shared by the named-local 2515// path (parse_primary_call) and the postfix field path below so the two can 2516// never drift. 23 = the IR's operand cap: op0 carries the fn-ptr and op1..op23 2517// the arguments. Was 6 (register-only codegen) until 2026-07-25, when the x86 2518// backend gained real stack args for indirect calls; the RISC-V backend fails 2519// loud past 7 until its own stack-arg arc lands. 2520const NX_FNPTR_MAX_ARGS: i64 = 23 2521// LN29 (2026-09-02): THE ONE FIELD RESOLVER for `x.f` -- the symbol the lang.matrix watch names 2522// (parse_field_of_struct). MEASURED: `f.name_bytes` on a *Function (which declares name_start/name_len) 2523// compiled and printed junk, because every `.f` site returned the BASE unchanged when the field was not 2524// found (T#field-chain-silent-return): the read took the pointer itself as the value. That silent path is 2525// still needed for an UNRESOLVED struct (a forward stub or a call's return type whose fields are not known 2526// yet -- n_fields == 0; the self-build relies on it, a loud fail there was tried and reverted). So the rule 2527// is exact: a struct that DECLARES fields and does not declare this one is refused, in the teaching voice, 2528// naming the struct and the fields it really has. A stub stays silent, as before, by construction. 2529func parse_field_of_struct(P: *Parser, stty: *Type, ftok: *Tok, fname: *u8, flen: i64) -> *StructField { 2530 let field: *StructField = ir_type_struct_find_field(stty, fname, flen) 2531 if field != (0 as *StructField) { return field } 2532 if stty.kind != TY_STRUCT { return field } 2533 if stty.n_fields <= 0 { return field } 2534 nx_diag_at(ftok.line) 2535 nx_diag_puts(": struct '" as *u8) 2536 if stty.name_len > 0 { sys_write(2, stty.name_bytes, stty.name_len) } else { nx_diag_puts("(anonymous)" as *u8) } 2537 nx_diag_puts("' has no field named '" as *u8) 2538 sys_write(2, fname, flen) 2539 nx_diag_puts("'.\n" as *u8) 2540 nx_diag_caret(ftok.line, ftok.col) 2541 nx_diag_puts(" why the build stopped: a field is an offset into this struct's layout, and a name the struct does not declare has no offset -- until today the read silently took the base pointer itself as the value (and a store would have scribbled at offset 0): a wrong value that compiles, links and runs.\n" as *u8) 2542 nx_diag_puts(" fix: use one of the fields this struct declares: " as *u8) 2543 let fbase: i64 = stty.fields as i64 2544 var fi: i64 = 0 2545 while fi < stty.n_fields { 2546 let sf: *StructField = (fbase + fi * 32) as *StructField 2547 if fi > 0 { nx_diag_puts(", " as *u8) } 2548 sys_write(2, sf.name_bytes, sf.name_len) 2549 fi = fi + 1 2550 } 2551 nx_diag_puts(" -- or read it through the struct that really declares it.\n" as *u8) 2552 nx_diag_note_error() 2553 return field 2554} 2555 2556func parse_field_chain(P: *Parser, base: i64, base_ty: *Type) -> i64 { 2557 nx_assert_ptr(P.current_fn as *u8, "parse_field_chain: P.current_fn" as *u8) 2558 nx_assert(P.current_fn.values_cap > 0, 2559 "parse_field_chain: P.current_fn init" as *u8) 2560 // LN3: snapshot the provenance handshake AT ENTRY. The index expression inside a `[...]` 2561 // arm re-enters parse_primary and overwrites the statics (that nested entry snapshots its 2562 // own base -- recursion is correct by the same rule). Only the FIRST chain link can match: 2563 // after one `[i]` or `.f`, v is a fresh value id and the compare below goes quiet. 2564 let pp_v: i64 = g_pp_base_val 2565 let pp_ext: i64 = g_pp_base_ext 2566 var v: i64 = base 2567 var ty: *Type = base_ty 2568 var keep_going: i64 = 1 2569 while keep_going == 1 { 2570 let k: i64 = peek_kind(P) 2571 if k == TK_DOT { 2572 advance_tok(P) 2573 let ftok: *Tok = advance_tok(P) 2574 let fname: *u8 = tok_text_ptr(ftok) 2575 var flen: i64 = 0 2576 while fname[flen] != 0 { flen = flen + 1 } 2577 // Auto-deref: if ty is *Struct, look through the pointer. 2578 var stty: *Type = ty 2579 if stty != (0 as *Type) { 2580 if stty.kind == TY_PTR { 2581 if stty.pointee != (0 as *Type) { stty = stty.pointee } 2582 oe_chain_check(P, v) // LN2: `p.f` reads through the pointer p 2583 } 2584 } 2585 if stty == (0 as *Type) { return v } 2586 let field: *StructField = parse_field_of_struct(P, stty, ftok, fname, flen) 2587 // T#field-chain-silent-return (KNOWN, four-pillar in progress): 2588 // when the field is not found on `stty` this returns the base 2589 // unchanged. That SILENTLY mis-compiles `call().field` when the 2590 // callee's return-type struct fields are unresolved (it yields the 2591 // POINTER -- the count_ok=0 bug in the netscope verdict layer). 2592 // A loud-fail PREVENT here was TRIED and REVERTED: it breaks the 2593 // compiler's own self-build (nx_compile_x86.nx / main.nx rely on 2594 // the silent return-base for `.toks`, a deeper type-resolution 2595 // gap). FIX today = bind call results to a typed temp before 2596 // `.field` (the idiom used everywhere). The real PREVENT needs 2597 // the return-type-struct-field resolution fixed first, THEN this 2598 // can loud-fail safely. See NISHI_DEBT_LEDGER.tsv. 2599 if field == (0 as *StructField) { return v } 2600 let off: i64 = safe_const_i64(P, field.offset, "parse.nx:LINE-field.offset" as *u8) 2601 let addr: i64 = ir_emit_gep(P.current_bb, v, off, field.ty) 2602 // Bug fix 2026-05-16 (substrate-bisect): 2603 // Don't LOAD the GEP result when the field is itself a 2604 // struct -- it's an intermediate address that the next 2605 // chain iteration will offset from. The original code 2606 // unconditionally loaded after every GEP, treating 2607 // struct-typed intermediates as pointers and producing 2608 // GEP -> LOAD -> GEP -> LOAD instead of the correct 2609 // GEP -> GEP -> LOAD. The C anchor handled this right; 2610 // this brings nx_parse.nx into parity. 2611 var is_struct_field: i64 = 0 2612 if field.ty != (0 as *Type) { 2613 if field.ty.kind == TY_STRUCT { is_struct_field = 1 } 2614 } 2615 if is_struct_field == 1 { 2616 v = addr 2617 } else { 2618 v = ir_emit_load(P.current_bb, addr, field.ty) 2619 } 2620 ty = field.ty 2621 } 2622 if k == TK_LBRACKET { 2623 advance_tok(P) 2624 let idx: i64 = parse_expr(P) 2625 match_kind(P, TK_RBRACKET) 2626 // Element size from the pointer/array pointee; default i64. 2627 var elem_sz: i64 = 8 2628 var elem_ty: *Type = ir_type_i64() 2629 var is_arr_r: i64 = 0 2630 var is_slice_r: i64 = 0 2631 if ty != (0 as *Type) { 2632 if ty.kind == TY_PTR { 2633 if ty.pointee != (0 as *Type) { 2634 elem_ty = ty.pointee 2635 if elem_ty.size > 0 { elem_sz = elem_ty.size } 2636 } 2637 } 2638 if ty.kind == TY_ARRAY { 2639 is_arr_r = 1 2640 if ty.pointee != (0 as *Type) { 2641 elem_ty = ty.pointee 2642 if elem_ty.size > 0 { elem_sz = elem_ty.size } 2643 } 2644 } 2645 if ty.kind == TY_SLICE { 2646 is_slice_r = 1 2647 if ty.pointee != (0 as *Type) { 2648 elem_ty = ty.pointee 2649 if elem_ty.size > 0 { elem_sz = elem_ty.size } 2650 } 2651 } 2652 } 2653 // SLICE READ: v is the HANDLE (address of {data,len}). Read the length FIRST, check the 2654 // index against it, and only then load the data pointer -- so the load of `data` happens 2655 // in the block that is reached only when the index is known good. After this, v holds 2656 // the data pointer and the ordinary POINTER path below computes the element address. 2657 if is_slice_r == 1 { 2658 let so_v: i64 = safe_const_i64(P, NX_SLICE_LEN_OFFSET, "parse.nx:slice-rd-off" as *u8) 2659 let sla: i64 = ir_emit_binop(P.current_bb, OP_ADD, v, so_v, ir_type_i64()) 2660 let slen: i64 = ir_emit_load(P.current_bb, sla, ir_type_i64()) 2661 emit_bounds_check_v(P, idx, slen) 2662 v = ir_emit_load(P.current_bb, v, ir_type_i64()) 2663 } 2664 // SPATIAL SAFETY (CWE-787/125): a fixed array [N]T indexed by a COMPILE-TIME CONSTANT is bounds- 2665 // checked at COMPILE TIME -- zero runtime cost, zero false positives (both N and the index are 2666 // known). Runtime indices + raw pointers stay unchecked = the honest C-class part; only [N]T 2667 // carries a length N. Moves nx_lang_sota_census's a[2]-on-[2] probe from silent-OOB to compile error. 2668 if is_arr_r == 1 { 2669 if ty != (0 as *Type) { 2670 if elem_sz > 0 { 2671 let idxv_r: *Value = val_at(P.current_fn, idx) 2672 let nelem_r: i64 = ty.size / elem_sz 2673 if idxv_r.kind == VK_CONST_INT { 2674 if idxv_r.const_int < 0 { parse_die("fixed-array index < 0" as *u8, 21) } 2675 if idxv_r.const_int >= nelem_r { parse_die("fixed-array index >= size" as *u8, 25) } 2676 } 2677 // RUNTIME index -> inject the check (see emit_bounds_check). This is the 2678 // half the compile-time test above cannot reach, and it was the measured 2679 // gap: nx_spatial_probe's buf[6] on a [4]i64 read out of range silently. 2680 if idxv_r.kind != VK_CONST_INT { emit_bounds_check(P, idx, nelem_r) } 2681 } 2682 } 2683 } 2684 // LN3 READ leg (CWE-125): a raw pointer base whose Local carries a provenance extent 2685 // is checked like a typed array. v == pp_v holds exactly when this base IS the 2686 // let-local parse_primary_local_or_const just resolved (value ids are monotonic per 2687 // function, so no other value can match). pp_idx_check gates on --ptrprov itself. 2688 // LN2: `p[i]` reads through a POINTER p (array and slice bases are not pointers). 2689 if is_arr_r == 0 { if is_slice_r == 0 { oe_chain_check(P, v) } } 2690 var pp_go_r: i64 = 0 2691 if is_arr_r == 0 { if is_slice_r == 0 { if v == pp_v { if pp_ext > 0 { pp_go_r = 1 } } } } 2692 if pp_go_r == 1 { if elem_sz > 0 { if ty != (0 as *Type) { if ty.kind == TY_PTR { 2693 // fn().field is a documented miscompile class -- hoist the token first. 2694 let pp_tok_r: *Tok = tok_at(P.toks, P.pos) 2695 pp_idx_check(P, idx, pp_ext / elem_sz, pp_tok_r.line) 2696 } } } } 2697 let esize: i64 = safe_const_i64(P, elem_sz, "parse.nx:LINE-elem_sz" as *u8) 2698 let off2: i64 = ir_emit_binop(P.current_bb, OP_MUL, 2699 idx, esize, ir_type_i64()) 2700 // ARRAY: v is the frame ADDRESS -> GEP (leaq base + off). POINTER: v is the loaded 2701 // pointer VALUE -> OP_ADD. Both then LOAD the element. 2702 var addr2: i64 = 0 2703 if is_arr_r == 1 { 2704 addr2 = ir_emit_gep(P.current_bb, v, off2, elem_ty) 2705 } else { 2706 addr2 = ir_emit_binop(P.current_bb, OP_ADD, v, off2, ty) 2707 } 2708 // AGGREGATE element (struct/array): KEEP its ADDRESS so a following `.field` / `[j]` GEPs 2709 // off it. Loading it (the scalar/pointer path) reads the first 8 bytes as a value and then 2710 // dereferences THAT as the field base -> wild access (arr-of-struct `arr[i].x` gave 0 / crash). 2711 var elem_is_agg: i64 = 0 2712 if elem_ty != (0 as *Type) { 2713 if elem_ty.kind == TY_STRUCT { elem_is_agg = 1 } 2714 if elem_ty.kind == TY_ARRAY { elem_is_agg = 1 } 2715 } 2716 if elem_is_agg == 1 { 2717 v = addr2 2718 } 2719 if elem_is_agg == 0 { 2720 v = ir_emit_load(P.current_bb, addr2, elem_ty) 2721 } 2722 ty = elem_ty 2723 } 2724 // POSTFIX INDIRECT CALL through a fn-pointer field: `obj.fn_field(args)`. 2725 // This is a PARITY GAP, not a regression: the C anchor parse.c:1498 has 2726 // carried this branch all along, the self-hosted parser never did (debt 2727 // seq715, diagnosis corrected 2026-07-25). Without it the loop exited on 2728 // TK_LPAREN and handed back the LOADED FUNCTION ADDRESS while `(args)` was 2729 // re-parsed as a stray parenthesised expression and DISCARDED -- the method 2730 // never ran and the caller got a code-segment pointer that reads as a 2731 // plausible integer. It failed OPEN, which is why every multi-method vtable 2732 // in the tree (CLAUDE rule 6 OOP) was silently broken and nobody saw it. 2733 // ADDITIVE: gated on ty.kind == TY_FUNC, so any source that compiled before 2734 // this change takes the byte-identical path it took before. 2735 var did_call: i64 = 0 2736 if k == TK_LPAREN { 2737 var is_fnptr: i64 = 0 2738 if ty != (0 as *Type) { 2739 if ty.kind == TY_FUNC { is_fnptr = 1 } 2740 } 2741 if is_fnptr == 1 { 2742 advance_tok(P) 2743 let cargs_raw: *u8 = sys_mmap(NX_FNPTR_MAX_ARGS * 8 + 16) 2744 let cargs: *i64 = cargs_raw as *i64 2745 var cn: i64 = 0 2746 if peek_kind(P) != TK_RPAREN { 2747 let c0: i64 = parse_expr(P) 2748 cargs[cn] = c0 2749 cn = cn + 1 2750 while peek_kind(P) == TK_COMMA { 2751 advance_tok(P) 2752 if cn >= NX_FNPTR_MAX_ARGS { 2753 parse_die("postfix fn-ptr call exceeds max args (register-only indirect codegen)" as *u8, 32) 2754 } 2755 let cx: i64 = parse_expr(P) 2756 cargs[cn] = cx 2757 cn = cn + 1 2758 } 2759 } 2760 match_kind(P, TK_RPAREN) 2761 // TY_FUNC keeps its RETURN type in `pointee` (parse_type, nx_types.nx). 2762 var cret: *Type = ty.pointee 2763 if cret == (0 as *Type) { cret = ir_type_i64() } 2764 v = ir_emit_call_indirect(P.current_bb, v, cret, cargs, cn) 2765 ty = cret 2766 did_call = 1 2767 } 2768 } 2769 if k != TK_DOT { 2770 if k != TK_LBRACKET { 2771 if did_call == 0 { keep_going = 0 } 2772 } 2773 } 2774 } 2775 return v 2776} 2777 2778func parse_primary(P: *Parser) -> i64 { 2779 nx_assert_ptr(P.current_fn as *u8, "parse_primary: P.current_fn" as *u8) 2780 nx_assert(P.current_fn.values_cap > 0, 2781 "parse_primary: P.current_fn init" as *u8) 2782 let t: *Tok = tok_at(P.toks, P.pos) 2783 if t.kind == TK_INT { 2784 advance_tok(P) 2785 return safe_const_i64(P, t.int_val, "parse.nx:LINE-t.int_val" as *u8) 2786 } 2787 if t.kind == TK_FLOAT { 2788 // Default-precision (f64) literal. Convert (whole, frac_num, 2789 // frac_digits) -> IEEE 754 binary64 bit pattern. Store the 2790 // bits in const_int; attach TY_F64 to the Value so downstream 2791 // passes (regalloc f-reg partition, rv_emit_fbinop) route it 2792 // as a double. 2793 // 2794 // Default literal type is f64 -- matches Rust / C-double / Zig 2795 // and gives sketches/numerics enough precision out of the box. 2796 // Explicit f32 selection: literal suffix `1.5f32` (TK_FLOAT_F32 2797 // path below) or type-context inference at `let x: f32 = ...` 2798 // sites (handled in parse_stmt_let by re-emitting the constant 2799 // as f32 when the LHS type demands it). 2800 advance_tok(P) 2801 let bits: i64 = fp64_from_dec(t.text2, t.text3, t.text4) // LN36/LN40: (m, e10, sticky) 2802 let vid: i64 = safe_const_i64(P, bits, "parse.nx:LINE-bits" as *u8) 2803 let base: i64 = P.current_fn.values as i64 2804 let v: *Value = (base + vid * 48) as *Value 2805 v.ty = alloc_type(TY_F64, 8, 8) 2806 return vid 2807 } 2808 if t.kind == TK_FLOAT_F32 { 2809 // Explicit single-precision literal (`1.5f32`). Same packing 2810 // as TK_FLOAT but routes through fp32_from_parts + TY_F32. 2811 advance_tok(P) 2812 // LN36: an f32 literal with an exponent packs as f64 and downcasts; the legacy triple stays 2813 // byte-identical for every f32 literal that has none. 2814 var bits32: i64 = 0 2815 if t.text5 == 1 { bits32 = fp64_to_fp32(fp64_from_dec(t.text2, t.text3, t.text4)) } 2816 if t.text5 == 0 { bits32 = fp32_from_parts(t.int_val, t.text0, t.text1) } 2817 let vid: i64 = safe_const_i64(P, bits32, "parse.nx:LINE-bits32" as *u8) 2818 let base: i64 = P.current_fn.values as i64 2819 let v: *Value = (base + vid * 48) as *Value 2820 v.ty = alloc_type(TY_F32, 4, 4) 2821 return vid 2822 } 2823 if t.kind == TK_TRUE { advance_tok(P); return safe_const_i64(P, 1, "parse.nx:LINE-const1" as *u8) } 2824 if t.kind == TK_FALSE { advance_tok(P); return safe_const_i64(P, 0, "parse.nx:LINE-const0" as *u8) } 2825 // String literal. Registers the bytes as a Module global, then 2826 // builds a VAL_GLOBAL_ADDR Value referencing that global's id. 2827 // The backend lowers this to `la reg, .Lg<id>` (or the global's 2828 // name when one was registered -- strings are anonymous so they 2829 // fall through to the .Lg id form). 2830 // V-LANGEXT M2: if-then-else as expression. 2831 // 2832 // `if cond then expr_t else expr_f` 2833 // 2834 // Distinct from statement-form `if c { ... }` (consumed at 2835 // parse_stmt level before reaching parse_primary). Matches 2836 // Haskell / OCaml / Elm convention; substrate authors already 2837 // use this syntax. 2838 // 2839 // HYGIENE GUARDS (per operator 2026-05-27 "we want to avoid 2840 // hoisting issues and namespace issues and all the other hygiene 2841 // issues that make languages suck, really research and make sure 2842 // we are adding good functionality not future nightmares"): 2843 // 2844 // G1 REQUIRED `then`: TK_THEN must follow cond; if absent the 2845 // parser dies clearly. This is the syntactic anchor that 2846 // disambiguates expression-form from statement-form (which 2847 // doesn't use TK_THEN at all). 2848 // G2 REQUIRED `else`: TK_ELSE must follow then-branch; no 2849 // orphan-if at expression position. Matches Haskell / Elm 2850 // (Rust without-else returns unit which doesn't compose at 2851 // expression position; we reject explicitly). 2852 // G3 DEPTH LIMIT: P.ifexp_depth incremented at entry; rejected 2853 // if exceeds NX_PARSE_IFEXP_MAX_DEPTH (16). Prevents any 2854 // pathological codegen path that could blow the stack at 2855 // compile or runtime. Real-world readable code never nests 2856 // ternary/if-expr beyond 4-5 levels; 16 is generous. 2857 // G4 TYPE-COMPATIBLE BRANCHES: both branches must produce 2858 // values whose ir types match (V1: same Type pointer or 2859 // same kind+size; rejects mismatch). Matches Rust / Zig / 2860 // Elm / Haskell. Prevents the JS-style implicit-coerce-to- 2861 // worst-common-type bugs. 2862 // G5 SINGLE COND EVAL: parse_expr(P) for cond runs ONCE before 2863 // the BB split. Side effects in cond happen exactly once. 2864 // G6 SINGLE BRANCH EVAL: ir_emit_br_cond emits a hard branch; 2865 // ONLY the chosen branch BB runs at runtime. No speculative 2866 // both-eval (rules out unintended side effects + duplicate 2867 // work). Lexical isolation: branches use fresh BBs so any 2868 // temporary IR values don't leak to merge BB (NishiLang 2869 // already lexically scoped). 2870 // 2871 if t.kind == TK_IF { 2872 advance_tok(P) // eat 'if' 2873 // G3: depth check on ENTRY (so the first if-expr-call counts). 2874 P.ifexp_depth = P.ifexp_depth + 1 2875 if P.ifexp_depth > 16 { 2876 parse_die("if-expression nested too deep (max 16); refactor with named locals" as *u8, 70) 2877 } 2878 let cond: i64 = parse_expr(P) // G5 2879 // G1 2880 if peek_kind(P) != TK_THEN { 2881 parse_die("expected 'then' after if-expression condition" as *u8, 46) 2882 } 2883 advance_tok(P) // eat 'then' 2884 // Allocate one result slot per if-expression (function-local 2885 // alloca). Pathological depth is bounded by G3 so per-function 2886 // alloca count is bounded. 2887 let result_ty: *Type = ir_type_i64() 2888 let result_addr: i64 = ir_emit_alloca(P.current_bb, result_ty) 2889 2890 let then_bb: *BasicBlock = ir_block_new(P.current_fn) // "ifexp_then" (label dropped; see ARITY FIX above) 2891 let else_bb: *BasicBlock = ir_block_new(P.current_fn) // "ifexp_else" 2892 let merge_bb: *BasicBlock = ir_block_new(P.current_fn) // "ifexp_merge" 2893 2894 ir_emit_br_cond(P.current_bb, cond, then_bb, else_bb) // G6 2895 2896 // then branch 2897 P.current_bb = then_bb 2898 let then_v: i64 = parse_expr(P) 2899 ir_emit_store(P.current_bb, result_addr, then_v, result_ty) 2900 ir_emit_br(P.current_bb, merge_bb) 2901 2902 // G2 2903 if peek_kind(P) != TK_ELSE { 2904 parse_die("if-expression requires 'else' branch (no orphan-if at expression position)" as *u8, 76) 2905 } 2906 advance_tok(P) // eat 'else' 2907 2908 // else branch 2909 P.current_bb = else_bb 2910 let else_v: i64 = parse_expr(P) 2911 ir_emit_store(P.current_bb, result_addr, else_v, result_ty) 2912 ir_emit_br(P.current_bb, merge_bb) 2913 2914 // merge: load result 2915 P.current_bb = merge_bb 2916 let result: i64 = ir_emit_load(P.current_bb, result_addr, result_ty) 2917 2918 // G4 type-check enforcement queued V+1 (needs IR-value->type 2919 // lookup wiring; current pipeline lacks a portable getter). 2920 // For V1 both branches are TREATED as i64 (the most common 2921 // case). Mismatched-type if-expr produces a warning at IR 2922 // validate (existing pass) but doesn't break compile. 2923 2924 P.ifexp_depth = P.ifexp_depth - 1 2925 return result 2926 } 2927 2928 if t.kind == TK_STRING { 2929 advance_tok(P) 2930 // Prefer the heap-grown str_data buffer (handles literals 2931 // longer than the 63-byte inline text[] cap). Fall back to 2932 // the inline text[] when str_data is NULL (legacy callers). 2933 // Same fix as nxc2/parse.c TK_STRING. 2934 var bytes: *u8 = 0 as *u8 2935 var blen: i64 = 0 2936 if t.str_data != 0 { 2937 bytes = t.str_data as *u8 2938 blen = t.str_len 2939 } 2940 if t.str_data == 0 { 2941 bytes = tok_text_ptr(t) 2942 while bytes[blen] != 0 { blen = blen + 1 } 2943 } 2944 let gid: i64 = ir_add_global_string(P.module, bytes, blen) 2945 // Type: pointer-to-u8 (string literals are *u8 in practice). 2946 let pt: *Type = alloc_type(TY_PTR, 8, 8) 2947 let u8ty: *Type = alloc_type(TY_I8, 1, 1) 2948 pt.pointee = u8ty 2949 // SAME ROOT DEFECT AS THE STRING-CONST PATH (seq1552): an inline string literal is also a 2950 // pointer value, so `"abc"[0]` must run the postfix chain or its `[0]` desyncs the parser. 2951 // Fixing only the const half would leave the class alive on the literal half. Gated on an 2952 // actual postfix token, so the ~every-string-in-the-corpus no-postfix case is untouched. 2953 let slv: i64 = ir_global_value(P.current_fn, gid, pt) 2954 if peek_kind(P) == TK_LBRACKET { return parse_field_chain(P, slv, pt) } 2955 if peek_kind(P) == TK_DOT { return parse_field_chain(P, slv, pt) } 2956 return slv 2957 } 2958 if t.kind == TK_LPAREN { 2959 advance_tok(P) 2960 let e: i64 = parse_expr(P) 2961 advance_tok(P) // expect ')' 2962 // SAME ROOT DEFECT AS THE STRING-LITERAL PATH ABOVE (seq1552 class): a parenthesized 2963 // expression is a value like any other, so `(p as *u8)[0]` / `(x).f` must run the 2964 // postfix chain. Without this, the pending `[`/`.` silently TERMINATED the expression: 2965 // the pointer value landed in the receiver truncated and the index was DROPPED 2966 // (measured: `("AB" as *u8)[0]` compiled clean and returned addr&0xFF=213, not 65; 2967 // in an if-condition the leftover `[` desynced the parser). Gated on an actual 2968 // postfix token so every plain `(expr)` in the corpus is untouched. 2969 if peek_kind(P) == TK_LBRACKET { 2970 let epv: *Value = val_at(P.current_fn, e) 2971 return parse_field_chain(P, e, epv.ty) 2972 } 2973 if peek_kind(P) == TK_DOT { 2974 let epv2: *Value = val_at(P.current_fn, e) 2975 return parse_field_chain(P, e, epv2.ty) 2976 } 2977 return e 2978 } 2979 // Ident -> call, local, or module const. Hoisted helper to 2980 // shrink parse_primary's stack frame (task #21 codegen workaround). 2981 if t.kind == TK_IDENT { 2982 return parse_primary_ident(P, t) 2983 } 2984 // PREVENT pillar (four-pillar), architecture-respecting denylist. 2985 // Die ONLY for tokens that can NEVER start a primary expression: the 2986 // operator range [TK_PLUS .. TK_SHR] (40..60) plus `@` `#` `=>`. 2987 // parse_unary consumes every prefix-unary operator (- ! ~ & *) BEFORE 2988 // calling parse_primary, so an operator reaching THIS fallthrough is 2989 // always a missing-operator desync (the `~` class that surfaced as 2990 // "address-of unknown local") or a parser regression -- never 2991 // legitimate. Every OTHER token (structural closers/separators `;` 2992 // `)` `]` `,` `}` `->` `:` `..`, EOF, ...) can legitimately land here 2993 // as an empty-expression terminator, where the historical `return 0` 2994 // is the contract real code depends on (empty statements, function- 2995 // pointer types, empty arg positions) -- preserve it. This kills the 2996 // silent-desync class WITHOUT fighting the parser's return-0 recovery. 2997 // LN13b: a function literal is an EXPRESSION. This must sit BEFORE the fallthrough denylist 2998 // below: TK_FUNC is outside every deny range, so without it the token reaches `return 0` 2999 // UNCONSUMED and the parser desyncs into the next statement -- which is why the failure used 3000 // to surface as a brace-balance complaint pointing at innocent code. 3001 if t.kind == TK_FUNC { return parse_func_literal(P) } 3002 let tk: i64 = t.kind 3003 var bad: i64 = 0 3004 if tk >= TK_PLUS { if tk <= TK_SHR { bad = 1 } } 3005 if tk == TK_AT { bad = 1 } 3006 if tk == TK_HASH { bad = 1 } 3007 if tk == TK_FAT_ARROW { bad = 1 } 3008 if bad == 1 { 3009 // LOCATE IT. This was the ONLY fatal in the parser that printed no file, no line and no caret, 3010 // while every other diagnostic here says exactly where it is -- and it is the error that 3011 // TERMINATES a desynced build, so the one message a reader most needs to place was the one that 3012 // refused to say. MEASURED COST (2026-08-16): an nx_browser COMPILE-FAIL was chased through seven 3013 // refuted hypotheses and five full-closure builds because its final line carried no location. 3014 // The anchor is the construct's OWN token, never P.pos at error time -- the parse_primary_call 3015 // lesson, reused here instead of re-derived. 3016 nx_diag_at(t.line) 3017 nx_diag_puts(": an operator starts this expression, so whatever it was meant to continue is not here (parser desync; token kind=" as *u8) 3018 nx_put_dec_err(t.kind) 3019 nx_diag_puts(")\n" as *u8) 3020 nx_diag_caret(t.line, t.col) 3021 nx_diag_puts(" why the build stopped: an operator can never START an expression at this point --\n" as *u8) 3022 nx_diag_puts(" parse_unary has already consumed every prefix form (- ! ~ & *), so an operator arriving\n" as *u8) 3023 nx_diag_puts(" here means the expression it belongs to was already closed off before it.\n" as *u8) 3024 nx_diag_puts(" fix: the usual cause is one expression split across lines with the operator LEADING the\n" as *u8) 3025 nx_diag_puts(" continuation line. Put the operator at the END of the previous line, or bind each part to\n" as *u8) 3026 nx_diag_puts(" a named value and combine them on one line.\n" as *u8) 3027 sys_exit(2) 3028 } 3029 return 0 3030} 3031 3032// ---- parse_primary TK_IDENT helpers (hoisted for stack-frame slim) ---- 3033 3034func parse_primary_ident(P: *Parser, t: *Tok) -> i64 { 3035 advance_tok(P) 3036 let name: *u8 = tok_text_ptr(t) 3037 if peek_kind(P) == TK_COLON_COLON { 3038 return parse_primary_qualified_variant(P, name) 3039 } 3040 if name[0] == 0x5F { 3041 if name[1] == 0x5F { 3042 let r: i64 = parse_primary_intrinsic(P, name) 3043 if r >= 0 { return r } 3044 } 3045 } 3046 if peek_kind(P) == TK_LPAREN { 3047 return parse_primary_call(P, name) 3048 } 3049 return parse_primary_local_or_const(P, name) 3050} 3051 3052func parse_primary_qualified_variant(P: *Parser, name: *u8) -> i64 { 3053 advance_tok(P) 3054 let vtok: *Tok = advance_tok(P) 3055 let vname: *u8 = tok_text_ptr(vtok) 3056 // SIZED FROM ITS INPUTS, not a fixed 80 (2026-08-07). This buffer holds a qualified name plus its terminator, 3057 // whose length is UNBOUNDED because identifiers are, and there was no length check of any 3058 // kind. Page granularity hid it -- an 80-byte request used to get a whole 4096-byte page, so 3059 // the overflow landed in 4KB of private slack. The small-allocation arena packs neighbours 3060 // together and exposed it: the arena ring canary reported overruns of an 80-byte allocation 3061 // reaching 33-64 bytes past the end, corrupting the parser's type table and producing 14 3062 // SPURIOUS type diagnostics against declarations that were correct. 3063 var qn0: i64 = 0 3064 while name[qn0] != 0 { qn0 = qn0 + 1 } 3065 var qv0: i64 = 0 3066 while vname[qv0] != 0 { qv0 = qv0 + 1 } 3067 let qbuf: *u8 = sys_mmap(qn0 + qv0 + 3) 3068 var nl: i64 = 0 3069 while name[nl] != 0 { qbuf[nl] = name[nl]; nl = nl + 1 } 3070 qbuf[nl] = 0x3A; nl = nl + 1 3071 qbuf[nl] = 0x3A; nl = nl + 1 3072 var vl: i64 = 0 3073 while vname[vl] != 0 { qbuf[nl + vl] = vname[vl]; vl = vl + 1 } 3074 qbuf[nl + vl] = 0 3075 3076 let out_raw: *u8 = sys_mmap(16) 3077 let out: *i64 = out_raw as *i64 3078 *out = 0 3079 lookup_mconst(P, qbuf, out) 3080 let disc: i64 = *out 3081 3082 var has_pay: i64 = 0 3083 var pay_v: i64 = 0 3084 if peek_kind(P) == TK_LPAREN { 3085 advance_tok(P) 3086 if peek_kind(P) != TK_RPAREN { 3087 pay_v = parse_expr(P) 3088 has_pay = 1 3089 } 3090 match_kind(P, TK_RPAREN) 3091 } 3092 3093 let ee: *EnumEntry = lookup_enum(P, name) 3094 if ee != (0 as *EnumEntry) { 3095 if ee.has_payload == 1 { 3096 if ee.shadow_ty != (0 as *Type) { 3097 let addr: i64 = ir_emit_alloca(P.current_bb, ee.shadow_ty) 3098 let off0: i64 = safe_const_i64(P, 0, "parse.nx:variant-z" as *u8) 3099 let tag_addr: i64 = ir_emit_gep(P.current_bb, addr, off0, ir_type_i64()) 3100 let disc_v: i64 = safe_const_i64(P, disc, "parse.nx:variant-d" as *u8) 3101 ir_emit_store(P.current_bb, tag_addr, disc_v, ir_type_i64()) 3102 let off8: i64 = safe_const_i64(P, 8, "parse.nx:variant-8" as *u8) 3103 let pay_addr: i64 = ir_emit_gep(P.current_bb, addr, off8, ir_type_i64()) 3104 var pv: i64 = pay_v 3105 if has_pay == 0 { pv = safe_const_i64(P, 0, "parse.nx:variant-p0" as *u8) } 3106 ir_emit_store(P.current_bb, pay_addr, pv, ir_type_i64()) 3107 // Hand back the struct's BASE ADDRESS as an ordinary SSA value. 3108 // Returning the alloca id itself hands the caller a SLOT, and every 3109 // as-VALUE consumer (return, binop, call argument) DEREFERENCES a 3110 // slot -- x86ctx_load_value_v emits `movq storage(%rbp)`. So 3111 // `return Result::Ok(v)` compiled to a load of the shadow's first 3112 // word and returned the TAG instead of the pointer: Ok(x) came back 3113 // as 0, indistinguishable from null, and Err(x) as 1, an equally 3114 // bogus pointer that merely looked non-null. Measured 2026-09-04. 3115 // A GEP result IS a materialised address -- x86ctx_emit_gep loads 3116 // the base as-address and stores rax into the result slot -- which 3117 // is exactly what a pointer-to-shadow value must be. The sibling 3118 // half of this bug (a GEP whose BASE is an alloca) was already 3119 // fixed in x86ctx_emit_gep and names this same constructor in its 3120 // comment; this is the half that was left. 3121 // Emitted AFTER the stores and consumed ONLY as the return value: 3122 // a GEP that feeds a load/store can be folded into that 3123 // instruction's SIB byte and marked dead (G8, sib_dead), which 3124 // would hand back a slot nothing ever wrote. 3125 let base_off: i64 = safe_const_i64(P, 0, "parse.nx:variant-base" as *u8) 3126 let base_addr: i64 = ir_emit_gep(P.current_bb, addr, base_off, ir_type_i64()) 3127 return base_addr 3128 } 3129 } 3130 } 3131 return safe_const_i64(P, disc, "parse.nx:variant-plain" as *u8) 3132} 3133 3134// Returns valueid >= 0 on match; -1 if name is not a recognised intrinsic. 3135func parse_layout_query_kind(name: *u8) -> i64 { 3136 var layout_query: i64 = 0 3137 var layout_name_n: i64 = 0 3138 while name[layout_name_n] != 0 { layout_name_n = layout_name_n + 1 } 3139 if layout_name_n == 9 { if streq_n(name, "__size_of", 9) == 1 { layout_query = 1 } } 3140 if layout_name_n == 10 { if streq_n(name, "__align_of", 10) == 1 { layout_query = 2 } } 3141 return layout_query 3142} 3143 3144func parse_layout_query_value(P: *Parser, layout_query: i64) -> i64 { 3145 if match_kind(P, TK_LPAREN) == 0 { 3146 nx_diag_puts("type-layout query requires parentheses containing a type\n" as *u8) 3147 nx_diag_note_error() 3148 return 0 3149 } 3150 let layout_type: *Type = parse_type(P) 3151 if match_kind(P, TK_RPAREN) == 0 { 3152 nx_diag_puts("type-layout query requires exactly one type, followed by ')'\n" as *u8) 3153 nx_diag_note_error() 3154 } 3155 if layout_type.kind == TY_PARAM { 3156 nx_diag_puts("type-layout query requires a concrete type; unresolved generic parameter has no qualified layout\n" as *u8) 3157 nx_diag_note_error() 3158 } 3159 if layout_type.size < 0 { 3160 nx_diag_puts("type-layout query refused an invalid negative size; allocation cannot be generated\n" as *u8) 3161 nx_diag_note_error() 3162 } 3163 // Use the same metadata as field addressing and array strides. This 3164 // exposes the current Nishi layout; it does not claim a C or WGSL ABI. 3165 if layout_type.kind == TY_STRUCT { 3166 if layout_type.size == 0 { 3167 nx_diag_puts("type-layout query requires a completed nonempty struct definition before use\n" as *u8) 3168 nx_diag_note_error() 3169 } 3170 } 3171 if layout_query == 1 { return layout_type.size } 3172 return layout_type.align 3173} 3174 3175func parse_primary_intrinsic(P: *Parser, name: *u8) -> i64 { 3176 let layout_query: i64 = parse_layout_query_kind(name) 3177 if layout_query != 0 { 3178 return safe_const_i64(P, parse_layout_query_value(P, layout_query), "type-layout" as *u8) 3179 } 3180 if streq_n(name, "__wfi", 5) == 1 { 3181 if peek_kind(P) == TK_LPAREN { 3182 advance_tok(P) 3183 match_kind(P, TK_RPAREN) 3184 ir_emit_wfi(P.current_bb) 3185 return safe_const_i64(P, 0, "parse.nx:wfi-0" as *u8) 3186 } 3187 } 3188 if streq_n(name, "__fence", 7) == 1 { 3189 if peek_kind(P) == TK_LPAREN { 3190 advance_tok(P) 3191 match_kind(P, TK_RPAREN) 3192 ir_emit_fence(P.current_bb) 3193 return safe_const_i64(P, 0, "parse.nx:fence-0" as *u8) 3194 } 3195 } 3196 if streq_n(name, "__mret", 6) == 1 { 3197 if peek_kind(P) == TK_LPAREN { 3198 advance_tok(P) 3199 match_kind(P, TK_RPAREN) 3200 ir_emit_mret(P.current_bb) 3201 return safe_const_i64(P, 0, "parse.nx:mret-0" as *u8) 3202 } 3203 } 3204 if streq_n(name, "__csrr", 6) == 1 { 3205 if peek_kind(P) == TK_LPAREN { 3206 advance_tok(P) 3207 let csr_tok: *Tok = advance_tok(P) 3208 let csr_num: i64 = csr_tok.int_val 3209 match_kind(P, TK_RPAREN) 3210 return ir_emit_csr_read(P.current_bb, csr_num) 3211 } 3212 } 3213 if streq_n(name, "__csrw", 6) == 1 { 3214 if peek_kind(P) == TK_LPAREN { 3215 advance_tok(P) 3216 let csr_tok: *Tok = advance_tok(P) 3217 let csr_num: i64 = csr_tok.int_val 3218 match_kind(P, TK_COMMA) 3219 let val_v: i64 = parse_expr(P) 3220 match_kind(P, TK_RPAREN) 3221 ir_emit_csr_write(P.current_bb, csr_num, val_v) 3222 return safe_const_i64(P, 0, "parse.nx:csrw-0" as *u8) 3223 } 3224 } 3225 // __slice(ptr, len) -> the handle for a []T : bind a length to a pointer. 3226 // Emits a 2-word header {data, len} on the frame and yields its ADDRESS. The ELEMENT TYPE is 3227 // NOT inferred here -- it comes from the declared type at the binding site 3228 // (`let s: []i64 = __slice(p, n)`), which is what makes indexing know its stride. That keeps 3229 // this builtin free of expression-type tracking the parser does not have. 3230 // NOTE (v1 limitation, deliberate): without the `: []T` annotation the local is a plain i64 3231 // holding a header address, and `s[i]` degrades to today's unchecked pointer arithmetic 3232 // rather than becoming unsafe in some NEW way. Requiring the annotation is a compile-time 3233 // error worth adding once slices have users. 3234 // ORDER MATTERS: streq_n compares only the first N bytes, so the 7-byte test for "__slice" 3235 // also matches "__slice_len". The longer name MUST be tested first or every __slice_len call 3236 // is silently parsed as a __slice construction and returns a header address instead of a 3237 // length. Caught by witness W8 printing len=129311793545216. 3238 if streq_n(name, "__slice_len", 11) == 1 { 3239 if peek_kind(P) == TK_LPAREN { 3240 advance_tok(P) 3241 let sh_v: i64 = parse_expr(P) 3242 match_kind(P, TK_RPAREN) 3243 let lo_v: i64 = safe_const_i64(P, NX_SLICE_LEN_OFFSET, "parse.nx:slicelen-off" as *u8) 3244 let la: i64 = ir_emit_binop(P.current_bb, OP_ADD, sh_v, lo_v, ir_type_i64()) 3245 return ir_emit_load(P.current_bb, la, ir_type_i64()) 3246 } 3247 } 3248 if streq_n(name, "__slice", 7) == 1 { 3249 if peek_kind(P) == TK_LPAREN { 3250 advance_tok(P) 3251 let sp_v: i64 = parse_expr(P) 3252 match_kind(P, TK_COMMA) 3253 let sl_v: i64 = parse_expr(P) 3254 match_kind(P, TK_RPAREN) 3255 let hdr_ty: *Type = alloc_type(TY_ARRAY, NX_SLICE_HDR_BYTES, 8) 3256 hdr_ty.pointee = ir_type_i64() 3257 let hdr: i64 = ir_emit_alloca(P.current_bb, hdr_ty) 3258 // An alloca id used as a VALUE operand AUTO-LOADS (it yields the slot's CONTENTS, not 3259 // its address) -- the same trap the array-decay path documents. Take the address once 3260 // with OP_ADDR_OF and use only that. Getting this wrong made `ADD(hdr, 8)` compute 3261 // data+8, so __slice wrote the LENGTH into the caller's element 1 and returned the 3262 // DATA pointer as the handle; every access then aliased the payload. Measured by 3263 // nx_bchk_dbg_addr printing delta=0 between the handle and the data pointer. 3264 let hdr_pt: *Type = alloc_type(TY_PTR, 8, 8) 3265 hdr_pt.pointee = ir_type_i64() 3266 let hdr_a: i64 = ir_emit_unop(P.current_bb, OP_ADDR_OF, hdr, hdr_pt) 3267 ir_emit_store(P.current_bb, hdr_a, sp_v, ir_type_i64()) 3268 let off_v: i64 = safe_const_i64(P, NX_SLICE_LEN_OFFSET, "parse.nx:slice-lenoff" as *u8) 3269 let lenaddr: i64 = ir_emit_binop(P.current_bb, OP_ADD, hdr_a, off_v, ir_type_i64()) 3270 ir_emit_store(P.current_bb, lenaddr, sl_v, ir_type_i64()) 3271 return hdr_a 3272 } 3273 } 3274 // (__slice_len is handled ABOVE __slice -- see the ORDER MATTERS note there.) 3275 if streq_n(name, "__syscall", 9) == 1 { 3276 if peek_kind(P) == TK_LPAREN { 3277 advance_tok(P) 3278 let sargs_raw: *u8 = sys_mmap(8 * 8 + 16) 3279 let sargs: *i64 = sargs_raw as *i64 3280 var sn: i64 = 0 3281 if peek_kind(P) != TK_RPAREN { 3282 sargs[sn] = parse_expr(P) 3283 sn = sn + 1 3284 while peek_kind(P) == TK_COMMA { 3285 advance_tok(P) 3286 if sn < 7 { 3287 sargs[sn] = parse_expr(P) 3288 sn = sn + 1 3289 } else { 3290 parse_expr(P) 3291 } 3292 } 3293 } 3294 match_kind(P, TK_RPAREN) 3295 return ir_emit_syscall(P.current_bb, sargs, sn) 3296 } 3297 } 3298 // Hardware f32 (IEEE-754 binary32) intrinsics. A float rides as the low 32 bits 3299 // of an i64 carrier (NishiLang has no f32 type); the x86 backend lowers these to 3300 // SSE scalar-single (cvtsi2ss/addss/mulss/divss/cvttss2si). __f32_from_i64(x) -> 3301 // float bits; __f32_to_i64(f) -> truncated int; __f32_add/mul/div(a,b) -> float bits. 3302 if streq_n(name, "__f32_from_i64", 14) == 1 { 3303 if peek_kind(P) == TK_LPAREN { 3304 advance_tok(P) 3305 let a_v: i64 = parse_expr(P) 3306 match_kind(P, TK_RPAREN) 3307 return ir_emit_f32_unop(P.current_bb, OP_FCAST_I_TO_F, a_v) 3308 } 3309 } 3310 if streq_n(name, "__f32_to_i64", 12) == 1 { 3311 if peek_kind(P) == TK_LPAREN { 3312 advance_tok(P) 3313 let a_v: i64 = parse_expr(P) 3314 match_kind(P, TK_RPAREN) 3315 return ir_emit_f32_unop(P.current_bb, OP_FCAST_F_TO_I, a_v) 3316 } 3317 } 3318 // Hardware f64 (IEEE-754 binary64) conversion + sqrt intrinsics. f64 literals 3319 // and f64 arithmetic already carry TY_F64; these bridge int<->f64 and add sqrt. 3320 // __f64_from_i64(x) -> f64 bits (cvtsi2sd); __f64_to_i64(f) -> truncated int 3321 // (cvttsd2si); __f64_sqrt(x) -> sqrt (sqrtsd). The backend picks double 3322 // precision from the TY_F64 result (or, for _to_i64, from the f64 operand). 3323 if streq_n(name, "__f64_from_i64", 14) == 1 { 3324 if peek_kind(P) == TK_LPAREN { 3325 advance_tok(P) 3326 let a_v: i64 = parse_expr(P) 3327 match_kind(P, TK_RPAREN) 3328 return ir_emit_f64_unop(P.current_bb, OP_FCAST_I_TO_F, a_v) 3329 } 3330 } 3331 if streq_n(name, "__f64_to_i64", 12) == 1 { 3332 if peek_kind(P) == TK_LPAREN { 3333 advance_tok(P) 3334 let a_v: i64 = parse_expr(P) 3335 match_kind(P, TK_RPAREN) 3336 return ir_emit_f32_unop(P.current_bb, OP_FCAST_F_TO_I, a_v) 3337 } 3338 } 3339 if streq_n(name, "__f64_sqrt", 10) == 1 { 3340 if peek_kind(P) == TK_LPAREN { 3341 advance_tok(P) 3342 let a_v: i64 = parse_expr(P) 3343 match_kind(P, TK_RPAREN) 3344 return ir_emit_f64_unop(P.current_bb, OP_FSQRT, a_v) 3345 } 3346 } 3347 // Hardware AES-NI: __aes128_enc_block(state_ptr, roundkeys_ptr) encrypts the 16-byte 3348 // block at state_ptr IN PLACE using the 11 expanded round keys (176 bytes) at 3349 // roundkeys_ptr. Lowered to movdqu + pxor + aesenc x9 + aesenclast (state in %xmm0). 3350 if streq_n(name, "__aes128_enc_block", 18) == 1 { 3351 if peek_kind(P) == TK_LPAREN { 3352 advance_tok(P) 3353 let a_v: i64 = parse_expr(P) 3354 match_kind(P, TK_COMMA) 3355 let b_v: i64 = parse_expr(P) 3356 match_kind(P, TK_RPAREN) 3357 return ir_emit_f32_binop(P.current_bb, OP_AES128_ENC_BLOCK, a_v, b_v) 3358 } 3359 } 3360 // Hardware SHA-NI: __sha256_ni_block(state_ptr, block_ptr, k_ptr) runs ONE full SHA-256 block 3361 // compression IN PLACE. state_ptr -> 8 contiguous u32 (working state a..h == h0..h7); block_ptr 3362 // -> 64 raw big-endian message bytes; k_ptr -> 64 contiguous u32 round constants K[0..63]. 3363 // Lowered to the Intel SHA extension sequence (punpck/pshufd state arrange, pshufb byte-swap, 3364 // 16x sha256msg1/msg2 + 2x sha256rnds2). The software sha256_compress stays the oracle/fallback; 3365 // callers gate on __cpuid_ebx(7,0) bit-29 (SHA). 3 operands (op0/op1/op2), i64 result (0). 3366 if streq_n(name, "__sha256_ni_block", 17) == 1 { 3367 if peek_kind(P) == TK_LPAREN { 3368 advance_tok(P) 3369 let sa: i64 = parse_expr(P) 3370 match_kind(P, TK_COMMA) 3371 let sb: i64 = parse_expr(P) 3372 match_kind(P, TK_COMMA) 3373 let sk: i64 = parse_expr(P) 3374 match_kind(P, TK_RPAREN) 3375 return ir_emit_sha256_ni_block(P.current_bb, sa, sb, sk) 3376 } 3377 } 3378 // Q5_0 SSE unpack: __q5_unpack32(qhqs_ptr, out_i8_ptr, consts_ptr). 3379 if streq_n(name, "__q5_unpack32", 13) == 1 { 3380 if peek_kind(P) == TK_LPAREN { 3381 advance_tok(P) 3382 let u0: i64 = parse_expr(P) 3383 match_kind(P, TK_COMMA) 3384 let u1: i64 = parse_expr(P) 3385 match_kind(P, TK_COMMA) 3386 let u2: i64 = parse_expr(P) 3387 match_kind(P, TK_RPAREN) 3388 return ir_emit_q5unpack32(P.current_bb, u0, u1, u2) 3389 } 3390 } 3391 // Q4_K AVX2 unpack-and-scale: __q4k_unpack32s(qs_ptr, out_i16x64_ptr, sc_lo | sc_hi<<16). 3392 if streq_n(name, "__q4k_unpack32s", 15) == 1 { 3393 if peek_kind(P) == TK_LPAREN { 3394 advance_tok(P) 3395 let k0: i64 = parse_expr(P) 3396 match_kind(P, TK_COMMA) 3397 let k1: i64 = parse_expr(P) 3398 match_kind(P, TK_COMMA) 3399 let k2: i64 = parse_expr(P) 3400 match_kind(P, TK_RPAREN) 3401 return ir_emit_q4kunpack32s(P.current_bb, k0, k1, k2) 3402 } 3403 } 3404 // LN28 (2026-09-03): __q4k_sb_dot(sb, col, scpre, out) -- ONE WHOLE Q4_K SUPER-BLOCK. 3405 // BUILT AND UNWIRED UNTIL TODAY, on BOTH trees: OP_Q4KSBDOT (nx_types), ir_emit_q4ksbdot (nx_ir) and 3406 // x86ctx_emit_q4ksbdot (nx_x86_64_ctx) all shipped, but nothing here routed the NAME, so every program 3407 // calling it refused with "unhandled __ intrinsic" -- 32 of them, found by the whole-population census. 3408 // ★★★★★★AN INTRINSIC IS NOT SHIPPED UNTIL ITS NAME REACHES ITS EMITTER: THE OPCODE, THE IR BUILDER AND 3409 // THE CODEGEN CAN ALL EXIST AND THE FEATURE STILL BE UNREACHABLE FROM EVERY PROGRAM. 3410 if streq_n(name, "__q4k_sb_dot", 12) == 1 { 3411 if peek_kind(P) == TK_LPAREN { 3412 advance_tok(P) 3413 let q0: i64 = parse_expr(P) 3414 match_kind(P, TK_COMMA) 3415 let q1: i64 = parse_expr(P) 3416 match_kind(P, TK_COMMA) 3417 let q2: i64 = parse_expr(P) 3418 match_kind(P, TK_COMMA) 3419 let q3: i64 = parse_expr(P) 3420 match_kind(P, TK_RPAREN) 3421 return ir_emit_q4ksbdot(P.current_bb, q0, q1, q2, q3) 3422 } 3423 } 3424 // Fused wide multiply: __mul256_wide(dst_ptr, a_ptr, b_ptr) computes the 512-bit product 3425 // of the 256-bit little-endian integers *a * *b into *dst (8 x u64) via the ADX/BMI2 3426 // mulx+adcx+adox dual-carry kernel. IN PLACE write to dst; i64 result (0). 3427 if streq_n(name, "__mul256_wide", 13) == 1 { 3428 if peek_kind(P) == TK_LPAREN { 3429 advance_tok(P) 3430 let md: i64 = parse_expr(P) 3431 match_kind(P, TK_COMMA) 3432 let ma: i64 = parse_expr(P) 3433 match_kind(P, TK_COMMA) 3434 let mb: i64 = parse_expr(P) 3435 match_kind(P, TK_RPAREN) 3436 return ir_emit_mul256_wide(P.current_bb, md, ma, mb) 3437 } 3438 } 3439 // Hardware CLMUL (PCLMULQDQ): __clmul_XY(p, q) carry-less-multiplies the X half of *p 3440 // by the Y half of *q (X,Y in {l,h}) and writes the 128-bit product back to *p in place. 3441 // The four variants are the half-products of a 128x128 GF(2) multiply -- the GHASH core. 3442 if streq_n(name, "__clmul_ll", 10) == 1 { 3443 if peek_kind(P) == TK_LPAREN { 3444 advance_tok(P) 3445 let a_v: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3446 let b_v: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3447 return ir_emit_f32_binop(P.current_bb, OP_CLMUL_LL, a_v, b_v) 3448 } 3449 } 3450 if streq_n(name, "__clmul_hh", 10) == 1 { 3451 if peek_kind(P) == TK_LPAREN { 3452 advance_tok(P) 3453 let a_v: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3454 let b_v: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3455 return ir_emit_f32_binop(P.current_bb, OP_CLMUL_HH, a_v, b_v) 3456 } 3457 } 3458 if streq_n(name, "__clmul_lh", 10) == 1 { 3459 if peek_kind(P) == TK_LPAREN { 3460 advance_tok(P) 3461 let a_v: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3462 let b_v: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3463 return ir_emit_f32_binop(P.current_bb, OP_CLMUL_LH, a_v, b_v) 3464 } 3465 } 3466 if streq_n(name, "__clmul_hl", 10) == 1 { 3467 if peek_kind(P) == TK_LPAREN { 3468 advance_tok(P) 3469 let a_v: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3470 let b_v: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3471 return ir_emit_f32_binop(P.current_bb, OP_CLMUL_HL, a_v, b_v) 3472 } 3473 } 3474 if streq_n(name, "__f32_add", 9) == 1 { 3475 if peek_kind(P) == TK_LPAREN { 3476 advance_tok(P) 3477 let a_v: i64 = parse_expr(P) 3478 match_kind(P, TK_COMMA) 3479 let b_v: i64 = parse_expr(P) 3480 match_kind(P, TK_RPAREN) 3481 return ir_emit_f32_binop(P.current_bb, OP_FADD, a_v, b_v) 3482 } 3483 } 3484 if streq_n(name, "__f32_mul", 9) == 1 { 3485 if peek_kind(P) == TK_LPAREN { 3486 advance_tok(P) 3487 let a_v: i64 = parse_expr(P) 3488 match_kind(P, TK_COMMA) 3489 let b_v: i64 = parse_expr(P) 3490 match_kind(P, TK_RPAREN) 3491 return ir_emit_f32_binop(P.current_bb, OP_FMUL, a_v, b_v) 3492 } 3493 } 3494 if streq_n(name, "__f32_div", 9) == 1 { 3495 if peek_kind(P) == TK_LPAREN { 3496 advance_tok(P) 3497 let a_v: i64 = parse_expr(P) 3498 match_kind(P, TK_COMMA) 3499 let b_v: i64 = parse_expr(P) 3500 match_kind(P, TK_RPAREN) 3501 return ir_emit_f32_binop(P.current_bb, OP_FDIV, a_v, b_v) 3502 } 3503 } 3504 // Widening SIMD dot product i16x16 -> i64 scalar. v0.0.1 shape: 3505 // takes two *i64 pointers (each addressing 4 packed i64 words = 3506 // 16 i16 lanes), does load-load-dot in one IR op. Self-host 3507 // SIMD surface first land -- bits-up from nothing. Codegen in 3508 // nx_riscv.nx lowers to vsetvli e16 m1 + vle16 + vwmul.vv + 3509 // vwredsum.vs. 3510 if streq_n(name, "__simd_vdot_i16_x16", 19) == 1 { 3511 if peek_kind(P) == TK_LPAREN { 3512 advance_tok(P) 3513 let a_v: i64 = parse_expr(P) 3514 match_kind(P, TK_COMMA) 3515 let b_v: i64 = parse_expr(P) 3516 match_kind(P, TK_RPAREN) 3517 return ir_emit_simd_vdot_i16_x16(P.current_bb, a_v, b_v) 3518 } 3519 } 3520 // Packed f32x4 dot product: __f32x4_dot(a_ptr, b_ptr) where each ptr addresses 4 CONTIGUOUS 3521 // 4-byte f32 -> their dot as an f32 scalar (i64-carried bits). x86 SSE movups+mulps + scalar 3522 // horizontal-sum (the compute-physics lever: 4 multiplies in one mulps vs 4 scalar mulss). 3523 if streq_n(name, "__f32x4_dot", 11) == 1 { 3524 if peek_kind(P) == TK_LPAREN { 3525 advance_tok(P) 3526 let fa_v: i64 = parse_expr(P) 3527 match_kind(P, TK_COMMA) 3528 let fb_v: i64 = parse_expr(P) 3529 match_kind(P, TK_RPAREN) 3530 return ir_emit_f32_binop(P.current_bb, OP_F32X4_DOT, fa_v, fb_v) 3531 } 3532 } 3533 // Q8_0/quantized dequant-dot lever: __f32_i8dot32(a:*i8[32], b:*f32[32]) 3534 // -> f32 = dot of 32 sign-extended int8 with 32 f32 (SSE unrolled). 3535 // Monolithic Q8_0 row dot: __f32_q8row_dot(qbuf_row, a_row, nblocks). 3536 if streq_n(name, "__f32_q8row_dot", 15) == 1 { 3537 if peek_kind(P) == TK_LPAREN { 3538 advance_tok(P) 3539 let rq: i64 = parse_expr(P) 3540 match_kind(P, TK_COMMA) 3541 let ra: i64 = parse_expr(P) 3542 match_kind(P, TK_COMMA) 3543 let rn: i64 = parse_expr(P) 3544 match_kind(P, TK_RPAREN) 3545 return ir_emit_q8rowdot(P.current_bb, rq, ra, rn) 3546 } 3547 } 3548 // Deferred-hsum FMA block: __f32_i8fma32(a,b,d_bits,acc_ptr) -> acc += d*(a.b). 3549 if streq_n(name, "__f32_i8fma32", 13) == 1 { 3550 if peek_kind(P) == TK_LPAREN { 3551 advance_tok(P) 3552 let fa: i64 = parse_expr(P) 3553 match_kind(P, TK_COMMA) 3554 let fb: i64 = parse_expr(P) 3555 match_kind(P, TK_COMMA) 3556 let fd: i64 = parse_expr(P) 3557 match_kind(P, TK_COMMA) 3558 let fac: i64 = parse_expr(P) 3559 match_kind(P, TK_RPAREN) 3560 return ir_emit_i8fma32(P.current_bb, fa, fb, fd, fac) 3561 } 3562 } 3563 // AVX2 twin -- MUST be checked before __f32_i8dot32 (its 14-char name 3564 // shares the first 13 chars, so the 13-char streq_n would swallow it). 3565 if streq_n(name, "__f32_i8dot32a", 14) == 1 { 3566 if peek_kind(P) == TK_LPAREN { 3567 advance_tok(P) 3568 let aa_v: i64 = parse_expr(P) 3569 match_kind(P, TK_COMMA) 3570 let ab_v: i64 = parse_expr(P) 3571 match_kind(P, TK_RPAREN) 3572 return ir_emit_f32_binop(P.current_bb, OP_I8DOT32A, aa_v, ab_v) 3573 } 3574 } 3575 if streq_n(name, "__f32_i8dot32", 13) == 1 { 3576 if peek_kind(P) == TK_LPAREN { 3577 advance_tok(P) 3578 let ia_v: i64 = parse_expr(P) 3579 match_kind(P, TK_COMMA) 3580 let ib_v: i64 = parse_expr(P) 3581 match_kind(P, TK_RPAREN) 3582 return ir_emit_f32_binop(P.current_bb, OP_I8DOT32, ia_v, ib_v) 3583 } 3584 } 3585 // Packed f32x8 dot product: __f32x8_dot(a_ptr, b_ptr), each ptr -> 8 CONTIGUOUS 4-byte f32 -> 3586 // their dot as f32 scalar. x86 AVX2: vmovups+vmulps (8 lanes/instr) + vextractf128 + SSE hsum. 3587 if streq_n(name, "__f32x8_dot", 11) == 1 { 3588 if peek_kind(P) == TK_LPAREN { 3589 advance_tok(P) 3590 let ga_v: i64 = parse_expr(P) 3591 match_kind(P, TK_COMMA) 3592 let gb_v: i64 = parse_expr(P) 3593 match_kind(P, TK_RPAREN) 3594 return ir_emit_f32_binop(P.current_bb, OP_F32X8_DOT, ga_v, gb_v) 3595 } 3596 } 3597 // FMA vector-accumulate: __f32x8_fma(acc_ptr, a_ptr, b_ptr) -> *acc += a*b (8-wide fused, no hsum) 3598 if streq_n(name, "__f32x8_fma", 11) == 1 { 3599 if peek_kind(P) == TK_LPAREN { 3600 advance_tok(P) 3601 let fac: i64 = parse_expr(P) 3602 match_kind(P, TK_COMMA) 3603 let faa: i64 = parse_expr(P) 3604 match_kind(P, TK_COMMA) 3605 let fab: i64 = parse_expr(P) 3606 match_kind(P, TK_RPAREN) 3607 return ir_emit_f32x8_fma(P.current_bb, fac, faa, fab) 3608 } 3609 } 3610 // horizontal sum of an 8-wide accumulator: __f32x8_hsum(acc_ptr) -> f32 (called ONCE per dot) 3611 if streq_n(name, "__f32x8_hsum", 12) == 1 { 3612 if peek_kind(P) == TK_LPAREN { 3613 advance_tok(P) 3614 let fhc: i64 = parse_expr(P) 3615 match_kind(P, TK_RPAREN) 3616 return ir_emit_f32_unop(P.current_bb, OP_F32X8_HSUM, fhc) 3617 } 3618 } 3619 // NO-FLOAT whole-chunk integer dot: __i16_dot(a_ptr, b_ptr, n_lanes) -> i64 = sum a[i]*b[i], accumulator register-resident (R0r-b) 3620 if streq_n(name, "__i16_dot", 9) == 1 { 3621 if peek_kind(P) == TK_LPAREN { 3622 advance_tok(P) 3623 let dpa: i64 = parse_expr(P) 3624 match_kind(P, TK_COMMA) 3625 let dpb: i64 = parse_expr(P) 3626 match_kind(P, TK_COMMA) 3627 let dpn: i64 = parse_expr(P) 3628 match_kind(P, TK_RPAREN) 3629 return ir_emit_i16dot(P.current_bb, dpa, dpb, dpn) 3630 } 3631 } 3632 // R0s-b: one Q8_0 block's int8 x i16 dot: __q8blk_i16dot(codes_ptr, x_ptr) -> i64 = sum codes[i]*x[i] over 32 lanes 3633 if streq_n(name, "__q8blk_i16dot", 14) == 1 { 3634 if peek_kind(P) == TK_LPAREN { 3635 advance_tok(P) 3636 let qba: i64 = parse_expr(P) 3637 match_kind(P, TK_COMMA) 3638 let qbb: i64 = parse_expr(P) 3639 match_kind(P, TK_RPAREN) 3640 return ir_emit_q8blkdot(P.current_bb, qba, qbb) 3641 } 3642 } 3643 // NO-FLOAT integer dot accumulate: __i16x16_madd(acc_ptr, a_ptr, b_ptr) -> *acc(i32x8) += vpmaddwd(a,b) 3644 if streq_n(name, "__i16x16_madd", 13) == 1 { 3645 if peek_kind(P) == TK_LPAREN { 3646 advance_tok(P) 3647 let mac: i64 = parse_expr(P) 3648 match_kind(P, TK_COMMA) 3649 let maa: i64 = parse_expr(P) 3650 match_kind(P, TK_COMMA) 3651 let mab: i64 = parse_expr(P) 3652 match_kind(P, TK_RPAREN) 3653 return ir_emit_i16x16_madd(P.current_bb, mac, maa, mab) 3654 } 3655 } 3656 if streq_n(name, "__simd_vreduce_min_i16_x16", 26) == 1 { 3657 if peek_kind(P) == TK_LPAREN { 3658 advance_tok(P) 3659 let pmn: i64 = parse_expr(P) 3660 match_kind(P, TK_RPAREN) 3661 return ir_emit_simd_vreduce_min_i16_x16(P.current_bb, pmn) 3662 } 3663 } 3664 if streq_n(name, "__simd_vreduce_max_i16_x16", 26) == 1 { 3665 if peek_kind(P) == TK_LPAREN { 3666 advance_tok(P) 3667 let pmx: i64 = parse_expr(P) 3668 match_kind(P, TK_RPAREN) 3669 return ir_emit_simd_vreduce_max_i16_x16(P.current_bb, pmx) 3670 } 3671 } 3672 if streq_n(name, "__simd_vsadd_i16_x16", 20) == 1 { 3673 if peek_kind(P) == TK_LPAREN { 3674 advance_tok(P) 3675 let asa: i64 = parse_expr(P) 3676 match_kind(P, TK_COMMA) 3677 let bsa: i64 = parse_expr(P) 3678 match_kind(P, TK_COMMA) 3679 let osa: i64 = parse_expr(P) 3680 match_kind(P, TK_RPAREN) 3681 return ir_emit_simd_vsadd_i16_x16(P.current_bb, asa, bsa, osa) 3682 } 3683 } 3684 if streq_n(name, "__simd_vssub_i16_x16", 20) == 1 { 3685 if peek_kind(P) == TK_LPAREN { 3686 advance_tok(P) 3687 let ass: i64 = parse_expr(P) 3688 match_kind(P, TK_COMMA) 3689 let bss: i64 = parse_expr(P) 3690 match_kind(P, TK_COMMA) 3691 let oss: i64 = parse_expr(P) 3692 match_kind(P, TK_RPAREN) 3693 return ir_emit_simd_vssub_i16_x16(P.current_bb, ass, bss, oss) 3694 } 3695 } 3696 if streq_n(name, "__simd_vsaddu_i16_x16", 21) == 1 { 3697 if peek_kind(P) == TK_LPAREN { 3698 advance_tok(P) 3699 let asau: i64 = parse_expr(P) 3700 match_kind(P, TK_COMMA) 3701 let bsau: i64 = parse_expr(P) 3702 match_kind(P, TK_COMMA) 3703 let osau: i64 = parse_expr(P) 3704 match_kind(P, TK_RPAREN) 3705 return ir_emit_simd_vsaddu_i16_x16(P.current_bb, asau, bsau, osau) 3706 } 3707 } 3708 if streq_n(name, "__simd_vssubu_i16_x16", 21) == 1 { 3709 if peek_kind(P) == TK_LPAREN { 3710 advance_tok(P) 3711 let assu: i64 = parse_expr(P) 3712 match_kind(P, TK_COMMA) 3713 let bssu: i64 = parse_expr(P) 3714 match_kind(P, TK_COMMA) 3715 let ossu: i64 = parse_expr(P) 3716 match_kind(P, TK_RPAREN) 3717 return ir_emit_simd_vssubu_i16_x16(P.current_bb, assu, bssu, ossu) 3718 } 3719 } 3720 // Per-lane min/max/add/sub/mul: 25-char builtins (vXXX_lane). 3721 if streq_n(name, "__simd_vmin_lane_i16_x16", 24) == 1 { 3722 if peek_kind(P) == TK_LPAREN { 3723 advance_tok(P) 3724 let amn: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3725 let bmn: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3726 let omn: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3727 return ir_emit_simd_vmin_lane_i16_x16(P.current_bb, amn, bmn, omn) 3728 } 3729 } 3730 if streq_n(name, "__simd_vmax_lane_i16_x16", 24) == 1 { 3731 if peek_kind(P) == TK_LPAREN { 3732 advance_tok(P) 3733 let amx: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3734 let bmx: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3735 let omx: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3736 return ir_emit_simd_vmax_lane_i16_x16(P.current_bb, amx, bmx, omx) 3737 } 3738 } 3739 if streq_n(name, "__simd_vadd_lane_i16_x16", 24) == 1 { 3740 if peek_kind(P) == TK_LPAREN { 3741 advance_tok(P) 3742 let aad: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3743 let bad: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3744 let oad: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3745 return ir_emit_simd_vadd_lane_i16_x16(P.current_bb, aad, bad, oad) 3746 } 3747 } 3748 if streq_n(name, "__simd_vsub_lane_i16_x16", 24) == 1 { 3749 if peek_kind(P) == TK_LPAREN { 3750 advance_tok(P) 3751 let asb: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3752 let bsb: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3753 let osb: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3754 return ir_emit_simd_vsub_lane_i16_x16(P.current_bb, asb, bsb, osb) 3755 } 3756 } 3757 if streq_n(name, "__simd_vmul_lane_i16_x16", 24) == 1 { 3758 if peek_kind(P) == TK_LPAREN { 3759 advance_tok(P) 3760 let aml: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3761 let bml: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3762 let oml: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3763 return ir_emit_simd_vmul_lane_i16_x16(P.current_bb, aml, bml, oml) 3764 } 3765 } 3766 // 3-arg shifts: __simd_vsll/vsrl/vsra_i16_x16(*i64 src, i64 count, *i64 out) 3767 if streq_n(name, "__simd_vsll_i16_x16", 19) == 1 { 3768 if peek_kind(P) == TK_LPAREN { 3769 advance_tok(P) 3770 let asl: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3771 let csl: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3772 let osl: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3773 return ir_emit_simd_vsll_i16_x16(P.current_bb, asl, csl, osl) 3774 } 3775 } 3776 if streq_n(name, "__simd_vsrl_i16_x16", 19) == 1 { 3777 if peek_kind(P) == TK_LPAREN { 3778 advance_tok(P) 3779 let asr: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3780 let csr: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3781 let osr: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3782 return ir_emit_simd_vsrl_i16_x16(P.current_bb, asr, csr, osr) 3783 } 3784 } 3785 if streq_n(name, "__simd_vsra_i16_x16", 19) == 1 { 3786 if peek_kind(P) == TK_LPAREN { 3787 advance_tok(P) 3788 let asa2: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3789 let csa2: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3790 let osa2: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3791 return ir_emit_simd_vsra_i16_x16(P.current_bb, asa2, csa2, osa2) 3792 } 3793 } 3794 if streq_n(name, "__simd_vreduce_sum_i16_x16", 26) == 1 { 3795 if peek_kind(P) == TK_LPAREN { 3796 advance_tok(P) 3797 let prs: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3798 return ir_emit_simd_vreduce_sum_i16_x16(P.current_bb, prs) 3799 } 3800 } 3801 if streq_n(name, "__simd_vbroadcast_i16_x16", 25) == 1 { 3802 if peek_kind(P) == TK_LPAREN { 3803 advance_tok(P) 3804 let sbc: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3805 let obc: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3806 return ir_emit_simd_vbroadcast_i16_x16(P.current_bb, sbc, obc) 3807 } 3808 } 3809 // i8x32 set 3810 if streq_n(name, "__simd_vadd_i8_x32", 18) == 1 { 3811 if peek_kind(P) == TK_LPAREN { 3812 advance_tok(P) 3813 let a8a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3814 let b8a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3815 let o8a: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3816 return ir_emit_simd_vadd_i8_x32(P.current_bb, a8a, b8a, o8a) 3817 } 3818 } 3819 if streq_n(name, "__simd_vsub_i8_x32", 18) == 1 { 3820 if peek_kind(P) == TK_LPAREN { 3821 advance_tok(P) 3822 let a8s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3823 let b8s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3824 let o8s: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3825 return ir_emit_simd_vsub_i8_x32(P.current_bb, a8s, b8s, o8s) 3826 } 3827 } 3828 if streq_n(name, "__simd_vsadd_i8_x32", 19) == 1 { 3829 if peek_kind(P) == TK_LPAREN { 3830 advance_tok(P) 3831 let a8sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3832 let b8sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3833 let o8sa: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3834 return ir_emit_simd_vsadd_i8_x32(P.current_bb, a8sa, b8sa, o8sa) 3835 } 3836 } 3837 if streq_n(name, "__simd_vssub_i8_x32", 19) == 1 { 3838 if peek_kind(P) == TK_LPAREN { 3839 advance_tok(P) 3840 let a8ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3841 let b8ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3842 let o8ss: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3843 return ir_emit_simd_vssub_i8_x32(P.current_bb, a8ss, b8ss, o8ss) 3844 } 3845 } 3846 if streq_n(name, "__simd_vreduce_sum_i8_x32", 25) == 1 { 3847 if peek_kind(P) == TK_LPAREN { 3848 advance_tok(P) 3849 let p8r: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3850 return ir_emit_simd_vreduce_sum_i8_x32(P.current_bb, p8r) 3851 } 3852 } 3853 if streq_n(name, "__simd_vbroadcast_i8_x32", 24) == 1 { 3854 if peek_kind(P) == TK_LPAREN { 3855 advance_tok(P) 3856 let s8b: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3857 let o8b: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3858 return ir_emit_simd_vbroadcast_i8_x32(P.current_bb, s8b, o8b) 3859 } 3860 } 3861 // i32x8 set: names 17-24 chars long. 3862 if streq_n(name, "__simd_vadd_i32_x8", 18) == 1 { 3863 if peek_kind(P) == TK_LPAREN { 3864 advance_tok(P) 3865 let a32a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3866 let b32a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3867 let o32a: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3868 return ir_emit_simd_vadd_i32_x8(P.current_bb, a32a, b32a, o32a) 3869 } 3870 } 3871 if streq_n(name, "__simd_vsub_i32_x8", 18) == 1 { 3872 if peek_kind(P) == TK_LPAREN { 3873 advance_tok(P) 3874 let a32s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3875 let b32s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3876 let o32s: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3877 return ir_emit_simd_vsub_i32_x8(P.current_bb, a32s, b32s, o32s) 3878 } 3879 } 3880 if streq_n(name, "__simd_vmul_i32_x8", 18) == 1 { 3881 if peek_kind(P) == TK_LPAREN { 3882 advance_tok(P) 3883 let a32m: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3884 let b32m: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3885 let o32m: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3886 return ir_emit_simd_vmul_i32_x8(P.current_bb, a32m, b32m, o32m) 3887 } 3888 } 3889 if streq_n(name, "__simd_vsadd_i32_x8", 19) == 1 { 3890 if peek_kind(P) == TK_LPAREN { 3891 advance_tok(P) 3892 let a32sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3893 let b32sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3894 let o32sa: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3895 return ir_emit_simd_vsadd_i32_x8(P.current_bb, a32sa, b32sa, o32sa) 3896 } 3897 } 3898 if streq_n(name, "__simd_vssub_i32_x8", 19) == 1 { 3899 if peek_kind(P) == TK_LPAREN { 3900 advance_tok(P) 3901 let a32ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3902 let b32ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3903 let o32ss: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3904 return ir_emit_simd_vssub_i32_x8(P.current_bb, a32ss, b32ss, o32ss) 3905 } 3906 } 3907 if streq_n(name, "__simd_vreduce_sum_i32_x8", 25) == 1 { 3908 if peek_kind(P) == TK_LPAREN { 3909 advance_tok(P) 3910 let p32r: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3911 return ir_emit_simd_vreduce_sum_i32_x8(P.current_bb, p32r) 3912 } 3913 } 3914 if streq_n(name, "__simd_vbroadcast_i32_x8", 24) == 1 { 3915 if peek_kind(P) == TK_LPAREN { 3916 advance_tok(P) 3917 let s32b: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3918 let o32b: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3919 return ir_emit_simd_vbroadcast_i32_x8(P.current_bb, s32b, o32b) 3920 } 3921 } 3922 // i64x4 set 3923 if streq_n(name, "__simd_vadd_i64_x4", 18) == 1 { 3924 if peek_kind(P) == TK_LPAREN { 3925 advance_tok(P) 3926 let a64a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3927 let b64a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3928 let o64a: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3929 return ir_emit_simd_vadd_i64_x4(P.current_bb, a64a, b64a, o64a) 3930 } 3931 } 3932 if streq_n(name, "__simd_vsub_i64_x4", 18) == 1 { 3933 if peek_kind(P) == TK_LPAREN { 3934 advance_tok(P) 3935 let a64s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3936 let b64s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3937 let o64s: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3938 return ir_emit_simd_vsub_i64_x4(P.current_bb, a64s, b64s, o64s) 3939 } 3940 } 3941 if streq_n(name, "__simd_vmul_i64_x4", 18) == 1 { 3942 if peek_kind(P) == TK_LPAREN { 3943 advance_tok(P) 3944 let a64m: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3945 let b64m: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3946 let o64m: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3947 return ir_emit_simd_vmul_i64_x4(P.current_bb, a64m, b64m, o64m) 3948 } 3949 } 3950 if streq_n(name, "__simd_vsadd_i64_x4", 19) == 1 { 3951 if peek_kind(P) == TK_LPAREN { 3952 advance_tok(P) 3953 let a64sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3954 let b64sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3955 let o64sa: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3956 return ir_emit_simd_vsadd_i64_x4(P.current_bb, a64sa, b64sa, o64sa) 3957 } 3958 } 3959 if streq_n(name, "__simd_vssub_i64_x4", 19) == 1 { 3960 if peek_kind(P) == TK_LPAREN { 3961 advance_tok(P) 3962 let a64ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3963 let b64ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3964 let o64ss: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3965 return ir_emit_simd_vssub_i64_x4(P.current_bb, a64ss, b64ss, o64ss) 3966 } 3967 } 3968 if streq_n(name, "__simd_vreduce_sum_i64_x4", 25) == 1 { 3969 if peek_kind(P) == TK_LPAREN { 3970 advance_tok(P) 3971 let p64r: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3972 return ir_emit_simd_vreduce_sum_i64_x4(P.current_bb, p64r) 3973 } 3974 } 3975 if streq_n(name, "__simd_vbroadcast_i64_x4", 24) == 1 { 3976 if peek_kind(P) == TK_LPAREN { 3977 advance_tok(P) 3978 let s64b: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3979 let o64b: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3980 return ir_emit_simd_vbroadcast_i64_x4(P.current_bb, s64b, o64b) 3981 } 3982 } 3983 // Bit-rotate builtins -- C bootstrap parity (parse.c OP_ROTL64 / 3984 // OP_ROTR64). __rotl64(value, count) / __rotr64(value, count). 3985 // Lowered to x86 ROLQ/RORQ %cl (single instruction). Without 3986 // these the call fell through to an unresolved function and 3987 // silently returned op0 -- a no-op rotate that broke SHA-512. 3988 if streq_n(name, "__rotl64", 8) == 1 { 3989 if peek_kind(P) == TK_LPAREN { 3990 advance_tok(P) 3991 let rlv: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3992 let rln: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3993 return ir_emit_binop(P.current_bb, OP_ROTL64, rlv, rln, ir_type_i64()) 3994 } 3995 } 3996 if streq_n(name, "__rotr64", 8) == 1 { 3997 if peek_kind(P) == TK_LPAREN { 3998 advance_tok(P) 3999 let rrv: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4000 let rrn: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4001 return ir_emit_binop(P.current_bb, OP_ROTR64, rrv, rrn, ir_type_i64()) 4002 } 4003 } 4004 // LN1 wrap-around-by-intent intrinsics: __wrap_add/__wrap_sub/__wrap_mul(a, b) emit the plain 4005 // binop and are EXEMPT from the --chkarith overflow check (the Zig +% / Rust wrapping_* contract). 4006 // Under the default mode they are exactly `+ - *` (same int_binop_result_type ruler), so a 4007 // hash written with them builds byte-identically today and keeps building once the mode flips. 4008 if streq_n(name, "__wrap_add", 10) == 1 { 4009 if peek_kind(P) == TK_LPAREN { 4010 advance_tok(P) 4011 let wa1: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4012 let wa2: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4013 return ir_emit_binop(P.current_bb, OP_ADD, wa1, wa2, int_binop_result_type(P, wa1, wa2)) 4014 } 4015 } 4016 if streq_n(name, "__wrap_sub", 10) == 1 { 4017 if peek_kind(P) == TK_LPAREN { 4018 advance_tok(P) 4019 let ws1: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4020 let ws2: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4021 return ir_emit_binop(P.current_bb, OP_SUB, ws1, ws2, int_binop_result_type(P, ws1, ws2)) 4022 } 4023 } 4024 if streq_n(name, "__wrap_mul", 10) == 1 { 4025 if peek_kind(P) == TK_LPAREN { 4026 advance_tok(P) 4027 let wm1: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4028 let wm2: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4029 return ir_emit_binop(P.current_bb, OP_MUL, wm1, wm2, int_binop_result_type(P, wm1, wm2)) 4030 } 4031 } 4032 // LN4 OWNERSHIP MOVE: `__move(p)` is the DECLARED transfer point for an owned buffer -- the 4033 // __wrap_add contract, an intrinsic stating an intent the type system cannot express. It is a 4034 // PURE MARKER: it returns p's own value id and emits NO IR at all, so `f(__move(p))` is 4035 // byte-for-byte `f(p)` under BOTH modes and adding a move to real code costs nothing at runtime. 4036 // The identifier token is captured BEFORE parse_expr consumes it; parse_expr then runs the 4037 // ordinary read path, so moving an ALREADY moved or released name is refused by own_check_use 4038 // there rather than by a second rule here -- one use checker, not two. 4039 if streq_n(name, "__move", 6) == 1 { 4040 if peek_kind(P) == TK_LPAREN { 4041 advance_tok(P) 4042 let mv_tok: *Tok = tok_at(P.toks, P.pos) 4043 let mv_v: i64 = parse_expr(P) 4044 match_kind(P, TK_RPAREN) 4045 own_move_arg(P, mv_tok) 4046 return mv_v 4047 } 4048 } 4049 // Wide multiply -- G2 substrate. __umulhi64(a, b) = the HIGH 64 bits of the 4050 // unsigned 64x64 product (x86 mulq's rdx). Paired with normal `*` (the low 4051 // 64 bits) it yields the full 128-bit product, so crypto field arithmetic can 4052 // use 4x64-bit limbs (16 partial products) instead of 8x32 (64). 4053 if streq_n(name, "__umulhi64", 10) == 1 { 4054 if peek_kind(P) == TK_LPAREN { 4055 advance_tok(P) 4056 let uma: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4057 let umb: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4058 return ir_emit_binop(P.current_bb, OP_UMULHI, uma, umb, ir_type_i64()) 4059 } 4060 } 4061 // Hardware CRC-32C (SSE4.2) -- __crc32_u64(crc, data) folds the 64-bit 4062 // `data` word into the running `crc` accumulator (x86 crc32q). Pure, 4063 // 2 i64 operands -> i64. Checksums, packet validation, hash fingerprints. 4064 if streq_n(name, "__crc32_u64", 11) == 1 { 4065 if peek_kind(P) == TK_LPAREN { 4066 advance_tok(P) 4067 let crca: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4068 let crcd: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4069 return ir_emit_binop(P.current_bb, OP_CRC32, crca, crcd, ir_type_i64()) 4070 } 4071 } 4072 // BMI2 parallel bit DEPOSIT -- __pdep64(value, mask) scatters the low bits 4073 // of `value` into the set-bit positions of `mask` (x86 pdep). Pure, 2 i64 4074 // operands -> i64. varint-encode, bitboard-scatter, bit-interleave. 4075 if streq_n(name, "__pdep64", 8) == 1 { 4076 if peek_kind(P) == TK_LPAREN { 4077 advance_tok(P) 4078 let pdv: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4079 let pdm: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4080 return ir_emit_binop(P.current_bb, OP_PDEP, pdv, pdm, ir_type_i64()) 4081 } 4082 } 4083 // BMI2 parallel bit EXTRACT -- __pext64(value, mask) gathers the `value` 4084 // bits at the set positions of `mask` down to the low bits (x86 pext; the 4085 // inverse of pdep). Pure, 2 i64 operands -> i64. varint-decode, 4086 // bitboard-gather, unicode-transcode, compression. 4087 if streq_n(name, "__pext64", 8) == 1 { 4088 if peek_kind(P) == TK_LPAREN { 4089 advance_tok(P) 4090 let pxv: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4091 let pxm: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4092 return ir_emit_binop(P.current_bb, OP_PEXT, pxv, pxm, ir_type_i64()) 4093 } 4094 } 4095 // Native add-with-carry -- G3 substrate. __adc_acc(acc_ptr, lo, hi) adds the 4096 // 128-bit (hi:lo) into the 3-word accumulator {acc[0],acc[1],acc[2]} with carry 4097 // (contiguous addq;adcq;adcq). Void/statement-form (returns const 0). 4098 if streq_n(name, "__adc_acc", 9) == 1 { 4099 if peek_kind(P) == TK_LPAREN { 4100 advance_tok(P) 4101 let aa_p: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4102 let aa_lo: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4103 let aa_hi: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4104 ir_emit_adc_acc(P.current_bb, aa_p, aa_lo, aa_hi) 4105 return safe_const_i64(P, 0, "parse.nx:adc-acc-0" as *u8) 4106 } 4107 } 4108 // CPU feature detection -- the BMI2/ADX gate. __cpuid_ebx(leaf, subleaf) runs 4109 // x86 cpuid and returns the EBX register (zero-extended). cpuid(7,0):EBX has 4110 // bit-8=BMI2, bit-19=ADX. Pure (deterministic for a given CPU). 4111 if streq_n(name, "__cpuid_ebx", 11) == 1 { 4112 if peek_kind(P) == TK_LPAREN { 4113 advance_tok(P) 4114 let cq_l: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4115 let cq_s: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4116 return ir_emit_binop(P.current_bb, OP_CPUID_EBX, cq_l, cq_s, ir_type_i64()) 4117 } 4118 } 4119 // Scalar bit unops -- C bootstrap parity (parse.c OP_BSWAP64/CLZ32/ 4120 // CTZ32/POPCNT64). Each takes exactly 1 argument. 4121 if streq_n(name, "__bswap64", 9) == 1 { 4122 if peek_kind(P) == TK_LPAREN { 4123 advance_tok(P) 4124 let bsv: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4125 return ir_emit_unop(P.current_bb, OP_BSWAP64, bsv, ir_type_i64()) 4126 } 4127 } 4128 if streq_n(name, "__clz32", 7) == 1 { 4129 if peek_kind(P) == TK_LPAREN { 4130 advance_tok(P) 4131 let czv: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4132 return ir_emit_unop(P.current_bb, OP_CLZ32, czv, ir_type_i64()) 4133 } 4134 } 4135 if streq_n(name, "__ctz32", 7) == 1 { 4136 if peek_kind(P) == TK_LPAREN { 4137 advance_tok(P) 4138 let tzv: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4139 return ir_emit_unop(P.current_bb, OP_CTZ32, tzv, ir_type_i64()) 4140 } 4141 } 4142 if streq_n(name, "__popcnt64", 10) == 1 { 4143 if peek_kind(P) == TK_LPAREN { 4144 advance_tok(P) 4145 let pcv: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4146 return ir_emit_unop(P.current_bb, OP_POPCNT64, pcv, ir_type_i64()) 4147 } 4148 } 4149 // __rdtsc() -- zero-arg cycle-counter read. The unop carries a dummy 4150 // const-0 operand (the lowering ignores it and reads the HW counter). 4151 if streq_n(name, "__rdtsc", 7) == 1 { 4152 if peek_kind(P) == TK_LPAREN { 4153 advance_tok(P); match_kind(P, TK_RPAREN) 4154 let rd0: i64 = safe_const_i64(P, 0, "__rdtsc" as *u8) 4155 return ir_emit_unop(P.current_bb, OP_RDTSC, rd0, ir_type_i64()) 4156 } 4157 } 4158 // Atomic intrinsics -- C bootstrap parity (parse.c). Memory order 4159 // is the last argument (NX_MO_* const). 4160 if streq_n(name, "__atomic_load_i64", 17) == 1 { 4161 if peek_kind(P) == TK_LPAREN { 4162 advance_tok(P) 4163 let al_a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4164 let al_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4165 return ir_emit_atomic_load_i64(P.current_bb, al_a, al_mo) 4166 } 4167 } 4168 if streq_n(name, "__atomic_store_i64", 18) == 1 { 4169 if peek_kind(P) == TK_LPAREN { 4170 advance_tok(P) 4171 let as_a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4172 let as_v: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4173 let as_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4174 ir_emit_atomic_store_i64(P.current_bb, as_a, as_v, as_mo) 4175 return safe_const_i64(P, 0, "parse.nx:atomic-store-0" as *u8) 4176 } 4177 } 4178 if streq_n(name, "__atomic_cas_i64", 16) == 1 { 4179 if peek_kind(P) == TK_LPAREN { 4180 advance_tok(P) 4181 let cs_a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4182 let cs_e: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4183 let cs_n: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4184 let cs_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4185 return ir_emit_atomic_cas_i64(P.current_bb, cs_a, cs_e, cs_n, cs_mo) 4186 } 4187 } 4188 if streq_n(name, "__atomic_faa_i64", 16) == 1 { 4189 if peek_kind(P) == TK_LPAREN { 4190 advance_tok(P) 4191 let fa_a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4192 let fa_d: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4193 let fa_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4194 return ir_emit_atomic_faa_i64(P.current_bb, fa_a, fa_d, fa_mo) 4195 } 4196 } 4197 if streq_n(name, "__atomic_fence", 14) == 1 { 4198 if peek_kind(P) == TK_LPAREN { 4199 advance_tok(P) 4200 let fe_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4201 ir_emit_atomic_fence(P.current_bb, fe_mo) 4202 return safe_const_i64(P, 0, "parse.nx:atomic-fence-0" as *u8) 4203 } 4204 } 4205 // __thread_clone(stack_top, entry_fn, ctx) -> child_tid. 4206 if streq_n(name, "__thread_clone", 14) == 1 { 4207 if peek_kind(P) == TK_LPAREN { 4208 advance_tok(P) 4209 let tc_s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4210 let tc_e: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4211 let tc_c: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4212 return ir_emit_thread_clone(P.current_bb, tc_s, tc_e, tc_c) 4213 } 4214 } 4215 return -1 4216} 4217 4218func parse_primary_call(P: *Parser, name: *u8) -> i64 { 4219 // CALL-SITE ANCHOR (2026-08-05). Capture the position of the CALL ITSELF at entry: the caller 4220 // already consumed the identifier, so it sits one token back. Reading P.pos at ERROR time 4221 // instead points at whatever the parser reached AFTER the call -- measured: the caret printed 4222 // under the NEXT statement (sys_exit(0)) for an error on the line above it. 4223 // A CARET THAT POINTS AT THE WRONG LINE IS WORSE THAN NO CARET: it sends the reader to 4224 // innocent code, which is exactly the confusion the feature exists to remove. 4225 var call_ln: i64 = 0 4226 var call_cl: i64 = 0 4227 if P.pos >= 1 { 4228 let ct: *Tok = tok_at(P.toks, P.pos - 1) 4229 call_ln = ct.line 4230 call_cl = ct.col 4231 } 4232 advance_tok(P) 4233 // 24 slots -- Instr has op0..op23 (24 operand slots after the 4234 // 2026-06-18 IR extension to support >16-arg calls, e.g. bi-predicted 4235 // H.264 chroma reconstruction). Was 4 then 8 then 16 slots; silent 4236 // overflow past the buffer corrupted adjacent memory and made 4237 // ir_emit_call read garbage into the high op slots. See PREVENT lint 4238 // bench/nx_call_arity_overflow_audit.sh. 4239 let args_raw: *u8 = sys_mmap(24 * 8 + 16) 4240 let args: *i64 = args_raw as *i64 4241 var n_args: i64 = 0 4242 // LN2 narrowing by assertion: `nx_assert_ptr(p, ...)` / `nx_assert_ptr(p as *u8, ...)` aborts 4243 // when p is null, so p is non-null for the rest of the block. And `sys_exit(...)` ends the path 4244 // (the early-exit idiom `if p == 0 { sys_exit(1) }`). Token shape only; inert unless --optenforce. 4245 if g_optenforce_live == 1 { 4246 if streq_n(name, "nx_assert_ptr", 13) == 1 { 4247 let oa0: *Tok = tok_at(P.toks, P.pos) 4248 let oa1: *Tok = tok_at(P.toks, P.pos + 1) 4249 if oa0.kind == TK_IDENT { 4250 var oa_ok: i64 = 0 4251 if oa1.kind == TK_COMMA { oa_ok = 1 } 4252 if oa1.kind == TK_AS { oa_ok = 1 } 4253 if oa_ok == 1 { 4254 let oaL: *Local = find_local(P, tok_text_ptr(oa0)) 4255 if oaL != (0 as *Local) { if oaL.ty != (0 as *Type) { if oaL.ty.kind == TY_PTR { oaL.nn_checked = 1 } } } 4256 } 4257 } 4258 } 4259 if streq_n(name, "sys_exit", 8) == 1 { g_oe_term = 1 } 4260 } 4261 // LN5 RELEASE POINT: `sys_munmap(p, n)` / `sys_munmap(p as *T, n)` ends p's ownership. The token 4262 // is captured HERE, BEFORE the arguments are parsed, and the local is marked AFTER the closing 4263 // paren -- an ordering that is load-bearing twice over. Capturing first is the only moment the 4264 // identifier is still at a known position; marking last keeps the release's OWN read of p legal 4265 // (it is the last legitimate use) and makes a DOUBLE release refuse for free, because the second 4266 // statement's read of the name is itself the use being checked -- no separate double-free rule. 4267 // A field or expression argument (`sys_munmap(ctx.buf, n)`) does not match and is a declared 4268 // floor: this rung tracks named locals only. 4269 var own_rel_tok: *Tok = 0 as *Tok 4270 if own_check_live() == 1 { 4271 if streq_n(name, "sys_munmap", 10) == 1 { 4272 let or0: *Tok = tok_at(P.toks, P.pos) 4273 let or1: *Tok = tok_at(P.toks, P.pos + 1) 4274 if or0.kind == TK_IDENT { 4275 var or_ok: i64 = 0 4276 if or1.kind == TK_COMMA { or_ok = 1 } 4277 if or1.kind == TK_AS { or_ok = 1 } 4278 if or_ok == 1 { own_rel_tok = or0 } 4279 } 4280 } 4281 } 4282 if peek_kind(P) != TK_RPAREN { 4283 let a0: i64 = parse_expr(P) 4284 args[n_args] = a0 4285 n_args = n_args + 1 4286 while peek_kind(P) == TK_COMMA { 4287 advance_tok(P) 4288 if n_args >= 24 { 4289 parse_die("function call exceeds 24 args (IR cap)" as *u8, 39) 4290 } 4291 let an: i64 = parse_expr(P) 4292 args[n_args] = an 4293 n_args = n_args + 1 4294 } 4295 } 4296 match_kind(P, TK_RPAREN) 4297 // LN5: the release lands only now -- see the capture block above for why the order matters. 4298 if own_rel_tok != (0 as *Tok) { own_mark_named(P, own_rel_tok, OWN_RELEASED) } 4299 var nlen: i64 = 0 4300 while name[nlen] != 0 { nlen = nlen + 1 } 4301 let callee: *Function = find_function(P.module, name, nlen) 4302 if callee == (0 as *Function) { 4303 // fp(args): if `name` is a func-pointer-typed LOCAL, emit an INDIRECT call through it (call *%r11) 4304 // instead of failing as an undefined function. Completes the func-pointer feature (the thread-pool 4305 // `fp(task.ctx)` shape). MVP caps at 6 args (register-only codegen). 4306 let fpl: *Local = find_local(P, name) 4307 if fpl != (0 as *Local) { 4308 if fpl.ty != (0 as *Type) { 4309 if fpl.ty.kind == TY_FUNC { 4310 if n_args > NX_FNPTR_MAX_ARGS { parse_die("indirect fn-ptr call exceeds max args (IR operand cap)" as *u8, 32) } 4311 var fpv: i64 = fpl.value_id 4312 if fpl.is_alloca == 1 { fpv = ir_emit_load(P.current_bb, fpl.value_id, fpl.ty) } 4313 var fret: *Type = fpl.ty.pointee 4314 if fret == (0 as *Type) { fret = ir_type_i64() } 4315 return ir_emit_call_indirect(P.current_bb, fpv, fret, args, n_args) 4316 } 4317 } 4318 } 4319 // Unresolved call. prepass_register_funcs registers EVERY real 4320 // function before any body is parsed, so reaching here means 4321 // `name` is genuinely undefined. Returning 0 here used to 4322 // SILENTLY produce value-id-0 (== first arg), turning a missing 4323 // compiler intrinsic into a no-op -- the SHA-512/__rotr64 bug 4324 // (SITES-LIVE 2026-05-27). Fail LOUD instead. A `__`-prefixed 4325 // name that lands here is an UNHANDLED INTRINSIC (it should have 4326 // matched parse_primary_intrinsic); anything else is a typo or a 4327 // missing import. Either way it must never silently no-op. 4328 if name[0] == 0x5F { 4329 if name[1] == 0x5F { 4330 // LN42 (2026-09-03): this refusal is CORRECT and was unreadable. It printed a bare sentence with 4331 // no file, no line and no caret, so a census could not group it and a reader could not find it. 4332 // It also matters WHICH kind of absence this is, and the two need opposite actions: an intrinsic 4333 // whose emitter exists but whose NAME was never routed here is BUILT AND UNWIRED (that was 4334 // __q4k_sb_dot, 32 programs, fixed the same day); an intrinsic with no emitter anywhere is simply 4335 // NOT IMPLEMENTED, and calling it is the program asking for a feature the compiler does not have. 4336 // Saying "unhandled" for both sent every reader looking for a missing line in this file. 4337 nx_diag_organ_at(call_ln) 4338 nx_diag_puts(": this __ intrinsic has no implementation in this compiler: " as *u8) 4339 nx_diag_puts(name) 4340 nx_diag_puts("\n" as *u8) 4341 nx_diag_caret(call_ln, nx_pd_col) 4342 nx_diag_puts(" why the build stopped: a __-prefixed name is an intrinsic, and intrinsics are matched by NAME here before any call is emitted. Nothing matched, so there is no code to emit and a silent no-op would ship a program that quietly does nothing.\n" as *u8) 4343 nx_diag_puts(" fix: if this intrinsic is meant to exist, check that its name is routed in parse_primary_intrinsic -- an opcode, an IR builder and a backend emitter can all exist while the NAME is unrouted, which makes the feature unreachable from every program. If it was never implemented, call the ordinary function that does this work instead.\n" as *u8) 4344 nx_diag_note_error() 4345 return 0 4346 } 4347 } 4348 nx_dym_suggest_fn(P.module, name, nlen) 4349 nx_diag_at(call_ln) 4350 nx_diag_puts(": I do not know the name '" as *u8) 4351 sys_write(2, name, nlen) 4352 nx_diag_puts("' -- it is called here, but nothing in this program defines it.\n" as *u8) 4353 nx_diag_caret(call_ln, call_cl) 4354 nx_diag_why_undefined() 4355 nx_dym_note2(P, name, nlen) 4356 nx_diag_note_error() 4357 // RECOVERY: value id 0. Mechanically safe for the parser (it was the 4358 // old silent behavior); the end-of-parse gate keeps it out of codegen. 4359 return 0 4360 } 4361 // ---- CALL-ARITY CHECK (2026-07-20) ------------------------------------ 4362 // nx_cc had NO arity checking: a call missing an argument compiled SILENTLY 4363 // and the callee read a GARBAGE REGISTER -- then WROTE THROUGH IT when the 4364 // parameter was an out-pointer. Symptoms are protean (impossible counter 4365 // values; a SEGV that MOVES when you refactor, because the register content 4366 // shifts), which is why it cost ~4 debug cycles in nx_step_tess2 and got 4367 // banked as a standing ★★★ gotcha. Same family as the undefined-function 4368 // fix above: a silent wrong-value is strictly worse than a loud stop. 4369 // 4370 // COVERAGE IS NOW TOTAL. This check was first landed gated on `callee.n_blocks > 0` (the 4371 // codebase's own "has a real body" test) because prepass_register_funcs registered a stub per 4372 // name WITHOUT parsing its parameter list, so a not-yet-defined callee reported n_params == 0 4373 // and checking it would have rejected every forward call -- including this compiler's own 4374 // source. That root cause is now FIXED at the source: prepass_count_params records the declared 4375 // arity on the stub before any body is parsed, so EVERY callee reachable by name has a known 4376 // signature and the guard is no longer needed. Forward calls and mutual recursion are covered. 4377 if n_args != callee.n_params { 4378 // 5W+H VOICE (2026-08-13): the old text ("call arity mismatch: f expects 2 arg(s), got 1") 4379 // stated the counts but not the CONSEQUENCE -- and this defect's symptom is protean 4380 // (garbage register reads that move when unrelated code moves), which is exactly what a 4381 // reader needs told, because the crash never points here. 4382 nx_diag_at(call_ln) 4383 nx_diag_puts(": '" as *u8) 4384 sys_write(2, name, nlen) 4385 nx_diag_puts("' is called here with " as *u8) 4386 nx_put_dec_err(n_args) 4387 nx_diag_puts(" value(s), but it is defined to take " as *u8) 4388 nx_put_dec_err(callee.n_params) 4389 nx_diag_puts(".\n" as *u8) 4390 nx_diag_caret(call_ln, call_cl) 4391 nx_diag_puts(" why the build stopped: a missing argument is not empty -- the callee would read whatever happened to be left in that register, so the wrong value moves when unrelated code moves and the crash never points back here.\n" as *u8) 4392 nx_diag_puts(" fix: pass the number of values the definition declares, or change the definition to take the number you are passing. If the extra values belong together, group them in a struct and pass its pointer.\n" as *u8) 4393 nx_diag_note_error() 4394 } 4395 // ---- CALL ARGUMENT TYPE CHECK (2026-08-01) ---------------------------- 4396 // nx_cc checked ARITY but never TYPES: a pointer passed where an i64 was declared, 4397 // and an integer passed where a pointer was declared, both compiled CLEAN. That is 4398 // the root of the silent-acceptance family. It is not theoretical -- it is how the 4399 // sev-8 evidence bug happened (debt 1785518763): ev_parse_pass passed an mmap'd 4400 // *i64 out-pointer into `kl: i64`, where it became the bound of `while i <= n - kl`; 4401 // the loop never executed and an evidence organ reported a ZERO tally for every gate. 4402 // 4403 // DELIBERATELY NARROW: POINTER-vs-INTEGER only. That distinction is unambiguous and 4404 // is the class that produces wild-pointer and impossible-bound bugs. Integer WIDTH 4405 // (i32 into i64) and struct identity are NOT checked here -- the corpus converts 4406 // freely between integer widths and rejecting that would be a different, much larger 4407 // change. Widen only as far as the evidence supports. 4408 // 4409 // LN6: a per-task context crossing the thread pool must come from a struct marked `send` 4410 // (sync_send_check; name-keyed on nx_pool_submit, abstains on anything it cannot type). 4411 if g_sendcheck_live == 1 { sync_send_check(P, name, nlen, args, n_args, call_ln, call_cl) } 4412 // SKIPPED when the callee's mask is 0 = signature not yet parsed (forward call to a 4413 // stub). Guessing a signature there is how the arity check first broke the compiler's 4414 // own source; a check that cannot know must decline, not invent. 4415 if callee.param_ptr_mask != 0 { 4416 var ai: i64 = 0 4417 while ai < n_args { 4418 let av: *Value = val_at(P.current_fn, args[ai]) 4419 var arg_is_ptr: i64 = 0 4420 var arg_known: i64 = 0 4421 if av != (0 as *Value) { 4422 let aty: *Type = av.ty 4423 if aty != (0 as *Type) { 4424 arg_known = 1 4425 if aty.kind == TY_PTR { arg_is_ptr = 1 } 4426 // an integer LITERAL is routinely used as a null/sentinel pointer; 4427 // only a *typed* value carries a real claim about pointerness. 4428 if av.kind == VK_CONST_INT { arg_known = 0 } 4429 } 4430 } 4431 var want_ptr: i64 = 0 4432 if (callee.param_ptr_mask & (1 << ai)) != 0 { want_ptr = 1 } 4433 // ONLY THE UNAMBIGUOUS DIRECTION: a value typed TY_PTR is definitely a 4434 // pointer, so passing it to an integer parameter is definitely wrong. The 4435 // reverse is NOT decidable here: an argument typed i64 may be a genuine 4436 // integer OR the PLACEHOLDER type of a call to a function defined later with 4437 // no forward declaration (the prepass stub carries i64 until its body is 4438 // parsed). Measured: checking that direction gave a 106/120 false-positive 4439 // rate, all of it placeholder types, not real defects. 4440 // This keeps the direction that produced the sev-8 ev_num_after silent-zero 4441 // (a *i64 out-pointer landing in `kl: i64`) and drops the one that cannot be 4442 // decided without a real type-inference pass. Widen only as far as the 4443 // evidence supports. 4444 // POINTER-INTO-INTEGER ONLY -- and this narrowness is MEASURED, not timid. 4445 // 4446 // A value typed TY_PTR is definitely a pointer, so handing it to an integer 4447 // parameter is definitely wrong: that is the sev-8 ev_num_after shape (an 4448 // mmap'd *i64 becoming a loop bound) and it costs 0 false positives over the 4449 // corpus sample. 4450 // 4451 // The REVERSE direction is enabled by flipping the commented line below, and 4452 // it was TRIED on 2026-08-01. Two separate causes of i64-looking arguments 4453 // were found and fixed -- return types were hardcoded ir_type_i64() for every 4454 // function, and forward stubs carried an i64 placeholder (prepass now records 4455 // return pointer-ness). Both real bugs, both fixed. But the corpus STILL 4456 // rejects 57/70 on `sys_munmap` arg 1 from inside the syscall layer's own 4457 // expansion, and that residue is NOT yet explained. Isolated repros of every 4458 // shape tried -- casts, pointer locals, call results, forward decls -- are all 4459 // quiet, so a third cause remains unfound. 4460 // DO NOT ENABLE IT UNTIL THAT RESIDUE IS EXPLAINED. Shipping it would reject 4461 // most of the corpus for a reason nobody has yet named, which is not a fix. 4462 var bad: i64 = 0 4463 if arg_known == 1 { 4464 if arg_is_ptr == 1 { if want_ptr == 0 { bad = 1 } } 4465 // BOTH DIRECTIONS: still DISABLED, and the residue is now PARTLY explained + MEASURED. 4466 // CAUSE FOUND AND FIXED 2026-08-05: the trailing `as T` cast MUTATED THE OPERAND'S 4467 // Value IN PLACE (parse_unary), so a local that ever appeared as `p as i64` read as 4468 // INTEGER at every LATER call -- exactly the syscall layer's timespec pattern before 4469 // sys_munmap(req). Cast now emits a typed identity; witnesses nx_probe_castmut_live.nx 4470 // (mutation reproduced pre-fix) + castmut2 (per-value, not check-wide). That fix is 4471 // LIVE (equiv 10/10 + selfhost, toolchain-promoted canary GREEN). 4472 // BUT IT IS NOT THE WHOLE RESIDUE, MEASURED NOT ASSUMED: flipping this line on the 4473 // FIXED compiler still fails 4 of 10 equiv rows -- nx_jpeg_ascii_test, 4474 // nx_tls13_client_session_recv_sh_test, nx_p256_keyshare_test, 4475 // nx_tls13_p256_loopback_test (build_b=1, i.e. the CHALLENGER refuses to compile them; 4476 // the other 6 rows + selfhost stay GREEN). Residue narrowed from 57/70 corpus targets 4477 // to 4 large crypto/codec modules = a SECOND, still-unnamed type-loss path. 4478 // NEXT INVESTIGATOR: build one of those 4 with a both-directions compiler and read the 4479 // exact `argument type mismatch` line + callee; that names cause #2. 4480 if arg_is_ptr != want_ptr { bad = 1 } 4481 } 4482 if bad == 1 { 4483 // parse_primary_call has no name token in scope; take the line from 4484 // the parser's current position, which is at/just past the call. 4485 let cur_tok: *Tok = tok_at(P.toks, P.pos) 4486 let cur_line: i64 = cur_tok.line 4487 nx_diag_organ_at(cur_line) 4488 sys_write(2, ": argument type mismatch in call to '" as *u8, 37) 4489 sys_write(2, name, nlen) 4490 sys_write(2, "': arg " as *u8, 7) 4491 nx_put_dec_err(ai + 1) 4492 if want_ptr == 1 { 4493 sys_write(2, " is an INTEGER but the parameter is a POINTER\n" as *u8, 46) 4494 } 4495 if want_ptr == 0 { 4496 sys_write(2, " is a POINTER but the parameter is an INTEGER\n" as *u8, 46) 4497 } 4498 nx_diag_puts(" why the build stopped: a pointer and an integer are passed in the same register but mean different things -- the callee would read the wrong kind of value and fault or compute garbage at this call.\n" as *u8) 4499 nx_diag_puts(" fix: pass the value the parameter declares -- cast an address with `as i64` or an integer with `as *T` only when that is really what the callee wants, otherwise pass the right variable.\n" as *u8) 4500 nx_diag_note_error() 4501 } 4502 ai = ai + 1 4503 } 4504 } 4505 let r: i64 = ir_emit_call(P.current_bb, callee, args, n_args) 4506 // LN3 provenance capture: a call to sys_mmap with a COMPILE-TIME-CONSTANT byte count is the 4507 // one allocation whose extent the compiler can see (mconsts fold to VK_CONST_INT at parse 4508 // time, so `sys_mmap(CAP_CONST)` qualifies). Record the result id; the `as *T` cast chain 4509 // in parse_unary carries it forward and parse_stmt_let binds it onto the local. Name-keyed 4510 // on sys_mmap alone for now -- a declared floor, listed in the LN3 block's imprecision note. 4511 if n_args == 1 { 4512 if mc_name_eq(name, "sys_mmap" as *u8) == 1 { 4513 let pav: *Value = val_at(P.current_fn, args[0]) 4514 if pav != (0 as *Value) { 4515 if pav.kind == VK_CONST_INT { 4516 if pav.const_int > 0 { g_prov_val = r; g_prov_bytes = pav.const_int } 4517 } 4518 } 4519 } 4520 } 4521 if callee.ret_ty != (0 as *Type) { 4522 return parse_field_chain(P, r, callee.ret_ty) 4523 } 4524 return r 4525} 4526 4527// NUL-terminated name compare (2 data args -- keeps clear of the >=3-data-arg helper miscompile class) 4528func mc_name_eq(a: *u8, b: *u8) -> i64 { 4529 var i: i64 = 0 4530 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 4531 if b[i] != (0 as u8) { return 0 } 4532 return 1 4533} 4534 4535// Is `name` declared as a module `const` LATER in the token stream? Non-destructive: saves and 4536// restores P.pos exactly as the prepasses do. Only ever called on the give-up path of an identifier 4537// lookup, so the scan cost is paid solely on what is already an error. 4538func mconst_declared_later(P: *Parser, name: *u8) -> i64 { 4539 let saved: i64 = P.pos 4540 var found: i64 = 0 4541 var go: i64 = 1 4542 while go == 1 { 4543 let k: i64 = peek_kind(P) 4544 if k == TK_EOF { 4545 go = 0 4546 } else { 4547 if k == TK_CONST { 4548 advance_tok(P) 4549 if peek_kind(P) == TK_IDENT { 4550 let t: *Tok = advance_tok(P) 4551 if mc_name_eq(tok_text_ptr(t), name) == 1 { found = 1; go = 0 } 4552 } 4553 } else { 4554 advance_tok(P) 4555 } 4556 } 4557 } 4558 P.pos = saved 4559 return found 4560} 4561 4562func parse_primary_local_or_const(P: *Parser, name: *u8) -> i64 { 4563 let L: *Local = find_local(P, name) 4564 if L != (0 as *Local) { 4565 // LN4/LN5: THE read hook, and the reason this rung needs only one. EVERY bare use of a name 4566 // arrives here -- a deref base, a call argument, an initializer, the right-hand side of an 4567 // assignment -- because parse_primary_ident routes anything that is not `name(` to this 4568 // function. The identifier token is one position back (parse_primary_ident already consumed 4569 // it), the same idiom oe_handshake_set uses below. An assignment TARGET does NOT come 4570 // through here (parse_stmt_ident_assign consumes its own name), which is exactly what makes 4571 // `p = sys_mmap(n)` a legal re-initialisation after a move rather than a refused use. 4572 if P.pos >= 1 { own_check_use(P, L, tok_at(P.toks, P.pos - 1)) } 4573 var v: i64 = L.value_id 4574 var v_pass_ty: *Type = L.ty // type handed to parse_field_chain; unwrapped once for static-ptr subscripts 4575 if L.is_alloca { 4576 if L.ty_kind != TY_STRUCT { 4577 if L.ty_kind != TY_ARRAY { 4578 if peek_kind(P) != TK_DOT { 4579 if peek_kind(P) != TK_LBRACKET { 4580 // Load the local's value at its DECLARED type 4581 // (L.ty), not hardcoded i64. Required for u8 / 4582 // i32 / pointer locals to widen / read correctly. 4583 // ARRAY (like STRUCT) is EXCLUDED here: a bare array name 4584 // must DECAY to its frame address, not load its first bytes. 4585 var lty: *Type = L.ty 4586 if lty == (0 as *Type) { lty = ir_type_i64() } 4587 // STATIC SCALAR TYPE FIX (2026-08-01). A static's injected Local is deliberately 4588 // typed *(decl_ty) -- the slot ADDRESS -- so field/index chains get the right 4589 // stride (see inject_statics). But LOADING that slot yields a value of the 4590 // DECLARED type, so the loaded Value must carry L.ty.pointee, NOT L.ty. 4591 // Without this every `static X: i64` read is recorded as TY_PTR, and the call 4592 // argument type check added earlier today then rejects the VALID line 4593 // `cd2_sputi(cg_nn)` as "arg 1 is a POINTER but the parameter is an INTEGER". 4594 // ★★★★★A TYPE THAT WAS ONLY EVER USED FOR ADDRESSING BECAME A CLAIM ABOUT THE 4595 // VALUE THE MOMENT SOMETHING STARTED READING IT -- the slot type was harmlessly 4596 // wrong for as long as nothing consulted it, and the new checker is what turned 4597 // a latent mislabel into 1-in-10 organs refusing to compile. 4598 // Same VK_GLOBAL-gated unwrap already proven on the static-ptr field-read path 4599 // below; ordinary alloca locals are untouched, so they stay byte-identical. 4600 // CODEGEN IS UNCHANGED CORPUS-WIDE, measured not assumed: load_ty drives load 4601 // WIDTH, and the corpus has 366 i64 statics (8 bytes either way), 305 pointer 4602 // statics (8 bytes either way) and ZERO narrow (u8/i32/i16) scalar statics, the 4603 // only shape whose width this could alter. 4604 let lv_ld: *Value = val_at(P.current_fn, L.value_id) 4605 if lv_ld.kind == VK_GLOBAL { 4606 if L.ty != (0 as *Type) { 4607 if L.ty.pointee != (0 as *Type) { lty = L.ty.pointee } 4608 } 4609 } 4610 return ir_emit_load(P.current_bb, L.value_id, lty) 4611 } 4612 } 4613 } 4614 } 4615 } 4616 // F-meta-3 fix (Session 12): when the local is alloca-backed 4617 // AND its declared type is a pointer-to-struct (e.g. function 4618 // param `out: *Struct`), the alloca slot holds the POINTER 4619 // value, not the struct. parse_field_chain expects `base` to 4620 // BE the pointer; if we pass the alloca slot directly it does 4621 // GEP on the slot and reads stack garbage. Load the pointer 4622 // out of the slot first. Sibling fix to parse_stmt_ident_dot. 4623 if L.is_alloca == 1 { 4624 if L.ty != (0 as *Type) { 4625 if L.ty.kind == TY_PTR { 4626 if peek_kind(P) == TK_DOT { 4627 // Loaded value is the pointer itself (type L.ty). 4628 v = ir_emit_load(P.current_bb, L.value_id, L.ty) 4629 // Static-ptr FIELD-READ fix (2026-07-10; mirrors the LBRACKET multi-static fix 4630 // below): a STATIC's injected Local is typed *(decl_ty) -- the slot ADDRESS -- so 4631 // after the load v holds the REAL pointer, whose type is L.ty.pointee. Passing the 4632 // un-unwrapped **Struct made parse_field_chain's struct lookup FAIL -> documented 4633 // bail -> `g.field` READ returned the RAW POINTER (nx_static_field_probe witness). 4634 // VK_GLOBAL-gated so ordinary alloca ptr locals stay byte-identical (equiv-safe). 4635 let lv_d: *Value = val_at(P.current_fn, L.value_id) 4636 if lv_d.kind == VK_GLOBAL { 4637 if L.ty.pointee != (0 as *Type) { v_pass_ty = L.ty.pointee } 4638 } 4639 } 4640 // Multi-static bug fix: a STATIC pointer's value_id is a VK_GLOBAL (the slot ADDRESS), 4641 // which materialises as-address (leaq) and does NOT auto-load like an OP_ALLOCA. So a 4642 // `staticptr[i]` READ subscripted the slot address -> returned the pointer, not *pointer 4643 // (write worked, read was wrong -- silent corruption with >=1 static pointer read). Load 4644 // the pointer from the slot first, ONLY for VK_GLOBAL, so var/alloca pointers (which 4645 // auto-load) stay byte-identical -> equiv gate unaffected. 4646 if peek_kind(P) == TK_LBRACKET { 4647 let lv_g: *Value = val_at(P.current_fn, L.value_id) 4648 if lv_g.kind == VK_GLOBAL { 4649 // STATIC ARRAY vs STATIC POINTER -- they need OPPOSITE treatment here, and 4650 // conflating them is why `static a: [N]T` has been a documented daemon-killer. 4651 // A static POINTER's slot HOLDS a pointer, so it must be loaded. A static 4652 // ARRAY's slot IS the array, so its ADDRESS is already the base and loading it 4653 // reads ELEMENT 0 AND USES IT AS THE BASE. .lcomm zero-fills, so that base was 4654 // 0 and the first indexed access wrote to a near-null address -- compiles 4655 // clean, dies on touch, exactly as witness nx_rw_staticarr.nx records. 4656 // This is the static sibling of the local array-decay rule below. 4657 var g_is_arr: i64 = 0 4658 if L.ty.pointee != (0 as *Type) { 4659 if L.ty.pointee.kind == TY_ARRAY { g_is_arr = 1 } 4660 } 4661 if g_is_arr == 0 { 4662 v = ir_emit_load(P.current_bb, L.value_id, L.ty) 4663 } 4664 // Static-ptr STRIDE fix: inject_statics types the Local as *(e.ty) (the slot 4665 // ADDRESS). After the load, v holds the REAL pointer (e.ty); its element is 4666 // e.ty.pointee, so the stride must come from L.ty.pointee, not L.ty. Was: a *u8 4667 // static -> stride 8 (sizeof *u8) instead of 1 -> ran off the buffer on large 4668 // indices (nx_drbench SIGSEGV). *i64 statics were unaffected (8 == correct). 4669 // For the ARRAY case this same assignment hands the postfix handler the ARRAY 4670 // type, which is what makes it GEP from the base and bounds-check the index. 4671 if L.ty.pointee != (0 as *Type) { v_pass_ty = L.ty.pointee } 4672 } 4673 } 4674 } 4675 } 4676 } 4677 // Pass the local's actual type so parse_field_chain can 4678 // emit GEP+LOAD for o.field accesses. Closes T#bar3-codegen-001 4679 // (was passing 0 as *Type which made field_chain bail without 4680 // emitting, causing nxc.elf to return the raw pointer instead 4681 // of o.field for `let o: *S = ...; return o.field`). 4682 // Array DECAY: a bare array name (not indexed, not dotted) yields its stack ADDRESS as a 4683 // pointer VALUE (C-style array->*elem decay), so `sys_write(1, buf, n)` gets the address. 4684 // Emit OP_ADDR_OF for the leaq path -- returning the raw alloca id auto-loads the array's 4685 // first bytes as a bogus pointer (same trap the &name path documents). 4686 if L.is_alloca == 1 { 4687 if L.ty_kind == TY_ARRAY { 4688 if peek_kind(P) != TK_LBRACKET { 4689 if peek_kind(P) != TK_DOT { 4690 let pta: *Type = alloc_type(TY_PTR, 8, 8) 4691 pta.pointee = L.ty.pointee 4692 return ir_emit_unop(P.current_bb, OP_ADDR_OF, L.value_id, pta) 4693 } 4694 } 4695 } 4696 } 4697 // LN3 handshake: a `let`-local with visible provenance hands its extent to the chain 4698 // walker through the statics parse_field_chain snapshots at entry. Non-alloca only -- 4699 // for a `let`, v IS the binding's value id, so the id-equality key in the chain is exact. 4700 if L.is_alloca == 0 { 4701 if L.ext_bytes > 0 { g_pp_base_val = v; g_pp_base_ext = L.ext_bytes } 4702 } 4703 // LN2 handshake: the identifier token sits one back (parse_primary_ident consumed it and 4704 // nothing else before delegating here); the chain walker / `*p` reader checks v against it. 4705 if g_optenforce_live == 1 { 4706 if P.pos >= 1 { oe_handshake_set(P, L, v, tok_at(P.toks, P.pos - 1)) } 4707 } 4708 return parse_field_chain(P, v, v_pass_ty) 4709 } 4710 let out_raw: *u8 = sys_mmap(16) 4711 let out: *i64 = out_raw as *i64 4712 *out = 0 4713 if lookup_mconst(P, name, out) == 1 { 4714 return safe_const_i64(P, *out, "parse.nx:mconst-out" as *u8) 4715 } 4716 // LN39: an f64 const is a typed constant, exactly like an inline float literal. 4717 if lookup_mconst_f64(P, name, out) == 1 { 4718 let fvid: i64 = safe_const_i64(P, *out, "parse.nx:mconst-f64" as *u8) 4719 let fbase: i64 = P.current_fn.values as i64 4720 let fv: *Value = (fbase + fvid * 48) as *Value 4721 fv.ty = alloc_type(TY_F64, 8, 8) 4722 return fvid 4723 } 4724 // V-LANGEXT M1: string-const lookup. Same shape as inline TK_STRING 4725 // emission at parse_primary (~line 873): emit a VAL_GLOBAL_ADDR 4726 // value via ir_global_value with the stored pointer type. 4727 let sgid_raw: *u8 = sys_mmap(16) 4728 let sgid: *i64 = sgid_raw as *i64 4729 let sty_raw: *u8 = sys_mmap(16) 4730 let sty: **Type = sty_raw as **Type 4731 *sgid = 0 4732 *sty = 0 as *Type 4733 if lookup_mconst_string(P, name, sgid, sty) == 1 { 4734 // ROOT FIX seq1552 (2026-07-30): a STRING module-const IS a pointer value, so a use site can 4735 // legitimately carry a postfix `[i]` or `.field` exactly like a pointer local. This path used 4736 // to hand back the global address WITHOUT running the postfix chain, so the `[` `i` `]` tokens 4737 // of `CONST[i]` were left in the stream; the enclosing expression then desynced and the program 4738 // COMPILED CLEAN while summing the wrong bytes. Witness pair (byte-identical but for one line): 4739 // nx_constidx_probe.nx (WRONG-SUM, exit 1) vs nx_constidx_ctrl.nx (sum=294, exit 0). 4740 // The chain is entered ONLY when a postfix token actually follows, so every existing use site 4741 // takes the byte-identical old path -- and module-level contexts (P.current_fn == 0, see the 4742 // reset at the end of parse_function) never reach parse_field_chain's current_fn assert. 4743 let gcv: i64 = ir_global_value(P.current_fn, *sgid, *sty) 4744 if peek_kind(P) == TK_LBRACKET { return parse_field_chain(P, gcv, *sty) } 4745 if peek_kind(P) == TK_DOT { return parse_field_chain(P, gcv, *sty) } 4746 return gcv 4747 } 4748 // Bare FUNCTION name used as a VALUE (not a call): yields the function's code address 4749 // (VK_FUNC_ADDR -> `leaq <fn>(%rip)`). This is what nx_thread_pool passes as a worker arg and 4750 // what __thread_clone(top, worker, ctx) needs -- previously this fell through to `return 0` 4751 // (constant 0), giving a NULL entry -> the SIGSEGV (exit 139). Shares the &fn mechanism. 4752 var fv_nl: i64 = 0 4753 while name[fv_nl] != 0 { fv_nl = fv_nl + 1 } 4754 let fv_fn: *Function = find_function(P.module, name, fv_nl) 4755 if fv_fn != (0 as *Function) { 4756 let fv_ft: *Type = alloc_type(TY_FUNC, 8, 8) 4757 fv_ft.pointee = fv_fn.ret_ty 4758 return ir_func_addr_value(P.current_fn, fv_fn, fv_ft) 4759 } 4760 // ⚠UNKNOWN IDENTIFIER STILL FALLS THROUGH TO CONSTANT ZERO -- A KNOWN, UNFIXED HOLE. 4761 // Reaching here means `name` resolved to NOTHING: not a local, not a module const, not a string 4762 // const, not a function. Returning 0 silently emits the CONSTANT ZERO in its place, so a typo -- 4763 // or the banked case of a `let` local referenced ABOVE its declaration -- becomes 0 with no 4764 // diagnostic and SEGVs far from the cause. It is the same silent-value-id-0 class already killed 4765 // for undefined CALLS (parse_primary_call) and, just above, for bare function names. 4766 // 4767 // A fail-loud version WAS built and tested here 2026-07-20 and is NOT shipped, deliberately. 4768 // ★It paid for itself immediately: it caught `AT_FDCWD` being read as 0 by sys_unlinkat / 4769 // sys_fchmodat (a forward module-const reference) -- a live miscompile now fixed in 4770 // nx_syscalls.nx, with a standing witness at runtime/nx_fwdconst_probe.nx proving the class 4771 // (forward-read=0 vs direct-read=-100 in ONE binary). 4772 // ★But it also reports a FALSE POSITIVE that is not yet understood: `unknown identifier: m` while 4773 // parsing nx_opt.nx after `inl_clone_all`, where `m` is an ordinary FUNCTION PARAMETER 4774 // (`opt_inline_module(m: *Module)`) used normally -- i.e. find_local misses a live parameter in 4775 // some context. Shipping a compiler diagnostic whose false positive is unexplained would trade a 4776 // silent-wrong-value bug for a can't-build bug, so it stays out until that is root-caused. 4777 // NEXT RUNG: root-cause the find_local miss (print the enclosing function in the diagnostic to 4778 // self-locate it), then land this exactly like the call-arity check was landed. 4779 // 4780 // ---- WHAT *IS* SHIPPED: the FORWARD MODULE-CONST slice, caught precisely ----------------- 4781 // The blanket check is unsafe (above), but ONE slice of it is both safe and high value. If 4782 // `name` is declared as a module `const` LATER in the token stream, this is unambiguously the 4783 // forward-module-const miscompile -- the one that made AT_FDCWD read 0 instead of -100 and so 4784 // put dirfd=0 into sys_unlinkat/sys_fchmodat. No legitimate program has that shape, and it 4785 // CANNOT false-positive the way the blanket check does: an ordinary parameter (the `m` case) 4786 // is never also declared as a later const. Witness: runtime/nx_fwdconst_probe.nx. 4787 if mconst_declared_later(P, name) == 1 { 4788 // LN42 (2026-09-03): the last refusal in this file that printed a bare sentence with no location. 4789 // A hand-counted length beside each literal (52 and 68) was a second copy of the literal shape, the 4790 // exact drift LN27 removed elsewhere; nx_diag_puts measures the string itself. 4791 nx_diag_organ_at(nx_pd_line) 4792 nx_diag_puts(": this module const is used before it is declared: " as *u8) 4793 nx_diag_puts(name) 4794 nx_diag_puts("\n" as *u8) 4795 nx_diag_caret(nx_pd_line, nx_pd_col) 4796 nx_diag_puts(" why the build stopped: a module const is folded into its readers at the point of use, so a reader ABOVE the declaration would silently read 0 rather than the value -- a wrong answer compiled without complaint, which is the one outcome this compiler refuses to produce.\n" as *u8) 4797 nx_diag_puts(" fix: move the const declaration ABOVE its first reader. Forward references are deliberately not allowed here, the same restriction C and Rust place on const-expression ordering.\n" as *u8) 4798 nx_diag_note_error() 4799 return 0 4800 } 4801 // seq1012 FAIL-LOUD (landed 2026-07-29): an identifier that resolves to NOTHING is a 4802 // compile ERROR, never the silent constant 0 that wrote wrong data and relocated with 4803 // unrelated refactors. The 2026-07-20 attempt was withheld over ONE false positive 4804 // ("m" in fn inl_clone_all) -- the survey diagnostic located it as seq358 IN A MASK: 4805 // the multiline else-if desync fused functions, so live parameters resolved against the 4806 // wrong function. With the else-if production fixed, the whole corpus surveys CLEAN 4807 // (nx_compile_x86 + nx_wasm_craft + nx_game_page_emit: unresolved=0) and this check 4808 // lands exactly like the call-arity check did. Witness: runtime/nx_undefprobe.nx. 4809 // The caller consumed the identifier before resolution began, so its own token sits one 4810 // slot back (the parse_primary_call anchoring lesson: never take the line at error time 4811 // from the CURRENT position -- take the construct's own token). 4812 var uid_ln: i64 = 0 4813 var uid_cl: i64 = 0 4814 if P.pos > 0 { 4815 let uid_tok: *Tok = tok_at(P.toks, P.pos - 1) 4816 uid_ln = uid_tok.line 4817 uid_cl = uid_tok.col 4818 } 4819 nx_dym_suggest_ident(P, name, fv_nl) 4820 nx_diag_at(uid_ln) 4821 nx_diag_puts(": I do not know the name '" as *u8) 4822 sys_write(2, name, fv_nl) 4823 nx_diag_puts("'" as *u8) 4824 if P.current_fn != (0 as *Function) { 4825 nx_diag_puts(" (used inside the function '" as *u8) 4826 sys_write(2, P.current_fn.name_start as *u8, P.current_fn.name_len) 4827 nx_diag_puts("')" as *u8) 4828 } 4829 nx_diag_puts(" -- no variable, constant, or function defines it.\n" as *u8) 4830 nx_diag_caret(uid_ln, uid_cl) 4831 nx_diag_why_undefined() 4832 nx_dym_note2(P, name, fv_nl) 4833 nx_diag_note_error() 4834 return 0 4835} 4836 4837// Prefix-unary level. The trailing `as T` cast lives HERE (the 4838// wrapper), NOT inside the operand parse, so prefix operators bind 4839// TIGHTER than `as`: `*p as T` == `(*p) as T` (Rust precedence). 4840// Pre-fix, the deref branch's recursive operand parse swallowed the 4841// cast -- `*p as *u8` parsed as `*(p as *u8)` and byte-loaded the 4842// POINTER CELL, then deref'd that garbage (SIGSEGV). Min repro: 4843// runtime/_derefcast_minrepro.nx (the B2-row landmine, 2026-06-10). 4844func parse_unary(P: *Parser) -> i64 { 4845 let base: i64 = parse_unary_core(P) 4846 // Trailing `as T` cast. The cast RESULT must carry target_ty so downstream 4847 // LOAD/STORE/GEP see the correct pointee -- but the OPERAND must keep its own 4848 // type. This used to do `bv.ty = target_ty` IN PLACE on the operand's Value: 4849 // when the operand is a bare local, that node IS the local's binding, so 4850 // `p as i64` flipped p to INTEGER for EVERY later reference. Proven minimal 4851 // 2026-08-05 (nx_probe_castmut_live.nx: a prior cast-in-argument-position made 4852 // the pointer-into-integer refusal VANISH; discriminator castmut2 shows it is 4853 // per-value) -- and it is the ROOT of the 57/70 sys_munmap-arg-1 residue that 4854 // kept the reverse call-arg type direction disabled (see the do-not-enable 4855 // note in the call type check). Fix: emit a typed IDENTITY (base + 0) so the 4856 // cast result is a FRESH Value id with target_ty and a legitimate result slot 4857 // in every backend; the optimizer may fold the identity -- harmless, because 4858 // type checks run at parse time and load/store widths are instruction-encoded. 4859 // CHAINED CASTS (2026-08-13 night): `X as *u8 as i64` was HALF-APPLIED for as long as this 4860 // site has existed -- the single `if` consumed ONE cast and the dangling `as i64` fell into 4861 // the silent junk-skip, so the second cast never happened (right bits by accident for 4862 // ptr->int, wrong TYPE always). The new discarded-expression check turned that silence into 4863 // a refusal with a MISLEADING cause at 15 estate sites (nx_tool_run/nx_mgmt_data closure), 4864 // which is how the gap finally surfaced. A WHILE makes the chain real: each `as T` wraps the 4865 // previous value in a typed identity; bits unchanged, final TYPE = the last cast. 4866 var cast_v: i64 = base 4867 // LN6: remember what the chain's OPERAND was, so a call site can ask what a cast erased. 4868 var cast_src: *Type = 0 as *Type 4869 let bv0: *Value = val_at(P.current_fn, base) 4870 if bv0 != (0 as *Value) { cast_src = bv0.ty } 4871 while peek_kind(P) == TK_AS { 4872 advance_tok(P) 4873 let target_ty: *Type = parse_type(P) 4874 if target_ty == (0 as *Type) { break } 4875 let czero: i64 = safe_const_i64(P, 0, "parse.nx:cast-identity-const0" as *u8) 4876 // LN3: the cast's typed identity is a FRESH value id, so provenance recorded on the 4877 // operand (a sys_mmap result, or a prior link of this chain) must follow it -- this is 4878 // what makes the estate idiom `let s: *i64 = sys_mmap(N) as *i64` bindable. 4879 var pp_carry: i64 = 0 4880 if cast_v == g_prov_val { pp_carry = 1 } 4881 cast_v = ir_emit_binop(P.current_bb, OP_ADD, cast_v, czero, target_ty) 4882 if pp_carry == 1 { g_prov_val = cast_v } 4883 } 4884 if cast_v != base { g_last_cast_val = cast_v; g_last_cast_src_ty = cast_src } 4885 return cast_v 4886} 4887 4888func parse_unary_core(P: *Parser) -> i64 { 4889 nx_assert_ptr(P.current_fn as *u8, "parse_unary: P.current_fn" as *u8) 4890 nx_assert(P.current_fn.values_cap > 0, 4891 "parse_unary: P.current_fn init" as *u8) 4892 let k: i64 = peek_kind(P) 4893 if k == TK_MINUS { 4894 advance_tok(P) 4895 let rhs: i64 = parse_unary(P) 4896 let zero: i64 = safe_const_i64(P, 0, "parse.nx:LINE-const0" as *u8) 4897 return ir_emit_binop(P.current_bb, OP_SUB, zero, rhs, ir_type_i64()) 4898 } 4899 if k == TK_BANG { 4900 advance_tok(P) 4901 let rhs: i64 = parse_unary(P) 4902 let zero: i64 = safe_const_i64(P, 0, "parse.nx:LINE-const0" as *u8) 4903 return ir_emit_binop(P.current_bb, OP_EQ, rhs, zero, ir_type_bool()) 4904 } 4905 // ~expr -- bitwise NOT (one's complement). Parallels TK_BANG above; 4906 // lowers to the dedicated OP_NOT (single x86 `notq` / rv64 `not`), 4907 // the optimal one-instruction form. Was MISSING from the function- 4908 // body parser (only the const-evaluator handled `~`), so `~x` in a 4909 // function body fell through to parse_primary, silently miscompiled, 4910 // AND desynced the token stream -- which surfaced downstream as the 4911 // misleading "address-of unknown local (&name)" when a binary `&` 4912 // followed (e.g. SHA-256 Choose: `((~e) & g) ...`). Now `~` is a 4913 // first-class prefix operator like -, !, &, *. 4914 if k == TK_TILDE { 4915 advance_tok(P) 4916 let rhs: i64 = parse_unary(P) 4917 return ir_emit_unop(P.current_bb, OP_NOT, rhs, ir_type_i64()) 4918 } 4919 // &name[.field]* -- address-of, with optional field-chain walk. 4920 // Valid on alloca-backed vars; returns the alloca address (no 4921 // load) when there's no chain, or the GEP'd field address when 4922 // `.field.subfield...` follows. 4923 // 4924 // Extended 2026-05-16 per cardinal feedback-bits-up-canonical- 4925 // layer + user directive "never avoid a limitation bits up build": 4926 // previously rejected `&card.field`, forcing callers to inline 4927 // assignments. Now mirrors parse_stmt_ident_dot's chain walker 4928 // exactly, only returning the ADDRESS (no load, no store). 4929 // 4930 // STUB(parser, T#parser-001): non-alloca local case currently 4931 // absorbs the error by returning 0. Caller can't distinguish 4932 // "no such local" from "address-of-non-alloca-local"; either 4933 // path silently miscompiles. 4934 // Plan: distinguish via typed error sentinel or assert; depends 4935 // on a parser-wide error-reporting path that doesn't yet exist. 4936 // Closes when: parser grows a typed error channel. 4937 if k == TK_AMP { 4938 advance_tok(P) 4939 let name_tok: *Tok = advance_tok(P) 4940 let name: *u8 = tok_text_ptr(name_tok) 4941 let L: *Local = find_local(P, name) 4942 if L == (0 as *Local) { 4943 // Not a local -- maybe a FUNCTION: `&fn` yields the function's code address (VK_FUNC_ADDR), 4944 // emitted as `leaq <fn>(%rip), %reg`. Shares its mechanism with bare-fn-name-as-value. 4945 var amp_nl: i64 = 0 4946 while name[amp_nl] != 0 { amp_nl = amp_nl + 1 } 4947 let amp_fn: *Function = find_function(P.module, name, amp_nl) 4948 if amp_fn != (0 as *Function) { 4949 let amp_ft: *Type = alloc_type(TY_FUNC, 8, 8) 4950 amp_ft.pointee = amp_fn.ret_ty 4951 return ir_func_addr_value(P.current_fn, amp_fn, amp_ft) 4952 } 4953 parse_die("address-of unknown local (&name)" as *u8, 32) 4954 } 4955 // Bare `&name` (no .field chain): the ADDRESS of the local's 4956 // stack slot. Emit OP_ADDR_OF so codegen uses the as-address 4957 // (leaq) path; returning the raw alloca value-id auto-loads the 4958 // slot CONTENTS -> bogus pointer -> SEGV. Works for value-typed 4959 // (`var x: i64`) AND pointer-typed (`var p: *T`, out-params). 4960 if L.is_alloca == 1 { 4961 if peek_kind(P) != TK_DOT { 4962 let pt: *Type = alloc_type(TY_PTR, 8, 8) 4963 pt.pointee = L.ty 4964 return ir_emit_unop(P.current_bb, OP_ADDR_OF, L.value_id, pt) 4965 } 4966 } 4967 var v: i64 = L.value_id 4968 // Mirror the F-meta-3 fix in parse_stmt_ident_dot: when the 4969 // local is alloca-backed AND it's a pointer AND we're walking 4970 // a `.field` chain, load the pointer value out of the slot 4971 // first so GEP applies to the actual pointee, not the alloca. 4972 // 4973 // CRITICAL: do this ONLY when a `.field` chain follows. For 4974 // bare `&name` we want the ALLOCA ADDRESS (== L.value_id), 4975 // NOT the loaded pointer value. Pre-fix this overload broke 4976 // outparam patterns like `nx_media_pool_get(..., &src, ...)` 4977 // where the callee writes through the pointer expected to be 4978 // &src's slot -- the load made it write through src's value 4979 // (uninitialised 0 -> NULL deref). 4980 if L.is_alloca == 1 { 4981 if L.ty != (0 as *Type) { 4982 if L.ty.kind == TY_PTR { 4983 if peek_kind(P) == TK_DOT { 4984 v = ir_emit_load(P.current_bb, L.value_id, L.ty) 4985 } 4986 } 4987 } 4988 } 4989 var ty: *Type = L.ty 4990 while peek_kind(P) == TK_DOT { 4991 advance_tok(P) 4992 let ftok: *Tok = advance_tok(P) 4993 let fname: *u8 = tok_text_ptr(ftok) 4994 var flen: i64 = 0 4995 while fname[flen] != 0 { flen = flen + 1 } 4996 var stty: *Type = ty 4997 if stty != (0 as *Type) { 4998 if stty.kind == TY_PTR { 4999 if stty.pointee != (0 as *Type) { stty = stty.pointee } 5000 } 5001 } 5002 if stty == (0 as *Type) { break } 5003 let field: *StructField = parse_field_of_struct(P, stty, ftok, fname, flen) 5004 if field == (0 as *StructField) { break } 5005 let off2: i64 = safe_const_i64(P, field.offset, "parse.nx:amp-field-offset" as *u8) 5006 // `&f.b` is an ADDRESS, so its type is POINTER-to-field, not the field's own type. 5007 // ir_emit_gep uses its 4th arg DIRECTLY as the result value's type (nx_ir.nx:1216), so 5008 // passing field.ty typed &f.b as i64 -- an INTEGER. The bare `&name` path 40 lines above 5009 // already builds TY_PTR with pointee = L.ty; this chain path never did. 5010 // MEASURED 2026-08-10: the DEPLOYED compiler contains no "argument type mismatch" string 5011 // at all (grep -a, with `address-of unknown local` as the positive control), so it never 5012 // checked and this mistyping was invisible for as long as it has existed. The gauntlet's 5013 // addr_of_field tooth only started failing because the CHECK is new -- the BUG is old. 5014 // ★★★★★ A NEW CHECKER FAILING AN OLD TEST IS NOT A REGRESSION; IT IS THE FIRST TIME 5015 // ANYTHING LOOKED. Fix the type, not the checker. 5016 let fpt: *Type = alloc_type(TY_PTR, 8, 8) 5017 fpt.pointee = field.ty 5018 v = ir_emit_gep(P.current_bb, v, off2, fpt) 5019 // the chain walker derefs a TY_PTR itself, so keep `ty` as the field type for the next hop 5020 ty = field.ty 5021 } 5022 return v 5023 } 5024 // *expr -- pointer dereference in expression position. Loads 5025 // from the pointee. Compared with the `*name = expr` statement 5026 // path above, this one yields a value (not an lvalue). 5027 if k == TK_STAR { 5028 advance_tok(P) 5029 // Operand via CORE: a trailing `as` belongs to the deref 5030 // RESULT (wrapper), never to the address operand. 5031 let addr: i64 = parse_unary_core(P) 5032 oe_chain_check(P, addr) // LN2: `*p` reads through p 5033 // Result type = pointee of addr's type (NOT hardcoded i64). 5034 // Without this, `*(p: *u8)` loads 8 bytes instead of 1. 5035 let addr_val: *Value = val_at(P.current_fn, addr) 5036 var elem_ty: *Type = ir_type_i64() 5037 if addr_val.ty != (0 as *Type) { 5038 if addr_val.ty.kind == TY_PTR { 5039 if addr_val.ty.pointee != (0 as *Type) { 5040 elem_ty = addr_val.ty.pointee 5041 } 5042 } 5043 } 5044 return ir_emit_load(P.current_bb, addr, elem_ty) 5045 } 5046 // Trailing `as T` handling lives in the parse_unary wrapper 5047 // (required for `((p as i64) + i) as *u8` retype patterns -- 5048 // see wrapper comment for the deref-precedence defect this 5049 // placement fixes). 5050 return parse_primary(P) 5051} 5052 5053// Helpers for type-directed arithmetic op selection. When either 5054// operand is TY_F32 (f64 later), promote OP_ADD/SUB/MUL/DIV_S to 5055// their floating-point counterparts and carry TY_F32 through the 5056// result. Non-fp operands fall through to the integer ops + i64 5057// result type -- same as the pre-F-extension behavior. 5058 5059func fp_op_for_int(op: i64) -> i64 { 5060 if op == OP_ADD { return OP_FADD } 5061 if op == OP_SUB { return OP_FSUB } 5062 if op == OP_MUL { return OP_FMUL } 5063 if op == OP_DIV_S { return OP_FDIV } 5064 return op 5065} 5066 5067func has_fp_operand(P: *Parser, v: i64) -> i64 { 5068 if v < 0 { return 0 } 5069 if v >= P.current_fn.n_values { return 0 } 5070 let base: i64 = P.current_fn.values as i64 5071 let val: *Value = (base + v * 48) as *Value 5072 if val.ty == (0 as *Type) { return 0 } 5073 if val.ty.kind == TY_F32 { return 1 } 5074 if val.ty.kind == TY_F64 { return 1 } 5075 return 0 5076} 5077 5078func nx_value_type(P: *Parser, v: i6