code wiki / (root) / nx_parse_field_candidate_t280.nx

nx_parse_field_candidate_t280.nx source

↩ module page · 5074 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 integer dot accumulate: __i16x16_madd(acc_ptr, a_ptr, b_ptr) -> *acc(i32x8) += vpmaddwd(a,b) 3620 if streq_n(name, "__i16x16_madd", 13) == 1 { 3621 if peek_kind(P) == TK_LPAREN { 3622 advance_tok(P) 3623 let mac: i64 = parse_expr(P) 3624 match_kind(P, TK_COMMA) 3625 let maa: i64 = parse_expr(P) 3626 match_kind(P, TK_COMMA) 3627 let mab: i64 = parse_expr(P) 3628 match_kind(P, TK_RPAREN) 3629 return ir_emit_i16x16_madd(P.current_bb, mac, maa, mab) 3630 } 3631 } 3632 if streq_n(name, "__simd_vreduce_min_i16_x16", 26) == 1 { 3633 if peek_kind(P) == TK_LPAREN { 3634 advance_tok(P) 3635 let pmn: i64 = parse_expr(P) 3636 match_kind(P, TK_RPAREN) 3637 return ir_emit_simd_vreduce_min_i16_x16(P.current_bb, pmn) 3638 } 3639 } 3640 if streq_n(name, "__simd_vreduce_max_i16_x16", 26) == 1 { 3641 if peek_kind(P) == TK_LPAREN { 3642 advance_tok(P) 3643 let pmx: i64 = parse_expr(P) 3644 match_kind(P, TK_RPAREN) 3645 return ir_emit_simd_vreduce_max_i16_x16(P.current_bb, pmx) 3646 } 3647 } 3648 if streq_n(name, "__simd_vsadd_i16_x16", 20) == 1 { 3649 if peek_kind(P) == TK_LPAREN { 3650 advance_tok(P) 3651 let asa: i64 = parse_expr(P) 3652 match_kind(P, TK_COMMA) 3653 let bsa: i64 = parse_expr(P) 3654 match_kind(P, TK_COMMA) 3655 let osa: i64 = parse_expr(P) 3656 match_kind(P, TK_RPAREN) 3657 return ir_emit_simd_vsadd_i16_x16(P.current_bb, asa, bsa, osa) 3658 } 3659 } 3660 if streq_n(name, "__simd_vssub_i16_x16", 20) == 1 { 3661 if peek_kind(P) == TK_LPAREN { 3662 advance_tok(P) 3663 let ass: i64 = parse_expr(P) 3664 match_kind(P, TK_COMMA) 3665 let bss: i64 = parse_expr(P) 3666 match_kind(P, TK_COMMA) 3667 let oss: i64 = parse_expr(P) 3668 match_kind(P, TK_RPAREN) 3669 return ir_emit_simd_vssub_i16_x16(P.current_bb, ass, bss, oss) 3670 } 3671 } 3672 if streq_n(name, "__simd_vsaddu_i16_x16", 21) == 1 { 3673 if peek_kind(P) == TK_LPAREN { 3674 advance_tok(P) 3675 let asau: i64 = parse_expr(P) 3676 match_kind(P, TK_COMMA) 3677 let bsau: i64 = parse_expr(P) 3678 match_kind(P, TK_COMMA) 3679 let osau: i64 = parse_expr(P) 3680 match_kind(P, TK_RPAREN) 3681 return ir_emit_simd_vsaddu_i16_x16(P.current_bb, asau, bsau, osau) 3682 } 3683 } 3684 if streq_n(name, "__simd_vssubu_i16_x16", 21) == 1 { 3685 if peek_kind(P) == TK_LPAREN { 3686 advance_tok(P) 3687 let assu: i64 = parse_expr(P) 3688 match_kind(P, TK_COMMA) 3689 let bssu: i64 = parse_expr(P) 3690 match_kind(P, TK_COMMA) 3691 let ossu: i64 = parse_expr(P) 3692 match_kind(P, TK_RPAREN) 3693 return ir_emit_simd_vssubu_i16_x16(P.current_bb, assu, bssu, ossu) 3694 } 3695 } 3696 // Per-lane min/max/add/sub/mul: 25-char builtins (vXXX_lane). 3697 if streq_n(name, "__simd_vmin_lane_i16_x16", 24) == 1 { 3698 if peek_kind(P) == TK_LPAREN { 3699 advance_tok(P) 3700 let amn: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3701 let bmn: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3702 let omn: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3703 return ir_emit_simd_vmin_lane_i16_x16(P.current_bb, amn, bmn, omn) 3704 } 3705 } 3706 if streq_n(name, "__simd_vmax_lane_i16_x16", 24) == 1 { 3707 if peek_kind(P) == TK_LPAREN { 3708 advance_tok(P) 3709 let amx: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3710 let bmx: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3711 let omx: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3712 return ir_emit_simd_vmax_lane_i16_x16(P.current_bb, amx, bmx, omx) 3713 } 3714 } 3715 if streq_n(name, "__simd_vadd_lane_i16_x16", 24) == 1 { 3716 if peek_kind(P) == TK_LPAREN { 3717 advance_tok(P) 3718 let aad: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3719 let bad: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3720 let oad: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3721 return ir_emit_simd_vadd_lane_i16_x16(P.current_bb, aad, bad, oad) 3722 } 3723 } 3724 if streq_n(name, "__simd_vsub_lane_i16_x16", 24) == 1 { 3725 if peek_kind(P) == TK_LPAREN { 3726 advance_tok(P) 3727 let asb: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3728 let bsb: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3729 let osb: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3730 return ir_emit_simd_vsub_lane_i16_x16(P.current_bb, asb, bsb, osb) 3731 } 3732 } 3733 if streq_n(name, "__simd_vmul_lane_i16_x16", 24) == 1 { 3734 if peek_kind(P) == TK_LPAREN { 3735 advance_tok(P) 3736 let aml: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3737 let bml: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3738 let oml: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3739 return ir_emit_simd_vmul_lane_i16_x16(P.current_bb, aml, bml, oml) 3740 } 3741 } 3742 // 3-arg shifts: __simd_vsll/vsrl/vsra_i16_x16(*i64 src, i64 count, *i64 out) 3743 if streq_n(name, "__simd_vsll_i16_x16", 19) == 1 { 3744 if peek_kind(P) == TK_LPAREN { 3745 advance_tok(P) 3746 let asl: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3747 let csl: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3748 let osl: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3749 return ir_emit_simd_vsll_i16_x16(P.current_bb, asl, csl, osl) 3750 } 3751 } 3752 if streq_n(name, "__simd_vsrl_i16_x16", 19) == 1 { 3753 if peek_kind(P) == TK_LPAREN { 3754 advance_tok(P) 3755 let asr: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3756 let csr: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3757 let osr: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3758 return ir_emit_simd_vsrl_i16_x16(P.current_bb, asr, csr, osr) 3759 } 3760 } 3761 if streq_n(name, "__simd_vsra_i16_x16", 19) == 1 { 3762 if peek_kind(P) == TK_LPAREN { 3763 advance_tok(P) 3764 let asa2: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3765 let csa2: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3766 let osa2: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3767 return ir_emit_simd_vsra_i16_x16(P.current_bb, asa2, csa2, osa2) 3768 } 3769 } 3770 if streq_n(name, "__simd_vreduce_sum_i16_x16", 26) == 1 { 3771 if peek_kind(P) == TK_LPAREN { 3772 advance_tok(P) 3773 let prs: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3774 return ir_emit_simd_vreduce_sum_i16_x16(P.current_bb, prs) 3775 } 3776 } 3777 if streq_n(name, "__simd_vbroadcast_i16_x16", 25) == 1 { 3778 if peek_kind(P) == TK_LPAREN { 3779 advance_tok(P) 3780 let sbc: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3781 let obc: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3782 return ir_emit_simd_vbroadcast_i16_x16(P.current_bb, sbc, obc) 3783 } 3784 } 3785 // i8x32 set 3786 if streq_n(name, "__simd_vadd_i8_x32", 18) == 1 { 3787 if peek_kind(P) == TK_LPAREN { 3788 advance_tok(P) 3789 let a8a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3790 let b8a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3791 let o8a: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3792 return ir_emit_simd_vadd_i8_x32(P.current_bb, a8a, b8a, o8a) 3793 } 3794 } 3795 if streq_n(name, "__simd_vsub_i8_x32", 18) == 1 { 3796 if peek_kind(P) == TK_LPAREN { 3797 advance_tok(P) 3798 let a8s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3799 let b8s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3800 let o8s: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3801 return ir_emit_simd_vsub_i8_x32(P.current_bb, a8s, b8s, o8s) 3802 } 3803 } 3804 if streq_n(name, "__simd_vsadd_i8_x32", 19) == 1 { 3805 if peek_kind(P) == TK_LPAREN { 3806 advance_tok(P) 3807 let a8sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3808 let b8sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3809 let o8sa: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3810 return ir_emit_simd_vsadd_i8_x32(P.current_bb, a8sa, b8sa, o8sa) 3811 } 3812 } 3813 if streq_n(name, "__simd_vssub_i8_x32", 19) == 1 { 3814 if peek_kind(P) == TK_LPAREN { 3815 advance_tok(P) 3816 let a8ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3817 let b8ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3818 let o8ss: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3819 return ir_emit_simd_vssub_i8_x32(P.current_bb, a8ss, b8ss, o8ss) 3820 } 3821 } 3822 if streq_n(name, "__simd_vreduce_sum_i8_x32", 25) == 1 { 3823 if peek_kind(P) == TK_LPAREN { 3824 advance_tok(P) 3825 let p8r: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3826 return ir_emit_simd_vreduce_sum_i8_x32(P.current_bb, p8r) 3827 } 3828 } 3829 if streq_n(name, "__simd_vbroadcast_i8_x32", 24) == 1 { 3830 if peek_kind(P) == TK_LPAREN { 3831 advance_tok(P) 3832 let s8b: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3833 let o8b: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3834 return ir_emit_simd_vbroadcast_i8_x32(P.current_bb, s8b, o8b) 3835 } 3836 } 3837 // i32x8 set: names 17-24 chars long. 3838 if streq_n(name, "__simd_vadd_i32_x8", 18) == 1 { 3839 if peek_kind(P) == TK_LPAREN { 3840 advance_tok(P) 3841 let a32a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3842 let b32a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3843 let o32a: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3844 return ir_emit_simd_vadd_i32_x8(P.current_bb, a32a, b32a, o32a) 3845 } 3846 } 3847 if streq_n(name, "__simd_vsub_i32_x8", 18) == 1 { 3848 if peek_kind(P) == TK_LPAREN { 3849 advance_tok(P) 3850 let a32s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3851 let b32s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3852 let o32s: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3853 return ir_emit_simd_vsub_i32_x8(P.current_bb, a32s, b32s, o32s) 3854 } 3855 } 3856 if streq_n(name, "__simd_vmul_i32_x8", 18) == 1 { 3857 if peek_kind(P) == TK_LPAREN { 3858 advance_tok(P) 3859 let a32m: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3860 let b32m: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3861 let o32m: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3862 return ir_emit_simd_vmul_i32_x8(P.current_bb, a32m, b32m, o32m) 3863 } 3864 } 3865 if streq_n(name, "__simd_vsadd_i32_x8", 19) == 1 { 3866 if peek_kind(P) == TK_LPAREN { 3867 advance_tok(P) 3868 let a32sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3869 let b32sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3870 let o32sa: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3871 return ir_emit_simd_vsadd_i32_x8(P.current_bb, a32sa, b32sa, o32sa) 3872 } 3873 } 3874 if streq_n(name, "__simd_vssub_i32_x8", 19) == 1 { 3875 if peek_kind(P) == TK_LPAREN { 3876 advance_tok(P) 3877 let a32ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3878 let b32ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3879 let o32ss: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3880 return ir_emit_simd_vssub_i32_x8(P.current_bb, a32ss, b32ss, o32ss) 3881 } 3882 } 3883 if streq_n(name, "__simd_vreduce_sum_i32_x8", 25) == 1 { 3884 if peek_kind(P) == TK_LPAREN { 3885 advance_tok(P) 3886 let p32r: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3887 return ir_emit_simd_vreduce_sum_i32_x8(P.current_bb, p32r) 3888 } 3889 } 3890 if streq_n(name, "__simd_vbroadcast_i32_x8", 24) == 1 { 3891 if peek_kind(P) == TK_LPAREN { 3892 advance_tok(P) 3893 let s32b: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3894 let o32b: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3895 return ir_emit_simd_vbroadcast_i32_x8(P.current_bb, s32b, o32b) 3896 } 3897 } 3898 // i64x4 set 3899 if streq_n(name, "__simd_vadd_i64_x4", 18) == 1 { 3900 if peek_kind(P) == TK_LPAREN { 3901 advance_tok(P) 3902 let a64a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3903 let b64a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3904 let o64a: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3905 return ir_emit_simd_vadd_i64_x4(P.current_bb, a64a, b64a, o64a) 3906 } 3907 } 3908 if streq_n(name, "__simd_vsub_i64_x4", 18) == 1 { 3909 if peek_kind(P) == TK_LPAREN { 3910 advance_tok(P) 3911 let a64s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3912 let b64s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3913 let o64s: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3914 return ir_emit_simd_vsub_i64_x4(P.current_bb, a64s, b64s, o64s) 3915 } 3916 } 3917 if streq_n(name, "__simd_vmul_i64_x4", 18) == 1 { 3918 if peek_kind(P) == TK_LPAREN { 3919 advance_tok(P) 3920 let a64m: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3921 let b64m: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3922 let o64m: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3923 return ir_emit_simd_vmul_i64_x4(P.current_bb, a64m, b64m, o64m) 3924 } 3925 } 3926 if streq_n(name, "__simd_vsadd_i64_x4", 19) == 1 { 3927 if peek_kind(P) == TK_LPAREN { 3928 advance_tok(P) 3929 let a64sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3930 let b64sa: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3931 let o64sa: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3932 return ir_emit_simd_vsadd_i64_x4(P.current_bb, a64sa, b64sa, o64sa) 3933 } 3934 } 3935 if streq_n(name, "__simd_vssub_i64_x4", 19) == 1 { 3936 if peek_kind(P) == TK_LPAREN { 3937 advance_tok(P) 3938 let a64ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3939 let b64ss: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3940 let o64ss: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3941 return ir_emit_simd_vssub_i64_x4(P.current_bb, a64ss, b64ss, o64ss) 3942 } 3943 } 3944 if streq_n(name, "__simd_vreduce_sum_i64_x4", 25) == 1 { 3945 if peek_kind(P) == TK_LPAREN { 3946 advance_tok(P) 3947 let p64r: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3948 return ir_emit_simd_vreduce_sum_i64_x4(P.current_bb, p64r) 3949 } 3950 } 3951 if streq_n(name, "__simd_vbroadcast_i64_x4", 24) == 1 { 3952 if peek_kind(P) == TK_LPAREN { 3953 advance_tok(P) 3954 let s64b: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3955 let o64b: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3956 return ir_emit_simd_vbroadcast_i64_x4(P.current_bb, s64b, o64b) 3957 } 3958 } 3959 // Bit-rotate builtins -- C bootstrap parity (parse.c OP_ROTL64 / 3960 // OP_ROTR64). __rotl64(value, count) / __rotr64(value, count). 3961 // Lowered to x86 ROLQ/RORQ %cl (single instruction). Without 3962 // these the call fell through to an unresolved function and 3963 // silently returned op0 -- a no-op rotate that broke SHA-512. 3964 if streq_n(name, "__rotl64", 8) == 1 { 3965 if peek_kind(P) == TK_LPAREN { 3966 advance_tok(P) 3967 let rlv: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3968 let rln: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3969 return ir_emit_binop(P.current_bb, OP_ROTL64, rlv, rln, ir_type_i64()) 3970 } 3971 } 3972 if streq_n(name, "__rotr64", 8) == 1 { 3973 if peek_kind(P) == TK_LPAREN { 3974 advance_tok(P) 3975 let rrv: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3976 let rrn: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3977 return ir_emit_binop(P.current_bb, OP_ROTR64, rrv, rrn, ir_type_i64()) 3978 } 3979 } 3980 // LN1 wrap-around-by-intent intrinsics: __wrap_add/__wrap_sub/__wrap_mul(a, b) emit the plain 3981 // binop and are EXEMPT from the --chkarith overflow check (the Zig +% / Rust wrapping_* contract). 3982 // Under the default mode they are exactly `+ - *` (same int_binop_result_type ruler), so a 3983 // hash written with them builds byte-identically today and keeps building once the mode flips. 3984 if streq_n(name, "__wrap_add", 10) == 1 { 3985 if peek_kind(P) == TK_LPAREN { 3986 advance_tok(P) 3987 let wa1: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3988 let wa2: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3989 return ir_emit_binop(P.current_bb, OP_ADD, wa1, wa2, int_binop_result_type(P, wa1, wa2)) 3990 } 3991 } 3992 if streq_n(name, "__wrap_sub", 10) == 1 { 3993 if peek_kind(P) == TK_LPAREN { 3994 advance_tok(P) 3995 let ws1: i64 = parse_expr(P); match_kind(P, TK_COMMA) 3996 let ws2: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 3997 return ir_emit_binop(P.current_bb, OP_SUB, ws1, ws2, int_binop_result_type(P, ws1, ws2)) 3998 } 3999 } 4000 if streq_n(name, "__wrap_mul", 10) == 1 { 4001 if peek_kind(P) == TK_LPAREN { 4002 advance_tok(P) 4003 let wm1: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4004 let wm2: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4005 return ir_emit_binop(P.current_bb, OP_MUL, wm1, wm2, int_binop_result_type(P, wm1, wm2)) 4006 } 4007 } 4008 // LN4 OWNERSHIP MOVE: `__move(p)` is the DECLARED transfer point for an owned buffer -- the 4009 // __wrap_add contract, an intrinsic stating an intent the type system cannot express. It is a 4010 // PURE MARKER: it returns p's own value id and emits NO IR at all, so `f(__move(p))` is 4011 // byte-for-byte `f(p)` under BOTH modes and adding a move to real code costs nothing at runtime. 4012 // The identifier token is captured BEFORE parse_expr consumes it; parse_expr then runs the 4013 // ordinary read path, so moving an ALREADY moved or released name is refused by own_check_use 4014 // there rather than by a second rule here -- one use checker, not two. 4015 if streq_n(name, "__move", 6) == 1 { 4016 if peek_kind(P) == TK_LPAREN { 4017 advance_tok(P) 4018 let mv_tok: *Tok = tok_at(P.toks, P.pos) 4019 let mv_v: i64 = parse_expr(P) 4020 match_kind(P, TK_RPAREN) 4021 own_move_arg(P, mv_tok) 4022 return mv_v 4023 } 4024 } 4025 // Wide multiply -- G2 substrate. __umulhi64(a, b) = the HIGH 64 bits of the 4026 // unsigned 64x64 product (x86 mulq's rdx). Paired with normal `*` (the low 4027 // 64 bits) it yields the full 128-bit product, so crypto field arithmetic can 4028 // use 4x64-bit limbs (16 partial products) instead of 8x32 (64). 4029 if streq_n(name, "__umulhi64", 10) == 1 { 4030 if peek_kind(P) == TK_LPAREN { 4031 advance_tok(P) 4032 let uma: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4033 let umb: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4034 return ir_emit_binop(P.current_bb, OP_UMULHI, uma, umb, ir_type_i64()) 4035 } 4036 } 4037 // Hardware CRC-32C (SSE4.2) -- __crc32_u64(crc, data) folds the 64-bit 4038 // `data` word into the running `crc` accumulator (x86 crc32q). Pure, 4039 // 2 i64 operands -> i64. Checksums, packet validation, hash fingerprints. 4040 if streq_n(name, "__crc32_u64", 11) == 1 { 4041 if peek_kind(P) == TK_LPAREN { 4042 advance_tok(P) 4043 let crca: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4044 let crcd: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4045 return ir_emit_binop(P.current_bb, OP_CRC32, crca, crcd, ir_type_i64()) 4046 } 4047 } 4048 // BMI2 parallel bit DEPOSIT -- __pdep64(value, mask) scatters the low bits 4049 // of `value` into the set-bit positions of `mask` (x86 pdep). Pure, 2 i64 4050 // operands -> i64. varint-encode, bitboard-scatter, bit-interleave. 4051 if streq_n(name, "__pdep64", 8) == 1 { 4052 if peek_kind(P) == TK_LPAREN { 4053 advance_tok(P) 4054 let pdv: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4055 let pdm: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4056 return ir_emit_binop(P.current_bb, OP_PDEP, pdv, pdm, ir_type_i64()) 4057 } 4058 } 4059 // BMI2 parallel bit EXTRACT -- __pext64(value, mask) gathers the `value` 4060 // bits at the set positions of `mask` down to the low bits (x86 pext; the 4061 // inverse of pdep). Pure, 2 i64 operands -> i64. varint-decode, 4062 // bitboard-gather, unicode-transcode, compression. 4063 if streq_n(name, "__pext64", 8) == 1 { 4064 if peek_kind(P) == TK_LPAREN { 4065 advance_tok(P) 4066 let pxv: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4067 let pxm: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4068 return ir_emit_binop(P.current_bb, OP_PEXT, pxv, pxm, ir_type_i64()) 4069 } 4070 } 4071 // Native add-with-carry -- G3 substrate. __adc_acc(acc_ptr, lo, hi) adds the 4072 // 128-bit (hi:lo) into the 3-word accumulator {acc[0],acc[1],acc[2]} with carry 4073 // (contiguous addq;adcq;adcq). Void/statement-form (returns const 0). 4074 if streq_n(name, "__adc_acc", 9) == 1 { 4075 if peek_kind(P) == TK_LPAREN { 4076 advance_tok(P) 4077 let aa_p: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4078 let aa_lo: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4079 let aa_hi: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4080 ir_emit_adc_acc(P.current_bb, aa_p, aa_lo, aa_hi) 4081 return safe_const_i64(P, 0, "parse.nx:adc-acc-0" as *u8) 4082 } 4083 } 4084 // CPU feature detection -- the BMI2/ADX gate. __cpuid_ebx(leaf, subleaf) runs 4085 // x86 cpuid and returns the EBX register (zero-extended). cpuid(7,0):EBX has 4086 // bit-8=BMI2, bit-19=ADX. Pure (deterministic for a given CPU). 4087 if streq_n(name, "__cpuid_ebx", 11) == 1 { 4088 if peek_kind(P) == TK_LPAREN { 4089 advance_tok(P) 4090 let cq_l: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4091 let cq_s: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4092 return ir_emit_binop(P.current_bb, OP_CPUID_EBX, cq_l, cq_s, ir_type_i64()) 4093 } 4094 } 4095 // Scalar bit unops -- C bootstrap parity (parse.c OP_BSWAP64/CLZ32/ 4096 // CTZ32/POPCNT64). Each takes exactly 1 argument. 4097 if streq_n(name, "__bswap64", 9) == 1 { 4098 if peek_kind(P) == TK_LPAREN { 4099 advance_tok(P) 4100 let bsv: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4101 return ir_emit_unop(P.current_bb, OP_BSWAP64, bsv, ir_type_i64()) 4102 } 4103 } 4104 if streq_n(name, "__clz32", 7) == 1 { 4105 if peek_kind(P) == TK_LPAREN { 4106 advance_tok(P) 4107 let czv: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4108 return ir_emit_unop(P.current_bb, OP_CLZ32, czv, ir_type_i64()) 4109 } 4110 } 4111 if streq_n(name, "__ctz32", 7) == 1 { 4112 if peek_kind(P) == TK_LPAREN { 4113 advance_tok(P) 4114 let tzv: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4115 return ir_emit_unop(P.current_bb, OP_CTZ32, tzv, ir_type_i64()) 4116 } 4117 } 4118 if streq_n(name, "__popcnt64", 10) == 1 { 4119 if peek_kind(P) == TK_LPAREN { 4120 advance_tok(P) 4121 let pcv: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4122 return ir_emit_unop(P.current_bb, OP_POPCNT64, pcv, ir_type_i64()) 4123 } 4124 } 4125 // __rdtsc() -- zero-arg cycle-counter read. The unop carries a dummy 4126 // const-0 operand (the lowering ignores it and reads the HW counter). 4127 if streq_n(name, "__rdtsc", 7) == 1 { 4128 if peek_kind(P) == TK_LPAREN { 4129 advance_tok(P); match_kind(P, TK_RPAREN) 4130 let rd0: i64 = safe_const_i64(P, 0, "__rdtsc" as *u8) 4131 return ir_emit_unop(P.current_bb, OP_RDTSC, rd0, ir_type_i64()) 4132 } 4133 } 4134 // Atomic intrinsics -- C bootstrap parity (parse.c). Memory order 4135 // is the last argument (NX_MO_* const). 4136 if streq_n(name, "__atomic_load_i64", 17) == 1 { 4137 if peek_kind(P) == TK_LPAREN { 4138 advance_tok(P) 4139 let al_a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4140 let al_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4141 return ir_emit_atomic_load_i64(P.current_bb, al_a, al_mo) 4142 } 4143 } 4144 if streq_n(name, "__atomic_store_i64", 18) == 1 { 4145 if peek_kind(P) == TK_LPAREN { 4146 advance_tok(P) 4147 let as_a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4148 let as_v: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4149 let as_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4150 ir_emit_atomic_store_i64(P.current_bb, as_a, as_v, as_mo) 4151 return safe_const_i64(P, 0, "parse.nx:atomic-store-0" as *u8) 4152 } 4153 } 4154 if streq_n(name, "__atomic_cas_i64", 16) == 1 { 4155 if peek_kind(P) == TK_LPAREN { 4156 advance_tok(P) 4157 let cs_a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4158 let cs_e: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4159 let cs_n: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4160 let cs_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4161 return ir_emit_atomic_cas_i64(P.current_bb, cs_a, cs_e, cs_n, cs_mo) 4162 } 4163 } 4164 if streq_n(name, "__atomic_faa_i64", 16) == 1 { 4165 if peek_kind(P) == TK_LPAREN { 4166 advance_tok(P) 4167 let fa_a: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4168 let fa_d: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4169 let fa_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4170 return ir_emit_atomic_faa_i64(P.current_bb, fa_a, fa_d, fa_mo) 4171 } 4172 } 4173 if streq_n(name, "__atomic_fence", 14) == 1 { 4174 if peek_kind(P) == TK_LPAREN { 4175 advance_tok(P) 4176 let fe_mo: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4177 ir_emit_atomic_fence(P.current_bb, fe_mo) 4178 return safe_const_i64(P, 0, "parse.nx:atomic-fence-0" as *u8) 4179 } 4180 } 4181 // __thread_clone(stack_top, entry_fn, ctx) -> child_tid. 4182 if streq_n(name, "__thread_clone", 14) == 1 { 4183 if peek_kind(P) == TK_LPAREN { 4184 advance_tok(P) 4185 let tc_s: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4186 let tc_e: i64 = parse_expr(P); match_kind(P, TK_COMMA) 4187 let tc_c: i64 = parse_expr(P); match_kind(P, TK_RPAREN) 4188 return ir_emit_thread_clone(P.current_bb, tc_s, tc_e, tc_c) 4189 } 4190 } 4191 return -1 4192} 4193 4194func parse_primary_call(P: *Parser, name: *u8) -> i64 { 4195 // CALL-SITE ANCHOR (2026-08-05). Capture the position of the CALL ITSELF at entry: the caller 4196 // already consumed the identifier, so it sits one token back. Reading P.pos at ERROR time 4197 // instead points at whatever the parser reached AFTER the call -- measured: the caret printed 4198 // under the NEXT statement (sys_exit(0)) for an error on the line above it. 4199 // A CARET THAT POINTS AT THE WRONG LINE IS WORSE THAN NO CARET: it sends the reader to 4200 // innocent code, which is exactly the confusion the feature exists to remove. 4201 var call_ln: i64 = 0 4202 var call_cl: i64 = 0 4203 if P.pos >= 1 { 4204 let ct: *Tok = tok_at(P.toks, P.pos - 1) 4205 call_ln = ct.line 4206 call_cl = ct.col 4207 } 4208 advance_tok(P) 4209 // 24 slots -- Instr has op0..op23 (24 operand slots after the 4210 // 2026-06-18 IR extension to support >16-arg calls, e.g. bi-predicted 4211 // H.264 chroma reconstruction). Was 4 then 8 then 16 slots; silent 4212 // overflow past the buffer corrupted adjacent memory and made 4213 // ir_emit_call read garbage into the high op slots. See PREVENT lint 4214 // bench/nx_call_arity_overflow_audit.sh. 4215 let args_raw: *u8 = sys_mmap(24 * 8 + 16) 4216 let args: *i64 = args_raw as *i64 4217 var n_args: i64 = 0 4218 // LN2 narrowing by assertion: `nx_assert_ptr(p, ...)` / `nx_assert_ptr(p as *u8, ...)` aborts 4219 // when p is null, so p is non-null for the rest of the block. And `sys_exit(...)` ends the path 4220 // (the early-exit idiom `if p == 0 { sys_exit(1) }`). Token shape only; inert unless --optenforce. 4221 if g_optenforce_live == 1 { 4222 if streq_n(name, "nx_assert_ptr", 13) == 1 { 4223 let oa0: *Tok = tok_at(P.toks, P.pos) 4224 let oa1: *Tok = tok_at(P.toks, P.pos + 1) 4225 if oa0.kind == TK_IDENT { 4226 var oa_ok: i64 = 0 4227 if oa1.kind == TK_COMMA { oa_ok = 1 } 4228 if oa1.kind == TK_AS { oa_ok = 1 } 4229 if oa_ok == 1 { 4230 let oaL: *Local = find_local(P, tok_text_ptr(oa0)) 4231 if oaL != (0 as *Local) { if oaL.ty != (0 as *Type) { if oaL.ty.kind == TY_PTR { oaL.nn_checked = 1 } } } 4232 } 4233 } 4234 } 4235 if streq_n(name, "sys_exit", 8) == 1 { g_oe_term = 1 } 4236 } 4237 // LN5 RELEASE POINT: `sys_munmap(p, n)` / `sys_munmap(p as *T, n)` ends p's ownership. The token 4238 // is captured HERE, BEFORE the arguments are parsed, and the local is marked AFTER the closing 4239 // paren -- an ordering that is load-bearing twice over. Capturing first is the only moment the 4240 // identifier is still at a known position; marking last keeps the release's OWN read of p legal 4241 // (it is the last legitimate use) and makes a DOUBLE release refuse for free, because the second 4242 // statement's read of the name is itself the use being checked -- no separate double-free rule. 4243 // A field or expression argument (`sys_munmap(ctx.buf, n)`) does not match and is a declared 4244 // floor: this rung tracks named locals only. 4245 var own_rel_tok: *Tok = 0 as *Tok 4246 if own_check_live() == 1 { 4247 if streq_n(name, "sys_munmap", 10) == 1 { 4248 let or0: *Tok = tok_at(P.toks, P.pos) 4249 let or1: *Tok = tok_at(P.toks, P.pos + 1) 4250 if or0.kind == TK_IDENT { 4251 var or_ok: i64 = 0 4252 if or1.kind == TK_COMMA { or_ok = 1 } 4253 if or1.kind == TK_AS { or_ok = 1 } 4254 if or_ok == 1 { own_rel_tok = or0 } 4255 } 4256 } 4257 } 4258 if peek_kind(P) != TK_RPAREN { 4259 let a0: i64 = parse_expr(P) 4260 args[n_args] = a0 4261 n_args = n_args + 1 4262 while peek_kind(P) == TK_COMMA { 4263 advance_tok(P) 4264 if n_args >= 24 { 4265 parse_die("function call exceeds 24 args (IR cap)" as *u8, 39) 4266 } 4267 let an: i64 = parse_expr(P) 4268 args[n_args] = an 4269 n_args = n_args + 1 4270 } 4271 } 4272 match_kind(P, TK_RPAREN) 4273 // LN5: the release lands only now -- see the capture block above for why the order matters. 4274 if own_rel_tok != (0 as *Tok) { own_mark_named(P, own_rel_tok, OWN_RELEASED) } 4275 var nlen: i64 = 0 4276 while name[nlen] != 0 { nlen = nlen + 1 } 4277 let callee: *Function = find_function(P.module, name, nlen) 4278 if callee == (0 as *Function) { 4279 // fp(args): if `name` is a func-pointer-typed LOCAL, emit an INDIRECT call through it (call *%r11) 4280 // instead of failing as an undefined function. Completes the func-pointer feature (the thread-pool 4281 // `fp(task.ctx)` shape). MVP caps at 6 args (register-only codegen). 4282 let fpl: *Local = find_local(P, name) 4283 if fpl != (0 as *Local) { 4284 if fpl.ty != (0 as *Type) { 4285 if fpl.ty.kind == TY_FUNC { 4286 if n_args > NX_FNPTR_MAX_ARGS { parse_die("indirect fn-ptr call exceeds max args (IR operand cap)" as *u8, 32) } 4287 var fpv: i64 = fpl.value_id 4288 if fpl.is_alloca == 1 { fpv = ir_emit_load(P.current_bb, fpl.value_id, fpl.ty) } 4289 var fret: *Type = fpl.ty.pointee 4290 if fret == (0 as *Type) { fret = ir_type_i64() } 4291 return ir_emit_call_indirect(P.current_bb, fpv, fret, args, n_args) 4292 } 4293 } 4294 } 4295 // Unresolved call. prepass_register_funcs registers EVERY real 4296 // function before any body is parsed, so reaching here means 4297 // `name` is genuinely undefined. Returning 0 here used to 4298 // SILENTLY produce value-id-0 (== first arg), turning a missing 4299 // compiler intrinsic into a no-op -- the SHA-512/__rotr64 bug 4300 // (SITES-LIVE 2026-05-27). Fail LOUD instead. A `__`-prefixed 4301 // name that lands here is an UNHANDLED INTRINSIC (it should have 4302 // matched parse_primary_intrinsic); anything else is a typo or a 4303 // missing import. Either way it must never silently no-op. 4304 if name[0] == 0x5F { 4305 if name[1] == 0x5F { 4306 // LN42 (2026-09-03): this refusal is CORRECT and was unreadable. It printed a bare sentence with 4307 // no file, no line and no caret, so a census could not group it and a reader could not find it. 4308 // It also matters WHICH kind of absence this is, and the two need opposite actions: an intrinsic 4309 // whose emitter exists but whose NAME was never routed here is BUILT AND UNWIRED (that was 4310 // __q4k_sb_dot, 32 programs, fixed the same day); an intrinsic with no emitter anywhere is simply 4311 // NOT IMPLEMENTED, and calling it is the program asking for a feature the compiler does not have. 4312 // Saying "unhandled" for both sent every reader looking for a missing line in this file. 4313 nx_diag_organ_at(call_ln) 4314 nx_diag_puts(": this __ intrinsic has no implementation in this compiler: " as *u8) 4315 nx_diag_puts(name) 4316 nx_diag_puts("\n" as *u8) 4317 nx_diag_caret(call_ln, nx_pd_col) 4318 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) 4319 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) 4320 nx_diag_note_error() 4321 return 0 4322 } 4323 } 4324 nx_dym_suggest_fn(P.module, name, nlen) 4325 nx_diag_at(call_ln) 4326 nx_diag_puts(": I do not know the name '" as *u8) 4327 sys_write(2, name, nlen) 4328 nx_diag_puts("' -- it is called here, but nothing in this program defines it.\n" as *u8) 4329 nx_diag_caret(call_ln, call_cl) 4330 nx_diag_why_undefined() 4331 nx_dym_note2(P, name, nlen) 4332 nx_diag_note_error() 4333 // RECOVERY: value id 0. Mechanically safe for the parser (it was the 4334 // old silent behavior); the end-of-parse gate keeps it out of codegen. 4335 return 0 4336 } 4337 // ---- CALL-ARITY CHECK (2026-07-20) ------------------------------------ 4338 // nx_cc had NO arity checking: a call missing an argument compiled SILENTLY 4339 // and the callee read a GARBAGE REGISTER -- then WROTE THROUGH IT when the 4340 // parameter was an out-pointer. Symptoms are protean (impossible counter 4341 // values; a SEGV that MOVES when you refactor, because the register content 4342 // shifts), which is why it cost ~4 debug cycles in nx_step_tess2 and got 4343 // banked as a standing ★★★ gotcha. Same family as the undefined-function 4344 // fix above: a silent wrong-value is strictly worse than a loud stop. 4345 // 4346 // COVERAGE IS NOW TOTAL. This check was first landed gated on `callee.n_blocks > 0` (the 4347 // codebase's own "has a real body" test) because prepass_register_funcs registered a stub per 4348 // name WITHOUT parsing its parameter list, so a not-yet-defined callee reported n_params == 0 4349 // and checking it would have rejected every forward call -- including this compiler's own 4350 // source. That root cause is now FIXED at the source: prepass_count_params records the declared 4351 // arity on the stub before any body is parsed, so EVERY callee reachable by name has a known 4352 // signature and the guard is no longer needed. Forward calls and mutual recursion are covered. 4353 if n_args != callee.n_params { 4354 // 5W+H VOICE (2026-08-13): the old text ("call arity mismatch: f expects 2 arg(s), got 1") 4355 // stated the counts but not the CONSEQUENCE -- and this defect's symptom is protean 4356 // (garbage register reads that move when unrelated code moves), which is exactly what a 4357 // reader needs told, because the crash never points here. 4358 nx_diag_at(call_ln) 4359 nx_diag_puts(": '" as *u8) 4360 sys_write(2, name, nlen) 4361 nx_diag_puts("' is called here with " as *u8) 4362 nx_put_dec_err(n_args) 4363 nx_diag_puts(" value(s), but it is defined to take " as *u8) 4364 nx_put_dec_err(callee.n_params) 4365 nx_diag_puts(".\n" as *u8) 4366 nx_diag_caret(call_ln, call_cl) 4367 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) 4368 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) 4369 nx_diag_note_error() 4370 } 4371 // ---- CALL ARGUMENT TYPE CHECK (2026-08-01) ---------------------------- 4372 // nx_cc checked ARITY but never TYPES: a pointer passed where an i64 was declared, 4373 // and an integer passed where a pointer was declared, both compiled CLEAN. That is 4374 // the root of the silent-acceptance family. It is not theoretical -- it is how the 4375 // sev-8 evidence bug happened (debt 1785518763): ev_parse_pass passed an mmap'd 4376 // *i64 out-pointer into `kl: i64`, where it became the bound of `while i <= n - kl`; 4377 // the loop never executed and an evidence organ reported a ZERO tally for every gate. 4378 // 4379 // DELIBERATELY NARROW: POINTER-vs-INTEGER only. That distinction is unambiguous and 4380 // is the class that produces wild-pointer and impossible-bound bugs. Integer WIDTH 4381 // (i32 into i64) and struct identity are NOT checked here -- the corpus converts 4382 // freely between integer widths and rejecting that would be a different, much larger 4383 // change. Widen only as far as the evidence supports. 4384 // 4385 // LN6: a per-task context crossing the thread pool must come from a struct marked `send` 4386 // (sync_send_check; name-keyed on nx_pool_submit, abstains on anything it cannot type). 4387 if g_sendcheck_live == 1 { sync_send_check(P, name, nlen, args, n_args, call_ln, call_cl) } 4388 // SKIPPED when the callee's mask is 0 = signature not yet parsed (forward call to a 4389 // stub). Guessing a signature there is how the arity check first broke the compiler's 4390 // own source; a check that cannot know must decline, not invent. 4391 if callee.param_ptr_mask != 0 { 4392 var ai: i64 = 0 4393 while ai < n_args { 4394 let av: *Value = val_at(P.current_fn, args[ai]) 4395 var arg_is_ptr: i64 = 0 4396 var arg_known: i64 = 0 4397 if av != (0 as *Value) { 4398 let aty: *Type = av.ty 4399 if aty != (0 as *Type) { 4400 arg_known = 1 4401 if aty.kind == TY_PTR { arg_is_ptr = 1 } 4402 // an integer LITERAL is routinely used as a null/sentinel pointer; 4403 // only a *typed* value carries a real claim about pointerness. 4404 if av.kind == VK_CONST_INT { arg_known = 0 } 4405 } 4406 } 4407 var want_ptr: i64 = 0 4408 if (callee.param_ptr_mask & (1 << ai)) != 0 { want_ptr = 1 } 4409 // ONLY THE UNAMBIGUOUS DIRECTION: a value typed TY_PTR is definitely a 4410 // pointer, so passing it to an integer parameter is definitely wrong. The 4411 // reverse is NOT decidable here: an argument typed i64 may be a genuine 4412 // integer OR the PLACEHOLDER type of a call to a function defined later with 4413 // no forward declaration (the prepass stub carries i64 until its body is 4414 // parsed). Measured: checking that direction gave a 106/120 false-positive 4415 // rate, all of it placeholder types, not real defects. 4416 // This keeps the direction that produced the sev-8 ev_num_after silent-zero 4417 // (a *i64 out-pointer landing in `kl: i64`) and drops the one that cannot be 4418 // decided without a real type-inference pass. Widen only as far as the 4419 // evidence supports. 4420 // POINTER-INTO-INTEGER ONLY -- and this narrowness is MEASURED, not timid. 4421 // 4422 // A value typed TY_PTR is definitely a pointer, so handing it to an integer 4423 // parameter is definitely wrong: that is the sev-8 ev_num_after shape (an 4424 // mmap'd *i64 becoming a loop bound) and it costs 0 false positives over the 4425 // corpus sample. 4426 // 4427 // The REVERSE direction is enabled by flipping the commented line below, and 4428 // it was TRIED on 2026-08-01. Two separate causes of i64-looking arguments 4429 // were found and fixed -- return types were hardcoded ir_type_i64() for every 4430 // function, and forward stubs carried an i64 placeholder (prepass now records 4431 // return pointer-ness). Both real bugs, both fixed. But the corpus STILL 4432 // rejects 57/70 on `sys_munmap` arg 1 from inside the syscall layer's own 4433 // expansion, and that residue is NOT yet explained. Isolated repros of every 4434 // shape tried -- casts, pointer locals, call results, forward decls -- are all 4435 // quiet, so a third cause remains unfound. 4436 // DO NOT ENABLE IT UNTIL THAT RESIDUE IS EXPLAINED. Shipping it would reject 4437 // most of the corpus for a reason nobody has yet named, which is not a fix. 4438 var bad: i64 = 0 4439 if arg_known == 1 { 4440 if arg_is_ptr == 1 { if want_ptr == 0 { bad = 1 } } 4441 // BOTH DIRECTIONS: still DISABLED, and the residue is now PARTLY explained + MEASURED. 4442 // CAUSE FOUND AND FIXED 2026-08-05: the trailing `as T` cast MUTATED THE OPERAND'S 4443 // Value IN PLACE (parse_unary), so a local that ever appeared as `p as i64` read as 4444 // INTEGER at every LATER call -- exactly the syscall layer's timespec pattern before 4445 // sys_munmap(req). Cast now emits a typed identity; witnesses nx_probe_castmut_live.nx 4446 // (mutation reproduced pre-fix) + castmut2 (per-value, not check-wide). That fix is 4447 // LIVE (equiv 10/10 + selfhost, toolchain-promoted canary GREEN). 4448 // BUT IT IS NOT THE WHOLE RESIDUE, MEASURED NOT ASSUMED: flipping this line on the 4449 // FIXED compiler still fails 4 of 10 equiv rows -- nx_jpeg_ascii_test, 4450 // nx_tls13_client_session_recv_sh_test, nx_p256_keyshare_test, 4451 // nx_tls13_p256_loopback_test (build_b=1, i.e. the CHALLENGER refuses to compile them; 4452 // the other 6 rows + selfhost stay GREEN). Residue narrowed from 57/70 corpus targets 4453 // to 4 large crypto/codec modules = a SECOND, still-unnamed type-loss path. 4454 // NEXT INVESTIGATOR: build one of those 4 with a both-directions compiler and read the 4455 // exact `argument type mismatch` line + callee; that names cause #2. 4456 if arg_is_ptr != want_ptr { bad = 1 } 4457 } 4458 if bad == 1 { 4459 // parse_primary_call has no name token in scope; take the line from 4460 // the parser's current position, which is at/just past the call. 4461 let cur_tok: *Tok = tok_at(P.toks, P.pos) 4462 let cur_line: i64 = cur_tok.line 4463 nx_diag_organ_at(cur_line) 4464 sys_write(2, ": argument type mismatch in call to '" as *u8, 37) 4465 sys_write(2, name, nlen) 4466 sys_write(2, "': arg " as *u8, 7) 4467 nx_put_dec_err(ai + 1) 4468 if want_ptr == 1 { 4469 sys_write(2, " is an INTEGER but the parameter is a POINTER\n" as *u8, 46) 4470 } 4471 if want_ptr == 0 { 4472 sys_write(2, " is a POINTER but the parameter is an INTEGER\n" as *u8, 46) 4473 } 4474 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) 4475 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) 4476 nx_diag_note_error() 4477 } 4478 ai = ai + 1 4479 } 4480 } 4481 let r: i64 = ir_emit_call(P.current_bb, callee, args, n_args) 4482 // LN3 provenance capture: a call to sys_mmap with a COMPILE-TIME-CONSTANT byte count is the 4483 // one allocation whose extent the compiler can see (mconsts fold to VK_CONST_INT at parse 4484 // time, so `sys_mmap(CAP_CONST)` qualifies). Record the result id; the `as *T` cast chain 4485 // in parse_unary carries it forward and parse_stmt_let binds it onto the local. Name-keyed 4486 // on sys_mmap alone for now -- a declared floor, listed in the LN3 block's imprecision note. 4487 if n_args == 1 { 4488 if mc_name_eq(name, "sys_mmap" as *u8) == 1 { 4489 let pav: *Value = val_at(P.current_fn, args[0]) 4490 if pav != (0 as *Value) { 4491 if pav.kind == VK_CONST_INT { 4492 if pav.const_int > 0 { g_prov_val = r; g_prov_bytes = pav.const_int } 4493 } 4494 } 4495 } 4496 } 4497 if callee.ret_ty != (0 as *Type) { 4498 return parse_field_chain(P, r, callee.ret_ty) 4499 } 4500 return r 4501} 4502 4503// NUL-terminated name compare (2 data args -- keeps clear of the >=3-data-arg helper miscompile class) 4504func mc_name_eq(a: *u8, b: *u8) -> i64 { 4505 var i: i64 = 0 4506 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 4507 if b[i] != (0 as u8) { return 0 } 4508 return 1 4509} 4510 4511// Is `name` declared as a module `const` LATER in the token stream? Non-destructive: saves and 4512// restores P.pos exactly as the prepasses do. Only ever called on the give-up path of an identifier 4513// lookup, so the scan cost is paid solely on what is already an error. 4514func mconst_declared_later(P: *Parser, name: *u8) -> i64 { 4515 let saved: i64 = P.pos 4516 var found: i64 = 0 4517 var go: i64 = 1 4518 while go == 1 { 4519 let k: i64 = peek_kind(P) 4520 if k == TK_EOF { 4521 go = 0 4522 } else { 4523 if k == TK_CONST { 4524 advance_tok(P) 4525 if peek_kind(P) == TK_IDENT { 4526 let t: *Tok = advance_tok(P) 4527 if mc_name_eq(tok_text_ptr(t), name) == 1 { found = 1; go = 0 } 4528 } 4529 } else { 4530 advance_tok(P) 4531 } 4532 } 4533 } 4534 P.pos = saved 4535 return found 4536} 4537 4538func parse_primary_local_or_const(P: *Parser, name: *u8) -> i64 { 4539 let L: *Local = find_local(P, name) 4540 if L != (0 as *Local) { 4541 // LN4/LN5: THE read hook, and the reason this rung needs only one. EVERY bare use of a name 4542 // arrives here -- a deref base, a call argument, an initializer, the right-hand side of an 4543 // assignment -- because parse_primary_ident routes anything that is not `name(` to this 4544 // function. The identifier token is one position back (parse_primary_ident already consumed 4545 // it), the same idiom oe_handshake_set uses below. An assignment TARGET does NOT come 4546 // through here (parse_stmt_ident_assign consumes its own name), which is exactly what makes 4547 // `p = sys_mmap(n)` a legal re-initialisation after a move rather than a refused use. 4548 if P.pos >= 1 { own_check_use(P, L, tok_at(P.toks, P.pos - 1)) } 4549 var v: i64 = L.value_id 4550 var v_pass_ty: *Type = L.ty // type handed to parse_field_chain; unwrapped once for static-ptr subscripts 4551 if L.is_alloca { 4552 if L.ty_kind != TY_STRUCT { 4553 if L.ty_kind != TY_ARRAY { 4554 if peek_kind(P) != TK_DOT { 4555 if peek_kind(P) != TK_LBRACKET { 4556 // Load the local's value at its DECLARED type 4557 // (L.ty), not hardcoded i64. Required for u8 / 4558 // i32 / pointer locals to widen / read correctly. 4559 // ARRAY (like STRUCT) is EXCLUDED here: a bare array name 4560 // must DECAY to its frame address, not load its first bytes. 4561 var lty: *Type = L.ty 4562 if lty == (0 as *Type) { lty = ir_type_i64() } 4563 // STATIC SCALAR TYPE FIX (2026-08-01). A static's injected Local is deliberately 4564 // typed *(decl_ty) -- the slot ADDRESS -- so field/index chains get the right 4565 // stride (see inject_statics). But LOADING that slot yields a value of the 4566 // DECLARED type, so the loaded Value must carry L.ty.pointee, NOT L.ty. 4567 // Without this every `static X: i64` read is recorded as TY_PTR, and the call 4568 // argument type check added earlier today then rejects the VALID line 4569 // `cd2_sputi(cg_nn)` as "arg 1 is a POINTER but the parameter is an INTEGER". 4570 // ★★★★★A TYPE THAT WAS ONLY EVER USED FOR ADDRESSING BECAME A CLAIM ABOUT THE 4571 // VALUE THE MOMENT SOMETHING STARTED READING IT -- the slot type was harmlessly 4572 // wrong for as long as nothing consulted it, and the new checker is what turned 4573 // a latent mislabel into 1-in-10 organs refusing to compile. 4574 // Same VK_GLOBAL-gated unwrap already proven on the static-ptr field-read path 4575 // below; ordinary alloca locals are untouched, so they stay byte-identical. 4576 // CODEGEN IS UNCHANGED CORPUS-WIDE, measured not assumed: load_ty drives load 4577 // WIDTH, and the corpus has 366 i64 statics (8 bytes either way), 305 pointer 4578 // statics (8 bytes either way) and ZERO narrow (u8/i32/i16) scalar statics, the 4579 // only shape whose width this could alter. 4580 let lv_ld: *Value = val_at(P.current_fn, L.value_id) 4581 if lv_ld.kind == VK_GLOBAL { 4582 if L.ty != (0 as *Type) { 4583 if L.ty.pointee != (0 as *Type) { lty = L.ty.pointee } 4584 } 4585 } 4586 return ir_emit_load(P.current_bb, L.value_id, lty) 4587 } 4588 } 4589 } 4590 } 4591 } 4592 // F-meta-3 fix (Session 12): when the local is alloca-backed 4593 // AND its declared type is a pointer-to-struct (e.g. function 4594 // param `out: *Struct`), the alloca slot holds the POINTER 4595 // value, not the struct. parse_field_chain expects `base` to 4596 // BE the pointer; if we pass the alloca slot directly it does 4597 // GEP on the slot and reads stack garbage. Load the pointer 4598 // out of the slot first. Sibling fix to parse_stmt_ident_dot. 4599 if L.is_alloca == 1 { 4600 if L.ty != (0 as *Type) { 4601 if L.ty.kind == TY_PTR { 4602 if peek_kind(P) == TK_DOT { 4603 // Loaded value is the pointer itself (type L.ty). 4604 v = ir_emit_load(P.current_bb, L.value_id, L.ty) 4605 // Static-ptr FIELD-READ fix (2026-07-10; mirrors the LBRACKET multi-static fix 4606 // below): a STATIC's injected Local is typed *(decl_ty) -- the slot ADDRESS -- so 4607 // after the load v holds the REAL pointer, whose type is L.ty.pointee. Passing the 4608 // un-unwrapped **Struct made parse_field_chain's struct lookup FAIL -> documented 4609 // bail -> `g.field` READ returned the RAW POINTER (nx_static_field_probe witness). 4610 // VK_GLOBAL-gated so ordinary alloca ptr locals stay byte-identical (equiv-safe). 4611 let lv_d: *Value = val_at(P.current_fn, L.value_id) 4612 if lv_d.kind == VK_GLOBAL { 4613 if L.ty.pointee != (0 as *Type) { v_pass_ty = L.ty.pointee } 4614 } 4615 } 4616 // Multi-static bug fix: a STATIC pointer's value_id is a VK_GLOBAL (the slot ADDRESS), 4617 // which materialises as-address (leaq) and does NOT auto-load like an OP_ALLOCA. So a 4618 // `staticptr[i]` READ subscripted the slot address -> returned the pointer, not *pointer 4619 // (write worked, read was wrong -- silent corruption with >=1 static pointer read). Load 4620 // the pointer from the slot first, ONLY for VK_GLOBAL, so var/alloca pointers (which 4621 // auto-load) stay byte-identical -> equiv gate unaffected. 4622 if peek_kind(P) == TK_LBRACKET { 4623 let lv_g: *Value = val_at(P.current_fn, L.value_id) 4624 if lv_g.kind == VK_GLOBAL { 4625 // STATIC ARRAY vs STATIC POINTER -- they need OPPOSITE treatment here, and 4626 // conflating them is why `static a: [N]T` has been a documented daemon-killer. 4627 // A static POINTER's slot HOLDS a pointer, so it must be loaded. A static 4628 // ARRAY's slot IS the array, so its ADDRESS is already the base and loading it 4629 // reads ELEMENT 0 AND USES IT AS THE BASE. .lcomm zero-fills, so that base was 4630 // 0 and the first indexed access wrote to a near-null address -- compiles 4631 // clean, dies on touch, exactly as witness nx_rw_staticarr.nx records. 4632 // This is the static sibling of the local array-decay rule below. 4633 var g_is_arr: i64 = 0 4634 if L.ty.pointee != (0 as *Type) { 4635 if L.ty.pointee.kind == TY_ARRAY { g_is_arr = 1 } 4636 } 4637 if g_is_arr == 0 { 4638 v = ir_emit_load(P.current_bb, L.value_id, L.ty) 4639 } 4640 // Static-ptr STRIDE fix: inject_statics types the Local as *(e.ty) (the slot 4641 // ADDRESS). After the load, v holds the REAL pointer (e.ty); its element is 4642 // e.ty.pointee, so the stride must come from L.ty.pointee, not L.ty. Was: a *u8 4643 // static -> stride 8 (sizeof *u8) instead of 1 -> ran off the buffer on large 4644 // indices (nx_drbench SIGSEGV). *i64 statics were unaffected (8 == correct). 4645 // For the ARRAY case this same assignment hands the postfix handler the ARRAY 4646 // type, which is what makes it GEP from the base and bounds-check the index. 4647 if L.ty.pointee != (0 as *Type) { v_pass_ty = L.ty.pointee } 4648 } 4649 } 4650 } 4651 } 4652 } 4653 // Pass the local's actual type so parse_field_chain can 4654 // emit GEP+LOAD for o.field accesses. Closes T#bar3-codegen-001 4655 // (was passing 0 as *Type which made field_chain bail without 4656 // emitting, causing nxc.elf to return the raw pointer instead 4657 // of o.field for `let o: *S = ...; return o.field`). 4658 // Array DECAY: a bare array name (not indexed, not dotted) yields its stack ADDRESS as a 4659 // pointer VALUE (C-style array->*elem decay), so `sys_write(1, buf, n)` gets the address. 4660 // Emit OP_ADDR_OF for the leaq path -- returning the raw alloca id auto-loads the array's 4661 // first bytes as a bogus pointer (same trap the &name path documents). 4662 if L.is_alloca == 1 { 4663 if L.ty_kind == TY_ARRAY { 4664 if peek_kind(P) != TK_LBRACKET { 4665 if peek_kind(P) != TK_DOT { 4666 let pta: *Type = alloc_type(TY_PTR, 8, 8) 4667 pta.pointee = L.ty.pointee 4668 return ir_emit_unop(P.current_bb, OP_ADDR_OF, L.value_id, pta) 4669 } 4670 } 4671 } 4672 } 4673 // LN3 handshake: a `let`-local with visible provenance hands its extent to the chain 4674 // walker through the statics parse_field_chain snapshots at entry. Non-alloca only -- 4675 // for a `let`, v IS the binding's value id, so the id-equality key in the chain is exact. 4676 if L.is_alloca == 0 { 4677 if L.ext_bytes > 0 { g_pp_base_val = v; g_pp_base_ext = L.ext_bytes } 4678 } 4679 // LN2 handshake: the identifier token sits one back (parse_primary_ident consumed it and 4680 // nothing else before delegating here); the chain walker / `*p` reader checks v against it. 4681 if g_optenforce_live == 1 { 4682 if P.pos >= 1 { oe_handshake_set(P, L, v, tok_at(P.toks, P.pos - 1)) } 4683 } 4684 return parse_field_chain(P, v, v_pass_ty) 4685 } 4686 let out_raw: *u8 = sys_mmap(16) 4687 let out: *i64 = out_raw as *i64 4688 *out = 0 4689 if lookup_mconst(P, name, out) == 1 { 4690 return safe_const_i64(P, *out, "parse.nx:mconst-out" as *u8) 4691 } 4692 // LN39: an f64 const is a typed constant, exactly like an inline float literal. 4693 if lookup_mconst_f64(P, name, out) == 1 { 4694 let fvid: i64 = safe_const_i64(P, *out, "parse.nx:mconst-f64" as *u8) 4695 let fbase: i64 = P.current_fn.values as i64 4696 let fv: *Value = (fbase + fvid * 48) as *Value 4697 fv.ty = alloc_type(TY_F64, 8, 8) 4698 return fvid 4699 } 4700 // V-LANGEXT M1: string-const lookup. Same shape as inline TK_STRING 4701 // emission at parse_primary (~line 873): emit a VAL_GLOBAL_ADDR 4702 // value via ir_global_value with the stored pointer type. 4703 let sgid_raw: *u8 = sys_mmap(16) 4704 let sgid: *i64 = sgid_raw as *i64 4705 let sty_raw: *u8 = sys_mmap(16) 4706 let sty: **Type = sty_raw as **Type 4707 *sgid = 0 4708 *sty = 0 as *Type 4709 if lookup_mconst_string(P, name, sgid, sty) == 1 { 4710 // ROOT FIX seq1552 (2026-07-30): a STRING module-const IS a pointer value, so a use site can 4711 // legitimately carry a postfix `[i]` or `.field` exactly like a pointer local. This path used 4712 // to hand back the global address WITHOUT running the postfix chain, so the `[` `i` `]` tokens 4713 // of `CONST[i]` were left in the stream; the enclosing expression then desynced and the program 4714 // COMPILED CLEAN while summing the wrong bytes. Witness pair (byte-identical but for one line): 4715 // nx_constidx_probe.nx (WRONG-SUM, exit 1) vs nx_constidx_ctrl.nx (sum=294, exit 0). 4716 // The chain is entered ONLY when a postfix token actually follows, so every existing use site 4717 // takes the byte-identical old path -- and module-level contexts (P.current_fn == 0, see the 4718 // reset at the end of parse_function) never reach parse_field_chain's current_fn assert. 4719 let gcv: i64 = ir_global_value(P.current_fn, *sgid, *sty) 4720 if peek_kind(P) == TK_LBRACKET { return parse_field_chain(P, gcv, *sty) } 4721 if peek_kind(P) == TK_DOT { return parse_field_chain(P, gcv, *sty) } 4722 return gcv 4723 } 4724 // Bare FUNCTION name used as a VALUE (not a call): yields the function's code address 4725 // (VK_FUNC_ADDR -> `leaq <fn>(%rip)`). This is what nx_thread_pool passes as a worker arg and 4726 // what __thread_clone(top, worker, ctx) needs -- previously this fell through to `return 0` 4727 // (constant 0), giving a NULL entry -> the SIGSEGV (exit 139). Shares the &fn mechanism. 4728 var fv_nl: i64 = 0 4729 while name[fv_nl] != 0 { fv_nl = fv_nl + 1 } 4730 let fv_fn: *Function = find_function(P.module, name, fv_nl) 4731 if fv_fn != (0 as *Function) { 4732 let fv_ft: *Type = alloc_type(TY_FUNC, 8, 8) 4733 fv_ft.pointee = fv_fn.ret_ty 4734 return ir_func_addr_value(P.current_fn, fv_fn, fv_ft) 4735 } 4736 // ⚠UNKNOWN IDENTIFIER STILL FALLS THROUGH TO CONSTANT ZERO -- A KNOWN, UNFIXED HOLE. 4737 // Reaching here means `name` resolved to NOTHING: not a local, not a module const, not a string 4738 // const, not a function. Returning 0 silently emits the CONSTANT ZERO in its place, so a typo -- 4739 // or the banked case of a `let` local referenced ABOVE its declaration -- becomes 0 with no 4740 // diagnostic and SEGVs far from the cause. It is the same silent-value-id-0 class already killed 4741 // for undefined CALLS (parse_primary_call) and, just above, for bare function names. 4742 // 4743 // A fail-loud version WAS built and tested here 2026-07-20 and is NOT shipped, deliberately. 4744 // ★It paid for itself immediately: it caught `AT_FDCWD` being read as 0 by sys_unlinkat / 4745 // sys_fchmodat (a forward module-const reference) -- a live miscompile now fixed in 4746 // nx_syscalls.nx, with a standing witness at runtime/nx_fwdconst_probe.nx proving the class 4747 // (forward-read=0 vs direct-read=-100 in ONE binary). 4748 // ★But it also reports a FALSE POSITIVE that is not yet understood: `unknown identifier: m` while 4749 // parsing nx_opt.nx after `inl_clone_all`, where `m` is an ordinary FUNCTION PARAMETER 4750 // (`opt_inline_module(m: *Module)`) used normally -- i.e. find_local misses a live parameter in 4751 // some context. Shipping a compiler diagnostic whose false positive is unexplained would trade a 4752 // silent-wrong-value bug for a can't-build bug, so it stays out until that is root-caused. 4753 // NEXT RUNG: root-cause the find_local miss (print the enclosing function in the diagnostic to 4754 // self-locate it), then land this exactly like the call-arity check was landed. 4755 // 4756 // ---- WHAT *IS* SHIPPED: the FORWARD MODULE-CONST slice, caught precisely ----------------- 4757 // The blanket check is unsafe (above), but ONE slice of it is both safe and high value. If 4758 // `name` is declared as a module `const` LATER in the token stream, this is unambiguously the 4759 // forward-module-const miscompile -- the one that made AT_FDCWD read 0 instead of -100 and so 4760 // put dirfd=0 into sys_unlinkat/sys_fchmodat. No legitimate program has that shape, and it 4761 // CANNOT false-positive the way the blanket check does: an ordinary parameter (the `m` case) 4762 // is never also declared as a later const. Witness: runtime/nx_fwdconst_probe.nx. 4763 if mconst_declared_later(P, name) == 1 { 4764 // LN42 (2026-09-03): the last refusal in this file that printed a bare sentence with no location. 4765 // A hand-counted length beside each literal (52 and 68) was a second copy of the literal shape, the 4766 // exact drift LN27 removed elsewhere; nx_diag_puts measures the string itself. 4767 nx_diag_organ_at(nx_pd_line) 4768 nx_diag_puts(": this module const is used before it is declared: " as *u8) 4769 nx_diag_puts(name) 4770 nx_diag_puts("\n" as *u8) 4771 nx_diag_caret(nx_pd_line, nx_pd_col) 4772 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) 4773 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) 4774 nx_diag_note_error() 4775 return 0 4776 } 4777 // seq1012 FAIL-LOUD (landed 2026-07-29): an identifier that resolves to NOTHING is a 4778 // compile ERROR, never the silent constant 0 that wrote wrong data and relocated with 4779 // unrelated refactors. The 2026-07-20 attempt was withheld over ONE false positive 4780 // ("m" in fn inl_clone_all) -- the survey diagnostic located it as seq358 IN A MASK: 4781 // the multiline else-if desync fused functions, so live parameters resolved against the 4782 // wrong function. With the else-if production fixed, the whole corpus surveys CLEAN 4783 // (nx_compile_x86 + nx_wasm_craft + nx_game_page_emit: unresolved=0) and this check 4784 // lands exactly like the call-arity check did. Witness: runtime/nx_undefprobe.nx. 4785 // The caller consumed the identifier before resolution began, so its own token sits one 4786 // slot back (the parse_primary_call anchoring lesson: never take the line at error time 4787 // from the CURRENT position -- take the construct's own token). 4788 var uid_ln: i64 = 0 4789 var uid_cl: i64 = 0 4790 if P.pos > 0 { 4791 let uid_tok: *Tok = tok_at(P.toks, P.pos - 1) 4792 uid_ln = uid_tok.line 4793 uid_cl = uid_tok.col 4794 } 4795 nx_dym_suggest_ident(P, name, fv_nl) 4796 nx_diag_at(uid_ln) 4797 nx_diag_puts(": I do not know the name '" as *u8) 4798 sys_write(2, name, fv_nl) 4799 nx_diag_puts("'" as *u8) 4800 if P.current_fn != (0 as *Function) { 4801 nx_diag_puts(" (used inside the function '" as *u8) 4802 sys_write(2, P.current_fn.name_start as *u8, P.current_fn.name_len) 4803 nx_diag_puts("')" as *u8) 4804 } 4805 nx_diag_puts(" -- no variable, constant, or function defines it.\n" as *u8) 4806 nx_diag_caret(uid_ln, uid_cl) 4807 nx_diag_why_undefined() 4808 nx_dym_note2(P, name, fv_nl) 4809 nx_diag_note_error() 4810 return 0 4811} 4812 4813// Prefix-unary level. The trailing `as T` cast lives HERE (the 4814// wrapper), NOT inside the operand parse, so prefix operators bind 4815// TIGHTER than `as`: `*p as T` == `(*p) as T` (Rust precedence). 4816// Pre-fix, the deref branch's recursive operand parse swallowed the 4817// cast -- `*p as *u8` parsed as `*(p as *u8)` and byte-loaded the 4818// POINTER CELL, then deref'd that garbage (SIGSEGV). Min repro: 4819// runtime/_derefcast_minrepro.nx (the B2-row landmine, 2026-06-10). 4820func parse_unary(P: *Parser) -> i64 { 4821 let base: i64 = parse_unary_core(P) 4822 // Trailing `as T` cast. The cast RESULT must carry target_ty so downstream 4823 // LOAD/STORE/GEP see the correct pointee -- but the OPERAND must keep its own 4824 // type. This used to do `bv.ty = target_ty` IN PLACE on the operand's Value: 4825 // when the operand is a bare local, that node IS the local's binding, so 4826 // `p as i64` flipped p to INTEGER for EVERY later reference. Proven minimal 4827 // 2026-08-05 (nx_probe_castmut_live.nx: a prior cast-in-argument-position made 4828 // the pointer-into-integer refusal VANISH; discriminator castmut2 shows it is 4829 // per-value) -- and it is the ROOT of the 57/70 sys_munmap-arg-1 residue that 4830 // kept the reverse call-arg type direction disabled (see the do-not-enable 4831 // note in the call type check). Fix: emit a typed IDENTITY (base + 0) so the 4832 // cast result is a FRESH Value id with target_ty and a legitimate result slot 4833 // in every backend; the optimizer may fold the identity -- harmless, because 4834 // type checks run at parse time and load/store widths are instruction-encoded. 4835 // CHAINED CASTS (2026-08-13 night): `X as *u8 as i64` was HALF-APPLIED for as long as this 4836 // site has existed -- the single `if` consumed ONE cast and the dangling `as i64` fell into 4837 // the silent junk-skip, so the second cast never happened (right bits by accident for 4838 // ptr->int, wrong TYPE always). The new discarded-expression check turned that silence into 4839 // a refusal with a MISLEADING cause at 15 estate sites (nx_tool_run/nx_mgmt_data closure), 4840 // which is how the gap finally surfaced. A WHILE makes the chain real: each `as T` wraps the 4841 // previous value in a typed identity; bits unchanged, final TYPE = the last cast. 4842 var cast_v: i64 = base 4843 // LN6: remember what the chain's OPERAND was, so a call site can ask what a cast erased. 4844 var cast_src: *Type = 0 as *Type 4845 let bv0: *Value = val_at(P.current_fn, base) 4846 if bv0 != (0 as *Value) { cast_src = bv0.ty } 4847 while peek_kind(P) == TK_AS { 4848 advance_tok(P) 4849 let target_ty: *Type = parse_type(P) 4850 if target_ty == (0 as *Type) { break } 4851 let czero: i64 = safe_const_i64(P, 0, "parse.nx:cast-identity-const0" as *u8) 4852 // LN3: the cast's typed identity is a FRESH value id, so provenance recorded on the 4853 // operand (a sys_mmap result, or a prior link of this chain) must follow it -- this is 4854 // what makes the estate idiom `let s: *i64 = sys_mmap(N) as *i64` bindable. 4855 var pp_carry: i64 = 0 4856 if cast_v == g_prov_val { pp_carry = 1 } 4857 cast_v = ir_emit_binop(P.current_bb, OP_ADD, cast_v, czero, target_ty) 4858 if pp_carry == 1 { g_prov_val = cast_v } 4859 } 4860 if cast_v != base { g_last_cast_val = cast_v; g_last_cast_src_ty = cast_src } 4861 return cast_v 4862} 4863 4864func parse_unary_core(P: *Parser) -> i64 { 4865 nx_assert_ptr(P.current_fn as *u8, "parse_unary: P.current_fn" as *u8) 4866 nx_assert(P.current_fn.values_cap > 0, 4867 "parse_unary: P.current_fn init" as *u8) 4868 let k: i64 = peek_kind(P) 4869 if k == TK_MINUS { 4870 advance_tok(P) 4871 let rhs: i64 = parse_unary(P) 4872 let zero: i64 = safe_const_i64(P, 0, "parse.nx:LINE-const0" as *u8) 4873 return ir_emit_binop(P.current_bb, OP_SUB, zero, rhs, ir_type_i64()) 4874 } 4875 if k == TK_BANG { 4876 advance_tok(P) 4877 let rhs: i64 = parse_unary(P) 4878 let zero: i64 = safe_const_i64(P, 0, "parse.nx:LINE-const0" as *u8) 4879 return ir_emit_binop(P.current_bb, OP_EQ, rhs, zero, ir_type_bool()) 4880 } 4881 // ~expr -- bitwise NOT (one's complement). Parallels TK_BANG above; 4882 // lowers to the dedicated OP_NOT (single x86 `notq` / rv64 `not`), 4883 // the optimal one-instruction form. Was MISSING from the function- 4884 // body parser (only the const-evaluator handled `~`), so `~x` in a 4885 // function body fell through to parse_primary, silently miscompiled, 4886 // AND desynced the token stream -- which surfaced downstream as the 4887 // misleading "address-of unknown local (&name)" when a binary `&` 4888 // followed (e.g. SHA-256 Choose: `((~e) & g) ...`). Now `~` is a 4889 // first-class prefix operator like -, !, &, *. 4890 if k == TK_TILDE { 4891 advance_tok(P) 4892 let rhs: i64 = parse_unary(P) 4893 return ir_emit_unop(P.current_bb, OP_NOT, rhs, ir_type_i64()) 4894 } 4895 // &name[.field]* -- address-of, with optional field-chain walk. 4896 // Valid on alloca-backed vars; returns the alloca address (no 4897 // load) when there's no chain, or the GEP'd field address when 4898 // `.field.subfield...` follows. 4899 // 4900 // Extended 2026-05-16 per cardinal feedback-bits-up-canonical- 4901 // layer + user directive "never avoid a limitation bits up build": 4902 // previously rejected `&card.field`, forcing callers to inline 4903 // assignments. Now mirrors parse_stmt_ident_dot's chain walker 4904 // exactly, only returning the ADDRESS (no load, no store). 4905 // 4906 // STUB(parser, T#parser-001): non-alloca local case currently 4907 // absorbs the error by returning 0. Caller can't distinguish 4908 // "no such local" from "address-of-non-alloca-local"; either 4909 // path silently miscompiles. 4910 // Plan: distinguish via typed error sentinel or assert; depends 4911 // on a parser-wide error-reporting path that doesn't yet exist. 4912 // Closes when: parser grows a typed error channel. 4913 if k == TK_AMP { 4914 advance_tok(P) 4915 let name_tok: *Tok = advance_tok(P) 4916 let name: *u8 = tok_text_ptr(name_tok) 4917 let L: *Local = find_local(P, name) 4918 if L == (0 as *Local) { 4919 // Not a local -- maybe a FUNCTION: `&fn` yields the function's code address (VK_FUNC_ADDR), 4920 // emitted as `leaq <fn>(%rip), %reg`. Shares its mechanism with bare-fn-name-as-value. 4921 var amp_nl: i64 = 0 4922 while name[amp_nl] != 0 { amp_nl = amp_nl + 1 } 4923 let amp_fn: *Function = find_function(P.module, name, amp_nl) 4924 if amp_fn != (0 as *Function) { 4925 let amp_ft: *Type = alloc_type(TY_FUNC, 8, 8) 4926 amp_ft.pointee = amp_fn.ret_ty 4927 return ir_func_addr_value(P.current_fn, amp_fn, amp_ft) 4928 } 4929 parse_die("address-of unknown local (&name)" as *u8, 32) 4930 } 4931 // Bare `&name` (no .field chain): the ADDRESS of the local's 4932 // stack slot. Emit OP_ADDR_OF so codegen uses the as-address 4933 // (leaq) path; returning the raw alloca value-id auto-loads the 4934 // slot CONTENTS -> bogus pointer -> SEGV. Works for value-typed 4935 // (`var x: i64`) AND pointer-typed (`var p: *T`, out-params). 4936 if L.is_alloca == 1 { 4937 if peek_kind(P) != TK_DOT { 4938 let pt: *Type = alloc_type(TY_PTR, 8, 8) 4939 pt.pointee = L.ty 4940 return ir_emit_unop(P.current_bb, OP_ADDR_OF, L.value_id, pt) 4941 } 4942 } 4943 var v: i64 = L.value_id 4944 // Mirror the F-meta-3 fix in parse_stmt_ident_dot: when the 4945 // local is alloca-backed AND it's a pointer AND we're walking 4946 // a `.field` chain, load the pointer value out of the slot 4947 // first so GEP applies to the actual pointee, not the alloca. 4948 // 4949 // CRITICAL: do this ONLY when a `.field` chain follows. For 4950 // bare `&name` we want the ALLOCA ADDRESS (== L.value_id), 4951 // NOT the loaded pointer value. Pre-fix this overload broke 4952 // outparam patterns like `nx_media_pool_get(..., &src, ...)` 4953 // where the callee writes through the pointer expected to be 4954 // &src's slot -- the load made it write through src's value 4955 // (uninitialised 0 -> NULL deref). 4956 if L.is_alloca == 1 { 4957 if L.ty != (0 as *Type) { 4958 if L.ty.kind == TY_PTR { 4959 if peek_kind(P) == TK_DOT { 4960 v = ir_emit_load(P.current_bb, L.value_id, L.ty) 4961 } 4962 } 4963 } 4964 } 4965 var ty: *Type = L.ty 4966 while peek_kind(P) == TK_DOT { 4967 advance_tok(P) 4968 let ftok: *Tok = advance_tok(P) 4969 let fname: *u8 = tok_text_ptr(ftok) 4970 var flen: i64 = 0 4971 while fname[flen] != 0 { flen = flen + 1 } 4972 var stty: *Type = ty 4973 if stty != (0 as *Type) { 4974 if stty.kind == TY_PTR { 4975 if stty.pointee != (0 as *Type) { stty = stty.pointee } 4976 } 4977 } 4978 if stty == (0 as *Type) { break } 4979 let field: *StructField = parse_field_of_struct(P, stty, ftok, fname, flen) 4980 if field == (0 as *StructField) { break } 4981 let off2: i64 = safe_const_i64(P, field.offset, "parse.nx:amp-field-offset" as *u8) 4982 // `&f.b` is an ADDRESS, so its type is POINTER-to-field, not the field's own type. 4983 // ir_emit_gep uses its 4th arg DIRECTLY as the result value's type (nx_ir.nx:1216), so 4984 // passing field.ty typed &f.b as i64 -- an INTEGER. The bare `&name` path 40 lines above 4985 // already builds TY_PTR with pointee = L.ty; this chain path never did. 4986 // MEASURED 2026-08-10: the DEPLOYED compiler contains no "argument type mismatch" string 4987 // at all (grep -a, with `address-of unknown local` as the positive control), so it never 4988 // checked and this mistyping was invisible for as long as it has existed. The gauntlet's 4989 // addr_of_field tooth only started failing because the CHECK is new -- the BUG is old. 4990 // ★★★★★ A NEW CHECKER FAILING AN OLD TEST IS NOT A REGRESSION; IT IS THE FIRST TIME 4991 // ANYTHING LOOKED. Fix the type, not the checker. 4992 let fpt: *Type = alloc_type(TY_PTR, 8, 8) 4993 fpt.pointee = field.ty 4994 v = ir_emit_gep(P.current_bb, v, off2, fpt) 4995 // the chain walker derefs a TY_PTR itself, so keep `ty` as the field type for the next hop 4996 ty = field.ty 4997 } 4998 return v 4999 } 5000 // *expr -- pointer dereference in expression position. Loads 5001 // from the pointee. Compared with the `*name = expr` statement 5002 // path above, this one yields a value (not an lvalue). 5003 if k == TK_STAR { 5004 advance_tok(P) 5005 // Operand via CORE: a trailing `as` belongs to the deref 5006 // RESULT (wrapper), never to the address operand. 5007 let addr: i64 = parse_unary_core(P) 5008 oe_chain_check(P, addr) // LN2: `*p` reads through p 5009 // Result type = pointee of addr's type (NOT hardcoded i64). 5010 // Without this, `*(p: *u8)` loads 8 bytes instead of 1. 5011 let addr_val: *Value = val_at(P.current_fn, addr) 5012 var elem_ty: *Type = ir_type_i64() 5013 if addr_val.ty != (0 as *Type) { 5014 if addr_val.ty.kind == TY_PTR { 5015 if addr_val.ty.pointee != (0 as *Type) { 5016 elem_ty = addr_val.ty.pointee 5017 } 5018 } 5019 } 5020 return ir_emit_load(P.current_bb, addr, elem_ty) 5021 } 5022 // Trailing `as T` handling lives in the parse_unary wrapper 5023 // (required for `((p as i64) + i) as *u8` retype patterns -- 5024 // see wrapper comment for the deref-precedence defect this 5025 // placement fixes). 5026 return parse_primary(P) 5027} 5028 5029// Helpers for type-directed arithmetic op selection. When either 5030// operand is TY_F32 (f64 later), promote OP_ADD/SUB/MUL/DIV_S to 5031// their floating-point counterparts and carry TY_F32 through the 5032// result. Non-fp operands fall through to the integer ops + i64 5033// result type -- same as the pre-F-extension behavior. 5034 5035func fp_op_for_int(op: i64) -> i64 { 5036 if op == OP_ADD { return OP_FADD } 5037 if op == OP_SUB { return OP_FSUB } 5038 if op == OP_MUL { return OP_FMUL } 5039 if op == OP_DIV_S { return OP_FDIV } 5040 return op 5041} 5042 5043func has_fp_operand(P: *Parser, v: i64) -> i64 { 5044 if v < 0 { return 0 } 5045 if v >= P.current_fn.n_values { return 0 } 5046 let base: i64 = P.current_fn.values as i64 5047 let val: *Value = (base + v * 48) as *Value 5048 if val.ty == (0 as *Type) { return 0 } 5049 if val.ty.kind == TY_F32 { return 1 } 5050 if val.ty.kind == TY_F64 { return 1 } 5051 return 0 5052} 5053 5054func nx_value_type(P: *Parser, v: i64) -> *Type { 5055 if v < 0 { return 0 as *Type } 5056 if v >= P.current_fn.n_values { return 0 as *Type } 5057 let base: i64 = P.current_fn.values as i64 5058 let val: *Value = (base + v * 48) as *Value 5059 return val.ty 5060} 5061 5062// Result type of an INTEGER binop. When BOTH operands are 32-bit (i32/u32) the result is 5063// 32-bit -> the backend then WRAPS it mod 2^32 (else RV64 computes in 64 bits and u32 5064// arithmetic never overflows: 4e9+1e9 stayed 5e9 instead of 705032704). Signed only when 5065// BOTH are signed i32 (so the wrap sign-extends); otherwise unsigned u32 (zero-extend). 5066// Any i64 operand -> i64 result: this is exactly what keeps POINTER arithmetic (ptr + i32 5067// offset, ptr is i64) full-width instead of truncating the address. 5068func nx_value_is_const(P: *Parser, v: i64) -> i64 { 5069 if v < 0 { return 0 } 5070 if v >= P.current_fn.n_values { return 0 } 5071 let base: i64 = P.current_fn.values as i64 5072 let val: *Value = (base + v * 48) as *Value 5073 if val.kind == 0 { return 1 } // VK_CONST_INT 5074 return 0 5075}